2024-07-12 02:30:23 +02:00
from typing import Any
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
2021-03-22 05:18:35 +01:00
import responses
2020-03-28 13:22:19 +01:00
2020-06-11 00:54:34 +02:00
from version import ZULIP_VERSION
2022-04-14 23:53:15 +02:00
from zerver . actions . create_user import do_create_user
2022-11-03 15:35:07 +01:00
from zerver . actions . streams import do_deactivate_stream
2022-05-16 03:17:23 +02:00
from zerver . lib . exceptions import JsonableError
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 ,
2022-11-03 15:35:07 +01:00
fail_with_message ,
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
2021-03-16 07:54:33 +01:00
from zerver . lib . url_encoding import near_message_url
2020-03-28 13:22:19 +01:00
from zerver . lib . users import add_service
2023-12-15 03:57:04 +01:00
from zerver . models import Recipient , Service , UserProfile
2023-12-15 02:14:24 +01:00
from zerver . models . realms import get_realm
2023-12-15 03:57:04 +01:00
from zerver . models . streams import get_stream
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 :
2021-02-12 08:19:30 +01: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
2021-02-12 08:19:30 +01:00
2021-03-27 04:59:27 +01:00
def request_exception_error ( final_url : 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
2021-02-12 08:19:30 +01:00
2021-03-27 04:59:27 +01:00
def timeout_error ( final_url : 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
2021-02-12 08:19:30 +01:00
2021-03-27 04:59:27 +01:00
def connection_error ( final_url : Any , * * request_kwargs : Any ) - > Any :
2023-02-04 02:07:20 +01:00
raise requests . exceptions . ConnectionError
2018-10-10 19:52:32 +02:00
2021-02-12 08:19:30 +01:00
2016-07-23 08:13:33 +02:00
class DoRestCallTests ( ZulipTestCase ) :
2024-07-12 02:30:17 +02:00
def mock_event ( self , bot_user : UserProfile ) - > dict [ str , Any ] :
2020-07-24 19:21:42 +02:00
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.
2021-02-12 08:20:45 +01:00
" failed_tries " : 3 ,
" message " : {
" display_recipient " : " Verona " ,
" stream_id " : 999 ,
2021-03-27 03:11:40 +01:00
" 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 ,
2021-10-26 09:15:16 +02:00
" sender_email_address_visibility " : UserProfile . EMAIL_ADDRESS_VISIBILITY_EVERYONE ,
2021-03-27 03:11:40 +01:00
" recipient_type " : " stream " ,
" recipient_type_id " : 999 ,
" sender_is_mirror_dummy " : False ,
2021-02-12 08:20:45 +01:00
TOPIC_NAME : " Foo " ,
" id " : " " ,
" type " : " stream " ,
2021-03-27 03:11:40 +01:00
" timestamp " : 1 ,
2021-02-12 08:19:30 +01:00
} ,
2021-03-27 03:11:40 +01:00
" trigger " : " mention " ,
2021-02-12 08:20:45 +01:00
" user_profile_id " : bot_user . id ,
" command " : " " ,
" service_name " : " " ,
2021-02-12 08:19:30 +01:00
}
2017-09-25 16:03:35 +02:00
2020-07-24 18:46:14 +02:00
def test_successful_request ( self ) - > None :
2021-02-12 08:20:45 +01:00
bot_user = self . example_user ( " outgoing_webhook_bot " )
2020-07-24 19:21:42 +02:00
mock_event = self . mock_event ( bot_user )
service_handler = GenericOutgoingWebhookService ( " token " , bot_user , " service " )
2024-07-12 02:30:23 +02:00
def _helper ( content : str | None ) - > None :
2022-05-16 03:17:23 +02:00
expect_send_response = mock . patch ( " zerver.lib.outgoing_webhook.send_response_message " )
2024-07-12 02:30:32 +02:00
with (
mock . patch . object ( service_handler , " session " ) as session ,
expect_send_response as mock_send ,
) :
2022-05-16 03:17:23 +02:00
session . post . return_value = ResponseMock ( 200 , orjson . dumps ( dict ( content = content ) ) )
2021-04-29 00:28:43 +02:00
with self . assertLogs ( level = " INFO " ) as logs :
2022-05-16 03:17:23 +02:00
do_rest_call ( " " , mock_event , service_handler )
2021-05-17 05:41:32 +02:00
self . assert_length ( logs . output , 1 )
2021-04-29 00:28:43 +02:00
self . assertIn (
f " Outgoing webhook request from { bot_user . id } @zulip took " , logs . output [ 0 ]
)
2022-05-16 03:17:23 +02:00
if content == " " :
self . assertFalse ( mock_send . called )
else :
self . assertTrue ( mock_send . called )
for service_class in [ GenericOutgoingWebhookService , SlackOutgoingWebhookService ] :
handler = service_class ( " token " , bot_user , " service " )
with mock . patch . object ( handler , " session " ) as session :
session . post . return_value = ResponseMock ( 200 , b " {} " )
with self . assertLogs ( level = " INFO " ) as logs :
do_rest_call ( " " , mock_event , handler )
self . assert_length ( logs . output , 1 )
self . assertIn (
f " Outgoing webhook request from { bot_user . id } @zulip took " , logs . output [ 0 ]
)
session . post . assert_called_once ( )
_helper ( " whatever " )
_helper ( " " )
_helper ( None )
2018-10-11 14:11:52 +02:00
2020-07-24 19:00:11 +02:00
def test_retry_request ( self ) - > None :
2021-02-12 08:20:45 +01:00
bot_user = self . example_user ( " outgoing_webhook_bot " )
2020-07-24 19:21:42 +02:00
mock_event = self . mock_event ( bot_user )
service_handler = GenericOutgoingWebhookService ( " token " , bot_user , " service " )
2024-07-12 02:30:32 +02:00
with (
mock . patch . object ( service_handler , " session " ) as session ,
self . assertLogs ( level = " WARNING " ) as m ,
) :
2021-03-27 04:59:27 +01:00
session . post . return_value = ResponseMock ( 500 )
2021-03-27 03:11:40 +01:00
final_response = do_rest_call ( " " , mock_event , service_handler )
2020-10-29 20:21:18 +01:00
assert final_response is not None
2017-08-29 15:21:25 +02:00
2021-02-12 08:19:30 +01:00
self . assertEqual (
m . output ,
[
2024-10-04 16:54:16 +02:00
f ' WARNING:root:Message http://zulip.testserver/#narrow/channel/999-Verona/topic/Foo/near/ triggered an outgoing webhook, returning status code 500. \n Content of response (in quotes): " { final_response . text } " '
2021-02-12 08:19:30 +01:00
] ,
)
2020-07-24 18:24:28 +02:00
bot_owner_notification = self . get_last_message ( )
2021-02-12 08:19:30 +01:00
self . assertEqual (
bot_owner_notification . content ,
2024-10-04 16:54:16 +02:00
""" [A message](http://zulip.testserver/#narrow/channel/999-Verona/topic/Foo/near/) to your bot @_**Outgoing Webhook** triggered an outgoing webhook.
2021-02-12 08:20:45 +01:00
The webhook got a response with status code * 500 * . """ ,
2021-02-12 08:19:30 +01:00
)
2020-07-24 19:00:11 +02:00
2020-07-24 19:21:42 +02:00
assert bot_user . bot_owner is not None
2021-02-02 14:09:11 +01:00
self . assertEqual ( bot_owner_notification . recipient_id , bot_user . bot_owner . recipient_id )
2016-07-23 08:13:33 +02:00
2022-05-16 03:17:23 +02:00
def test_bad_msg_type ( self ) - > None :
bot_user = self . example_user ( " outgoing_webhook_bot " )
mock_event = self . mock_event ( bot_user )
service_handler = GenericOutgoingWebhookService ( " token " , bot_user , " service " )
mock_event [ " message " ] [ " type " ] = " unknown "
2024-07-12 02:30:32 +02:00
with (
mock . patch . object ( service_handler , " session " ) as session ,
self . assertRaises ( JsonableError ) ,
self . assertLogs ( level = " INFO " ) ,
) :
2022-05-16 03:17:23 +02:00
session . post . return_value = ResponseMock ( 200 )
url = " http://somewhere.com/api/call "
with mock . patch ( " zerver.lib.outgoing_webhook.get_message_url " , return_value = url ) :
do_rest_call ( " " , mock_event , service_handler )
def test_response_none ( self ) - > None :
bot_user = self . example_user ( " outgoing_webhook_bot " )
mock_event = self . mock_event ( bot_user )
service_handler = GenericOutgoingWebhookService ( " token " , bot_user , " service " )
2024-07-12 02:30:32 +02:00
with (
mock . patch (
" zerver.lib.outgoing_webhook.GenericOutgoingWebhookService.make_request " ,
return_value = None ,
) ,
self . assertLogs ( level = " INFO " ) as logs ,
) :
2022-05-16 03:17:23 +02:00
resp = do_rest_call ( " " , mock_event , service_handler )
self . assertEqual ( resp , None )
self . assert_length ( logs . output , 1 )
2020-07-24 18:46:14 +02:00
def test_fail_request ( self ) - > None :
2021-02-12 08:20:45 +01:00
bot_user = self . example_user ( " outgoing_webhook_bot " )
2020-07-24 19:21:42 +02:00
mock_event = self . mock_event ( bot_user )
service_handler = GenericOutgoingWebhookService ( " token " , bot_user , " service " )
2020-07-24 18:46:14 +02:00
expect_fail = mock . patch ( " zerver.lib.outgoing_webhook.fail_with_message " )
2024-07-12 02:30:32 +02:00
with (
mock . patch . object ( service_handler , " session " ) as session ,
expect_fail as mock_fail ,
self . assertLogs ( level = " WARNING " ) as m ,
) :
2021-03-27 04:59:27 +01:00
session . post . return_value = ResponseMock ( 400 )
2021-03-27 03:11:40 +01:00
final_response = do_rest_call ( " " , mock_event , service_handler )
2020-10-29 20:21:18 +01:00
assert final_response is not None
2021-02-12 08:19:30 +01:00
self . assertEqual (
m . output ,
[
2024-10-04 16:54:16 +02:00
f ' WARNING:root:Message http://zulip.testserver/#narrow/channel/999-Verona/topic/Foo/near/ triggered an outgoing webhook, returning status code 400. \n Content of response (in quotes): " { final_response . text } " '
2021-02-12 08:19:30 +01:00
] ,
)
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 ( )
2021-02-12 08:19:30 +01:00
self . assertEqual (
bot_owner_notification . content ,
2024-10-04 16:54:16 +02:00
""" [A message](http://zulip.testserver/#narrow/channel/999-Verona/topic/Foo/near/) to your bot @_**Outgoing Webhook** triggered an outgoing webhook.
2021-02-12 08:20:45 +01:00
The webhook got a response with status code * 400 * . """ ,
2021-02-12 08:19:30 +01:00
)
2020-07-24 19:00:11 +02:00
2020-07-24 19:21:42 +02:00
assert bot_user . bot_owner is not None
2021-02-02 14:09:11 +01:00
self . assertEqual ( bot_owner_notification . recipient_id , bot_user . bot_owner . recipient_id )
2016-07-23 08:13:33 +02:00
2019-01-09 15:29:17 +01:00
def test_headers ( self ) - > None :
2021-02-12 08:20:45 +01:00
bot_user = self . example_user ( " outgoing_webhook_bot " )
2020-07-24 19:21:42 +02:00
mock_event = self . mock_event ( bot_user )
service_handler = GenericOutgoingWebhookService ( " token " , bot_user , " service " )
2021-03-27 04:59:27 +01:00
session = service_handler . session
with mock . patch . object ( session , " send " ) as mock_send :
2021-04-25 21:18:42 +02:00
mock_send . return_value = ResponseMock ( 200 , b " {} " )
2021-04-29 00:28:43 +02:00
with self . assertLogs ( level = " INFO " ) as logs :
final_response = do_rest_call ( " https://example.com/ " , mock_event , service_handler )
2020-10-29 20:21:18 +01:00
assert final_response is not None
2021-05-17 05:41:32 +02:00
self . assert_length ( logs . output , 1 )
2021-04-29 00:28:43 +02:00
self . assertIn (
f " Outgoing webhook request from { bot_user . id } @zulip took " , logs . output [ 0 ]
)
2021-03-27 04:14:41 +01:00
mock_send . assert_called_once ( )
prepared_request = mock_send . call_args [ 0 ] [ 0 ]
user_agent = " ZulipOutgoingWebhook/ " + ZULIP_VERSION
headers = {
" Content-Type " : " application/json " ,
" User-Agent " : user_agent ,
}
self . assertLessEqual ( headers . items ( ) , prepared_request . headers . items ( ) )
2019-01-09 15:29:17 +01:00
2018-10-10 19:52:32 +02:00
def test_error_handling ( self ) - > None :
2021-02-12 08:20:45 +01:00
bot_user = self . example_user ( " outgoing_webhook_bot " )
2020-10-29 20:21:18 +01:00
mock_event = self . mock_event ( bot_user )
service_handler = GenericOutgoingWebhookService ( " token " , bot_user , " service " )
2021-02-12 08:20:45 +01:00
bot_user_email = self . example_user_map [ " outgoing_webhook_bot " ]
2020-10-29 20:21:18 +01:00
2018-10-10 19:52:32 +02:00
def helper ( side_effect : Any , error_text : str ) - > None :
2021-03-27 04:59:27 +01:00
with mock . patch . object ( service_handler , " session " ) as session :
session . post . side_effect = side_effect
2021-03-27 03:11:40 +01:00
do_rest_call ( " " , 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 )
2021-02-12 08:20:45 +01:00
self . assertIn ( " triggered " , bot_owner_notification . content )
2020-07-24 19:21:42 +02:00
assert bot_user . bot_owner is not None
2021-02-02 14:09:11 +01:00
self . assertEqual ( bot_owner_notification . recipient_id , bot_user . bot_owner . recipient_id )
2018-10-10 19:52:32 +02:00
2020-10-29 20:21:18 +01:00
with self . assertLogs ( level = " INFO " ) as i :
2021-05-05 09:22:41 +02:00
helper ( side_effect = timeout_error , error_text = " Request timed out after " )
2021-02-12 08:20:45 +01:00
helper ( side_effect = connection_error , error_text = " A connection error occurred. " )
2020-10-29 20:21:18 +01:00
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 " ,
2021-02-12 08:19:30 +01:00
f " WARNING:root:Maximum retries exceeded for trigger: { bot_user_email } event: { mock_event [ ' command ' ] } " ,
2020-10-29 20:21:18 +01:00
]
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 :
2021-02-12 08:20:45 +01:00
bot_user = self . example_user ( " outgoing_webhook_bot " )
2020-07-24 19:21:42 +02:00
mock_event = self . mock_event ( bot_user )
service_handler = GenericOutgoingWebhookService ( " token " , bot_user , " service " )
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.
2024-07-12 02:30:32 +02:00
with (
mock . patch . object ( service_handler , " session " ) as session ,
expect_logging_exception ,
expect_fail as mock_fail ,
) :
2021-03-27 04:59:27 +01:00
session . post . side_effect = request_exception_error
2021-03-27 03:11:40 +01:00
do_rest_call ( " " , 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 ( )
2021-02-12 08:19:30 +01:00
self . assertEqual (
bot_owner_notification . content ,
2024-10-04 16:54:16 +02:00
""" [A message](http://zulip.testserver/#narrow/channel/999-Verona/topic/Foo/near/) to your bot @_**Outgoing Webhook** 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 :(
2021-02-12 08:20:45 +01:00
` ` ` """ ,
2021-02-12 08:19:30 +01:00
)
2020-07-24 19:21:42 +02:00
assert bot_user . bot_owner is not None
2021-02-02 14:09:11 +01:00
self . assertEqual ( bot_owner_notification . recipient_id , bot_user . bot_owner . recipient_id )
2017-11-03 13:38:49 +01:00
2021-03-22 05:18:35 +01:00
def test_jsonable_exception ( self ) - > None :
bot_user = self . example_user ( " outgoing_webhook_bot " )
mock_event = self . mock_event ( bot_user )
service_handler = GenericOutgoingWebhookService ( " token " , bot_user , " service " )
# The "widget_content" key is required to be a string which is
# itself JSON-encoded; passing arbitrary text data in it will
# cause the hook to fail.
response = { " content " : " whatever " , " widget_content " : " test " }
expect_logging_info = self . assertLogs ( level = " INFO " )
expect_fail = mock . patch ( " zerver.lib.outgoing_webhook.fail_with_message " )
with responses . RequestsMock ( assert_all_requests_are_fired = True ) as requests_mock :
requests_mock . add (
requests_mock . POST , " https://example.zulip.com " , status = 200 , json = response
)
with expect_logging_info , expect_fail as mock_fail :
do_rest_call ( " https://example.zulip.com " , mock_event , service_handler )
self . assertTrue ( mock_fail . called )
bot_owner_notification = self . get_last_message ( )
self . assertEqual (
bot_owner_notification . content ,
2024-10-04 16:54:16 +02:00
""" [A message](http://zulip.testserver/#narrow/channel/999-Verona/topic/Foo/near/) to your bot @_**Outgoing Webhook** triggered an outgoing webhook.
2021-03-22 05:18:35 +01:00
The outgoing webhook server attempted to send a message in Zulip , but that request resulted in the following error :
2021-05-11 22:53:58 +02:00
> Widgets : API programmer sent invalid JSON content \nThe response contains the following payload : \n ` ` ` \n ' { " content " : " whatever " , " widget_content " : " test " } ' \n ` ` ` """ ,
2021-03-22 05:18:35 +01:00
)
assert bot_user . bot_owner is not None
self . assertEqual ( bot_owner_notification . recipient_id , bot_user . bot_owner . recipient_id )
2021-04-25 20:54:32 +02:00
def test_invalid_response_format ( self ) - > None :
bot_user = self . example_user ( " outgoing_webhook_bot " )
mock_event = self . mock_event ( bot_user )
service_handler = GenericOutgoingWebhookService ( " token " , bot_user , " service " )
expect_logging_info = self . assertLogs ( level = " INFO " )
expect_fail = mock . patch ( " zerver.lib.outgoing_webhook.fail_with_message " )
with responses . RequestsMock ( assert_all_requests_are_fired = True ) as requests_mock :
# We mock the endpoint to return response with valid json which doesn't
# translate to a dict like is expected,
requests_mock . add (
requests_mock . POST , " https://example.zulip.com " , status = 200 , json = True
)
with expect_logging_info , expect_fail as mock_fail :
do_rest_call ( " https://example.zulip.com " , mock_event , service_handler )
self . assertTrue ( mock_fail . called )
bot_owner_notification = self . get_last_message ( )
self . assertEqual (
bot_owner_notification . content ,
2024-10-04 16:54:16 +02:00
""" [A message](http://zulip.testserver/#narrow/channel/999-Verona/topic/Foo/near/) to your bot @_**Outgoing Webhook** triggered an outgoing webhook.
2021-04-25 20:54:32 +02:00
The outgoing webhook server attempted to send a message in Zulip , but that request resulted in the following error :
2021-05-11 22:53:58 +02:00
> Invalid response format \nThe response contains the following payload : \n ` ` ` \n ' true ' \n ` ` ` """ ,
2021-04-25 20:54:32 +02:00
)
assert bot_user . bot_owner is not None
self . assertEqual ( bot_owner_notification . recipient_id , bot_user . bot_owner . recipient_id )
2021-04-25 21:23:52 +02:00
def test_invalid_json_in_response ( self ) - > None :
bot_user = self . example_user ( " outgoing_webhook_bot " )
mock_event = self . mock_event ( bot_user )
service_handler = GenericOutgoingWebhookService ( " token " , bot_user , " service " )
expect_logging_info = self . assertLogs ( level = " INFO " )
expect_fail = mock . patch ( " zerver.lib.outgoing_webhook.fail_with_message " )
with responses . RequestsMock ( assert_all_requests_are_fired = True ) as requests_mock :
# We mock the endpoint to return response with a body which isn't valid json.
requests_mock . add (
requests_mock . POST ,
" https://example.zulip.com " ,
status = 200 ,
body = " this isn ' t valid json " ,
)
with expect_logging_info , expect_fail as mock_fail :
do_rest_call ( " https://example.zulip.com " , mock_event , service_handler )
self . assertTrue ( mock_fail . called )
bot_owner_notification = self . get_last_message ( )
self . assertEqual (
bot_owner_notification . content ,
2024-10-04 16:54:16 +02:00
""" [A message](http://zulip.testserver/#narrow/channel/999-Verona/topic/Foo/near/) to your bot @_**Outgoing Webhook** triggered an outgoing webhook.
2021-04-25 21:23:52 +02:00
The outgoing webhook server attempted to send a message in Zulip , but that request resulted in the following error :
2021-05-11 22:53:58 +02:00
> Invalid JSON in response \nThe response contains the following payload : \n ` ` ` \n " this isn ' t valid json " \n ` ` ` """ ,
2021-04-25 21:23:52 +02:00
)
assert bot_user . bot_owner is not None
self . assertEqual ( bot_owner_notification . recipient_id , bot_user . bot_owner . recipient_id )
2021-02-12 08:19:30 +01:00
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 (
2021-02-12 08:20:45 +01:00
" outgoing-webhook " ,
2020-03-28 12:25:37 +01:00
bot_owner ,
2021-02-12 08:20:45 +01:00
full_name = " Outgoing Webhook bot " ,
2020-03-28 12:25:37 +01:00
bot_type = UserProfile . OUTGOING_WEBHOOK_BOT ,
2021-02-12 08:20:45 +01:00
service_name = " foo-service " ,
2021-05-07 03:00:26 +02:00
payload_url = ' " https://bot.example.com/ " ' ,
2020-03-28 12:25:37 +01:00
)
2017-11-03 13:38:49 +01:00
2021-05-07 03:00:26 +02:00
@responses.activate
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 ,
2021-02-12 08:20:45 +01:00
full_name = " Outgoing Webhook Bot " ,
email = " whatever " ,
2020-03-28 13:22:19 +01:00
realm = bot_owner . realm ,
password = None ,
2021-02-06 14:27:06 +01:00
acting_user = None ,
2020-03-28 13:22:19 +01:00
)
add_service (
2021-02-12 08:20:45 +01:00
" weather " ,
2020-03-28 13:22:19 +01:00
user_profile = bot ,
interface = Service . GENERIC ,
2021-05-07 03:00:26 +02:00
base_url = " https://weather.example.com/ " ,
2021-02-12 08:20:45 +01:00
token = " weather_token " ,
2020-03-28 13:22:19 +01:00
)
add_service (
2021-02-12 08:20:45 +01:00
" qotd " ,
2020-03-28 13:22:19 +01:00
user_profile = bot ,
interface = Service . GENERIC ,
2021-05-07 03:00:26 +02:00
base_url = " https://qotd.example.com/ " ,
2021-02-12 08:20:45 +01:00
token = " qotd_token " ,
2020-03-28 13:22:19 +01:00
)
sender = self . example_user ( " hamlet " )
2021-05-07 03:00:26 +02:00
responses . add (
responses . POST ,
" https://weather.example.com/ " ,
json = { } ,
)
responses . add (
responses . POST ,
" https://qotd.example.com/ " ,
json = { } ,
2020-03-28 13:22:19 +01:00
)
2021-05-07 03:00:26 +02:00
with self . assertLogs ( level = " INFO " ) as logs :
self . send_personal_message (
sender ,
bot ,
content = " some content " ,
)
2021-05-17 05:41:32 +02:00
self . assert_length ( logs . output , 2 )
2021-05-07 03:00:26 +02:00
self . assertIn ( f " Outgoing webhook request from { bot . id } @zulip took " , logs . output [ 0 ] )
self . assertIn ( f " Outgoing webhook request from { bot . id } @zulip took " , logs . output [ 1 ] )
2021-05-17 05:41:32 +02:00
self . assert_length ( responses . calls , 2 )
2021-05-07 03:00:26 +02:00
calls_by_url = {
call . request . url : orjson . loads ( call . request . body or b " " ) for call in responses . calls
}
weather_req = calls_by_url [ " https://weather.example.com/ " ]
self . assertEqual ( weather_req [ " token " ] , " weather_token " )
self . assertEqual ( weather_req [ " message " ] [ " content " ] , " some content " )
self . assertEqual ( weather_req [ " message " ] [ " sender_id " ] , sender . id )
2020-03-28 13:22:19 +01:00
2021-05-07 03:00:26 +02:00
qotd_req = calls_by_url [ " https://qotd.example.com/ " ]
self . assertEqual ( qotd_req [ " token " ] , " qotd_token " )
self . assertEqual ( qotd_req [ " message " ] [ " content " ] , " some content " )
self . assertEqual ( qotd_req [ " message " ] [ " sender_id " ] , sender . id )
@responses.activate
2021-03-27 04:59:27 +01:00
def test_pm_to_outgoing_webhook_bot ( self ) - > 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
2021-05-07 03:00:26 +02:00
responses . add (
responses . POST ,
" https://bot.example.com/ " ,
json = { " response_string " : " Hidley ho, I ' m a webhook responding! " } ,
2021-03-27 04:59:27 +01:00
)
2021-05-07 03:00:26 +02:00
with self . assertLogs ( level = " INFO " ) as logs :
self . send_personal_message ( sender , bot , content = " foo " )
2021-05-17 05:41:32 +02:00
self . assert_length ( logs . output , 1 )
2021-04-29 00:28:43 +02:00
self . assertIn ( f " Outgoing webhook request from { bot . id } @zulip took " , logs . output [ 0 ] )
2021-05-17 05:41:32 +02:00
self . assert_length ( responses . calls , 1 )
2021-05-07 03:00:26 +02:00
2017-11-03 13:38:49 +01:00
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
2021-05-07 03:00:26 +02:00
@responses.activate
2021-03-16 07:54:33 +01:00
def test_pm_to_outgoing_webhook_bot_for_407_error_code ( self ) - > None :
bot_owner = self . example_user ( " othello " )
bot = self . create_outgoing_bot ( bot_owner )
sender = self . example_user ( " hamlet " )
realm = get_realm ( " zulip " )
2021-05-07 03:00:26 +02:00
responses . add ( responses . POST , " https://bot.example.com/ " , status = 407 , body = " " )
2021-03-16 07:54:33 +01:00
expect_fail = mock . patch ( " zerver.lib.outgoing_webhook.fail_with_message " )
2021-05-07 03:00:26 +02:00
with expect_fail as mock_fail , self . assertLogs ( level = " WARNING " ) :
2021-03-16 07:54:33 +01:00
message_id = self . send_personal_message ( sender , bot , content = " foo " )
2021-05-17 05:41:32 +02:00
self . assert_length ( responses . calls , 1 )
2021-05-07 03:00:26 +02:00
2021-03-16 07:54:33 +01:00
# create message dict to get the message url
message = {
" display_recipient " : [ { " id " : bot . id } , { " id " : sender . id } ] ,
" stream_id " : 999 ,
TOPIC_NAME : " Foo " ,
" id " : message_id ,
" type " : " " ,
}
message_url = near_message_url ( realm , message )
last_message = self . get_last_message ( )
self . assertEqual (
last_message . content ,
f " [A message]( { message_url } ) to your bot @_** { bot . full_name } ** triggered an outgoing webhook. \n "
" The URL configured for the webhook is for a private or disallowed network. " ,
)
self . assertEqual ( last_message . sender_id , bot . id )
self . assertEqual (
last_message . recipient . type_id ,
bot_owner . id ,
)
self . assertEqual (
last_message . recipient . type ,
Recipient . PERSONAL ,
)
self . assertTrue ( mock_fail . called )
2021-05-07 03:00:26 +02:00
@responses.activate
2021-03-27 04:59:27 +01:00
def test_stream_message_to_outgoing_webhook_bot ( self ) - > None :
2020-03-28 12:25:37 +01:00
bot_owner = self . example_user ( " othello " )
bot = self . create_outgoing_bot ( bot_owner )
2021-05-07 03:00:26 +02:00
responses . add (
responses . POST ,
" https://bot.example.com/ " ,
json = { " response_string " : " Hidley ho, I ' m a webhook responding! " } ,
2021-02-12 08:19:30 +01:00
)
2021-05-07 03:00:26 +02:00
with self . assertLogs ( level = " INFO " ) as logs :
self . send_stream_message (
bot_owner , " Denmark " , content = f " @** { bot . full_name } ** foo " , topic_name = " bar "
)
2021-05-17 05:41:32 +02:00
self . assert_length ( responses . calls , 1 )
2021-05-07 03:00:26 +02:00
2021-05-17 05:41:32 +02:00
self . assert_length ( logs . output , 1 )
2021-04-29 00:28:43 +02:00
self . assertIn ( f " Outgoing webhook request from { bot . id } @zulip took " , logs . output [ 0 ] )
2017-11-03 13:38:49 +01:00
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 " )
2023-07-16 12:08:57 +02:00
self . assert_message_stream_name ( last_message , " Denmark " )
2021-05-13 15:09:58 +02:00
2022-11-03 17:12:33 +01:00
@responses.activate
def test_stream_message_failure_to_outgoing_webhook_bot ( self ) - > None :
realm = get_realm ( " zulip " )
bot_owner = self . example_user ( " othello " )
bot = self . create_outgoing_bot ( bot_owner )
responses . add (
responses . POST ,
" https://bot.example.com/ " ,
body = requests . exceptions . Timeout ( " Time is up! " ) ,
)
with self . assertLogs ( level = " INFO " ) as logs :
sent_message_id = self . send_stream_message (
bot_owner , " Denmark " , content = f " @** { bot . full_name } ** foo " , topic_name = " bar "
)
self . assert_length ( responses . calls , 4 )
self . assert_length ( logs . output , 5 )
self . assertEqual (
[
" INFO:root:Trigger event @**Outgoing Webhook bot** foo on foo-service timed out. Retrying " ,
f " INFO:root:Trigger event @** { bot . full_name } ** foo on foo-service timed out. Retrying " ,
f " INFO:root:Trigger event @** { bot . full_name } ** foo on foo-service timed out. Retrying " ,
f " INFO:root:Trigger event @** { bot . full_name } ** foo on foo-service timed out. Retrying " ,
f " WARNING:root:Maximum retries exceeded for trigger:outgoing-webhook-bot@zulip.testserver event:@** { bot . full_name } ** foo " ,
] ,
logs . output ,
)
last_message = self . get_last_message ( )
message_dict = {
" stream_id " : get_stream ( " Denmark " , realm ) . id ,
" display_recipient " : " Denmark " ,
TOPIC_NAME : " bar " ,
" id " : sent_message_id ,
" type " : " stream " ,
}
message_url = near_message_url ( realm , message_dict )
self . assertEqual (
last_message . content ,
f " [A message]( { message_url } ) to your bot @_** { bot . full_name } ** triggered an outgoing webhook. \n "
" Request timed out after 10 seconds. " ,
)
self . assertEqual ( last_message . sender_id , bot . id )
assert bot . bot_owner is not None
self . assertEqual ( last_message . recipient_id , bot . bot_owner . recipient_id )
stream_message = self . get_second_to_last_message ( )
self . assertEqual ( stream_message . content , " Failure! Bot is unavailable " )
self . assertEqual ( stream_message . sender_id , bot . id )
self . assertEqual ( stream_message . topic_name ( ) , " bar " )
2023-07-16 12:08:57 +02:00
self . assert_message_stream_name ( stream_message , " Denmark " )
2022-11-03 17:12:33 +01:00
2022-11-03 15:35:07 +01:00
@responses.activate
def test_stream_message_failure_deactivated_to_outgoing_webhook_bot ( self ) - > None :
bot_owner = self . example_user ( " othello " )
bot = self . create_outgoing_bot ( bot_owner )
2024-07-12 02:30:17 +02:00
def wrapped ( event : dict [ str , Any ] , failure_message : str ) - > None :
2022-11-03 15:35:07 +01:00
do_deactivate_stream ( get_stream ( " Denmark " , get_realm ( " zulip " ) ) , acting_user = None )
fail_with_message ( event , failure_message )
responses . add (
responses . POST ,
" https://bot.example.com/ " ,
body = requests . exceptions . Timeout ( " Time is up! " ) ,
)
2024-07-14 20:30:42 +02:00
with (
mock . patch (
" zerver.lib.outgoing_webhook.fail_with_message " , side_effect = wrapped
) as fail ,
self . assertLogs ( level = " INFO " ) as logs ,
) :
self . send_stream_message (
bot_owner , " Denmark " , content = f " @** { bot . full_name } ** foo " , topic_name = " bar "
)
2022-11-03 15:35:07 +01:00
self . assert_length ( logs . output , 5 )
fail . assert_called_once ( )
last_message = self . get_last_message ( )
self . assertIn ( " Request timed out after 10 seconds " , last_message . content )
prev_message = self . get_second_to_last_message ( )
self . assertIn (
2024-04-16 17:58:08 +02:00
" tried to send a message to channel #**Denmark**, but that channel does not exist " ,
2022-11-03 15:35:07 +01:00
prev_message . content ,
)
2021-05-13 15:09:58 +02:00
@responses.activate
def test_empty_string_json_as_response_to_outgoing_webhook_request ( self ) - > None :
"""
Verifies that if the response to the request triggered by mentioning the bot
is the json representation of the empty string , the outcome is the same
as { " response_not_required " : True } - since this behavior is kept for
backwards - compatibility .
"""
bot_owner = self . example_user ( " othello " )
bot = self . create_outgoing_bot ( bot_owner )
responses . add (
responses . POST ,
" https://bot.example.com/ " ,
json = " " ,
)
with self . assertLogs ( level = " INFO " ) as logs :
stream_message_id = self . send_stream_message (
bot_owner , " Denmark " , content = f " @** { bot . full_name } ** foo " , topic_name = " bar "
)
2021-05-17 05:41:32 +02:00
self . assert_length ( responses . calls , 1 )
2021-05-13 15:09:58 +02:00
2021-05-17 05:41:32 +02:00
self . assert_length ( logs . output , 1 )
2021-05-13 15:09:58 +02:00
self . assertIn ( f " Outgoing webhook request from { bot . id } @zulip took " , logs . output [ 0 ] )
# We verify that no new message was sent, since that's the behavior implied
# by the response_not_required option.
last_message = self . get_last_message ( )
self . assertEqual ( last_message . id , stream_message_id )