mirror of
https://github.com/caronc/apprise.git
synced 2025-01-05 21:49:02 +01:00
Added Matrix Attachment support (#921)
This commit is contained in:
parent
c56d4c500d
commit
207db69e1a
@ -53,8 +53,11 @@ from ..utils import validate_regex
|
||||
from ..AppriseLocale import gettext_lazy as _
|
||||
|
||||
# Define default path
|
||||
MATRIX_V2_API_PATH = '/_matrix/client/r0'
|
||||
MATRIX_V1_WEBHOOK_PATH = '/api/v1/matrix/hook'
|
||||
MATRIX_V2_API_PATH = '/_matrix/client/r0'
|
||||
MATRIX_V3_API_PATH = '/_matrix/client/v3'
|
||||
MATRIX_V3_MEDIA_PATH = '/_matrix/media/v3'
|
||||
MATRIX_V2_MEDIA_PATH = '/_matrix/media/r0'
|
||||
|
||||
# Extend HTTP Error Messages
|
||||
MATRIX_HTTP_ERROR_MAP = {
|
||||
@ -88,6 +91,21 @@ MATRIX_MESSAGE_TYPES = (
|
||||
)
|
||||
|
||||
|
||||
class MatrixVersion:
|
||||
# Version 2
|
||||
V2 = "2"
|
||||
|
||||
# Version 3
|
||||
V3 = "3"
|
||||
|
||||
|
||||
# webhook modes are placed into this list for validation purposes
|
||||
MATRIX_VERSIONS = (
|
||||
MatrixVersion.V2,
|
||||
MatrixVersion.V3,
|
||||
)
|
||||
|
||||
|
||||
class MatrixWebhookMode:
|
||||
# Webhook Mode is disabled
|
||||
DISABLED = "off"
|
||||
@ -128,6 +146,9 @@ class NotifyMatrix(NotifyBase):
|
||||
# The default secure protocol
|
||||
secure_protocol = 'matrixs'
|
||||
|
||||
# Support Attachments
|
||||
attachment_support = True
|
||||
|
||||
# A URL that takes you to the setup/help of the specific protocol
|
||||
setup_url = 'https://github.com/caronc/apprise/wiki/Notify_matrix'
|
||||
|
||||
@ -147,6 +168,9 @@ class NotifyMatrix(NotifyBase):
|
||||
# Throttle a wee-bit to avoid thrashing
|
||||
request_rate_per_sec = 0.5
|
||||
|
||||
# Our Matrix API Version
|
||||
matrix_api_version = '3'
|
||||
|
||||
# How many retry attempts we'll make in the event the server asks us to
|
||||
# throttle back.
|
||||
default_retries = 2
|
||||
@ -234,6 +258,12 @@ class NotifyMatrix(NotifyBase):
|
||||
'values': MATRIX_WEBHOOK_MODES,
|
||||
'default': MatrixWebhookMode.DISABLED,
|
||||
},
|
||||
'version': {
|
||||
'name': _('Matrix API Verion'),
|
||||
'type': 'choice:string',
|
||||
'values': MATRIX_VERSIONS,
|
||||
'default': MatrixVersion.V3,
|
||||
},
|
||||
'msgtype': {
|
||||
'name': _('Message Type'),
|
||||
'type': 'choice:string',
|
||||
@ -248,7 +278,7 @@ class NotifyMatrix(NotifyBase):
|
||||
},
|
||||
})
|
||||
|
||||
def __init__(self, targets=None, mode=None, msgtype=None,
|
||||
def __init__(self, targets=None, mode=None, msgtype=None, version=None,
|
||||
include_image=False, **kwargs):
|
||||
"""
|
||||
Initialize Matrix Object
|
||||
@ -282,6 +312,14 @@ class NotifyMatrix(NotifyBase):
|
||||
self.logger.warning(msg)
|
||||
raise TypeError(msg)
|
||||
|
||||
# Setup our version
|
||||
self.version = self.template_args['version']['default'] \
|
||||
if not isinstance(version, str) else version
|
||||
if self.version not in MATRIX_VERSIONS:
|
||||
msg = 'The version specified ({}) is invalid.'.format(version)
|
||||
self.logger.warning(msg)
|
||||
raise TypeError(msg)
|
||||
|
||||
# Setup our message type
|
||||
self.msgtype = self.template_args['msgtype']['default'] \
|
||||
if not isinstance(msgtype, str) else msgtype.lower()
|
||||
@ -521,7 +559,8 @@ class NotifyMatrix(NotifyBase):
|
||||
return payload
|
||||
|
||||
def _send_server_notification(self, body, title='',
|
||||
notify_type=NotifyType.INFO, **kwargs):
|
||||
notify_type=NotifyType.INFO, attach=None,
|
||||
**kwargs):
|
||||
"""
|
||||
Perform Direct Matrix Server Notification (no webhook)
|
||||
"""
|
||||
@ -548,6 +587,13 @@ class NotifyMatrix(NotifyBase):
|
||||
# Initiaize our error tracking
|
||||
has_error = False
|
||||
|
||||
attachments = None
|
||||
if attach and self.attachment_support:
|
||||
attachments = self._send_attachments(attach)
|
||||
if not attachments:
|
||||
# take an early exit
|
||||
return False
|
||||
|
||||
while len(rooms) > 0:
|
||||
|
||||
# Get our room
|
||||
@ -568,6 +614,10 @@ class NotifyMatrix(NotifyBase):
|
||||
image_url = None if not self.include_image else \
|
||||
self.image_url(notify_type)
|
||||
|
||||
# Build our path
|
||||
path = '/rooms/{}/send/m.room.message'.format(
|
||||
NotifyMatrix.quote(room_id))
|
||||
|
||||
if image_url:
|
||||
# Define our payload
|
||||
image_payload = {
|
||||
@ -575,9 +625,6 @@ class NotifyMatrix(NotifyBase):
|
||||
'url': image_url,
|
||||
'body': '{}'.format(notify_type if not title else title),
|
||||
}
|
||||
# Build our path
|
||||
path = '/rooms/{}/send/m.room.message'.format(
|
||||
NotifyMatrix.quote(room_id))
|
||||
|
||||
# Post our content
|
||||
postokay, response = self._fetch(path, payload=image_payload)
|
||||
@ -586,6 +633,14 @@ class NotifyMatrix(NotifyBase):
|
||||
has_error = True
|
||||
continue
|
||||
|
||||
if attachments:
|
||||
for attachment in attachments:
|
||||
postokay, response = self._fetch(path, payload=attachment)
|
||||
if not postokay:
|
||||
# Mark our failure
|
||||
has_error = True
|
||||
continue
|
||||
|
||||
# Define our payload
|
||||
payload = {
|
||||
'msgtype': 'm.{}'.format(self.msgtype),
|
||||
@ -615,10 +670,6 @@ class NotifyMatrix(NotifyBase):
|
||||
)
|
||||
})
|
||||
|
||||
# Build our path
|
||||
path = '/rooms/{}/send/m.room.message'.format(
|
||||
NotifyMatrix.quote(room_id))
|
||||
|
||||
# Post our content
|
||||
postokay, response = self._fetch(path, payload=payload)
|
||||
if not postokay:
|
||||
@ -632,6 +683,44 @@ class NotifyMatrix(NotifyBase):
|
||||
|
||||
return not has_error
|
||||
|
||||
def _send_attachments(self, attach):
|
||||
"""
|
||||
Posts all of the provided attachments
|
||||
"""
|
||||
|
||||
payloads = []
|
||||
for attachment in attach:
|
||||
if not attachment:
|
||||
# invalid attachment (bad file)
|
||||
return False
|
||||
|
||||
if not re.match(r'^image/', attachment.mimetype, re.I):
|
||||
# unsuppored at this time
|
||||
continue
|
||||
|
||||
postokay, response = \
|
||||
self._fetch('/upload', attachment=attachment)
|
||||
if not (postokay and isinstance(response, dict)):
|
||||
# Failed to perform upload
|
||||
return False
|
||||
|
||||
# If we get here, we'll have a response that looks like:
|
||||
# {
|
||||
# "content_uri": "mxc://example.com/a-unique-key"
|
||||
# }
|
||||
|
||||
# Prepare our payload
|
||||
payloads.append({
|
||||
"info": {
|
||||
"mimetype": attachment.mimetype,
|
||||
},
|
||||
"msgtype": "m.image",
|
||||
"body": "tta.webp",
|
||||
"url": response.get('content_uri'),
|
||||
})
|
||||
|
||||
return payloads
|
||||
|
||||
def _register(self):
|
||||
"""
|
||||
Register with the service if possible.
|
||||
@ -970,7 +1059,8 @@ class NotifyMatrix(NotifyBase):
|
||||
|
||||
return None
|
||||
|
||||
def _fetch(self, path, payload=None, params=None, method='POST'):
|
||||
def _fetch(self, path, payload=None, params=None, attachment=None,
|
||||
method='POST'):
|
||||
"""
|
||||
Wrapper to request.post() to manage it's response better and make
|
||||
the send() function cleaner and easier to maintain.
|
||||
@ -983,6 +1073,7 @@ class NotifyMatrix(NotifyBase):
|
||||
headers = {
|
||||
'User-Agent': self.app_id,
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
}
|
||||
|
||||
if self.access_token is not None:
|
||||
@ -991,13 +1082,32 @@ class NotifyMatrix(NotifyBase):
|
||||
default_port = 443 if self.secure else 80
|
||||
|
||||
url = \
|
||||
'{schema}://{hostname}{port}{matrix_api}{path}'.format(
|
||||
'{schema}://{hostname}{port}'.format(
|
||||
schema='https' if self.secure else 'http',
|
||||
hostname=self.host,
|
||||
port='' if self.port is None
|
||||
or self.port == default_port else f':{self.port}',
|
||||
matrix_api=MATRIX_V2_API_PATH,
|
||||
path=path)
|
||||
or self.port == default_port else f':{self.port}')
|
||||
|
||||
if path == '/upload':
|
||||
if self.version == MatrixVersion.V3:
|
||||
url += MATRIX_V3_MEDIA_PATH + path
|
||||
|
||||
else:
|
||||
url += MATRIX_V2_MEDIA_PATH + path
|
||||
|
||||
params = {'filename': attachment.name}
|
||||
with open(attachment.path, 'rb') as fp:
|
||||
payload = fp.read()
|
||||
|
||||
# Update our content type
|
||||
headers['Content-Type'] = attachment.mimetype
|
||||
|
||||
else:
|
||||
if self.version == MatrixVersion.V3:
|
||||
url += MATRIX_V3_API_PATH + path
|
||||
|
||||
else:
|
||||
url += MATRIX_V2_API_PATH + path
|
||||
|
||||
# Our response object
|
||||
response = {}
|
||||
@ -1024,7 +1134,7 @@ class NotifyMatrix(NotifyBase):
|
||||
try:
|
||||
r = fn(
|
||||
url,
|
||||
data=dumps(payload),
|
||||
data=dumps(payload) if not attachment else payload,
|
||||
params=params,
|
||||
headers=headers,
|
||||
verify=self.verify_certificate,
|
||||
@ -1095,6 +1205,13 @@ class NotifyMatrix(NotifyBase):
|
||||
# Return; we're done
|
||||
return (False, response)
|
||||
|
||||
except (OSError, IOError) as e:
|
||||
self.logger.warning(
|
||||
'An I/O error occurred while reading {}.'.format(
|
||||
attachment.name if attachment else 'unknown file'))
|
||||
self.logger.debug('I/O Exception: %s' % str(e))
|
||||
return (False, {})
|
||||
|
||||
return (True, response)
|
||||
|
||||
# If we get here, we ran out of retries
|
||||
@ -1161,6 +1278,7 @@ class NotifyMatrix(NotifyBase):
|
||||
params = {
|
||||
'image': 'yes' if self.include_image else 'no',
|
||||
'mode': self.mode,
|
||||
'version': self.version,
|
||||
'msgtype': self.msgtype,
|
||||
}
|
||||
|
||||
@ -1258,6 +1376,14 @@ class NotifyMatrix(NotifyBase):
|
||||
if 'token' in results['qsd'] and len(results['qsd']['token']):
|
||||
results['password'] = NotifyMatrix.unquote(results['qsd']['token'])
|
||||
|
||||
# Support the use of the version= or v= keyword
|
||||
if 'version' in results['qsd'] and len(results['qsd']['version']):
|
||||
results['version'] = \
|
||||
NotifyMatrix.unquote(results['qsd']['version'])
|
||||
|
||||
elif 'v' in results['qsd'] and len(results['qsd']['v']):
|
||||
results['version'] = NotifyMatrix.unquote(results['qsd']['v'])
|
||||
|
||||
return results
|
||||
|
||||
@staticmethod
|
||||
@ -1267,7 +1393,7 @@ class NotifyMatrix(NotifyBase):
|
||||
"""
|
||||
|
||||
result = re.match(
|
||||
r'^https?://webhooks\.t2bot\.io/api/v1/matrix/hook/'
|
||||
r'^https?://webhooks\.t2bot\.io/api/v[0-9]+/matrix/hook/'
|
||||
r'(?P<webhook_token>[A-Z0-9_-]+)/?'
|
||||
r'(?P<params>\?.+)?$', url, re.I)
|
||||
|
||||
|
@ -32,9 +32,10 @@
|
||||
|
||||
from unittest import mock
|
||||
|
||||
import os
|
||||
import requests
|
||||
import pytest
|
||||
from apprise import AppriseAsset
|
||||
from apprise import Apprise, AppriseAsset, AppriseAttachment, NotifyType
|
||||
from json import dumps
|
||||
|
||||
from apprise.plugins.NotifyMatrix import NotifyMatrix
|
||||
@ -44,6 +45,17 @@ from helpers import AppriseURLTester
|
||||
import logging
|
||||
logging.disable(logging.CRITICAL)
|
||||
|
||||
MATRIX_GOOD_RESPONSE = dumps({
|
||||
'room_id': '!abc123:localhost',
|
||||
'room_alias': '#abc123:localhost',
|
||||
'joined_rooms': ['!abc123:localhost', '!def456:localhost'],
|
||||
'access_token': 'abcd1234',
|
||||
'home_server': 'localhost',
|
||||
})
|
||||
|
||||
# Attachment Directory
|
||||
TEST_VAR_DIR = os.path.join(os.path.dirname(__file__), 'var')
|
||||
|
||||
# Our Testing URLs
|
||||
apprise_url_tests = (
|
||||
##################################
|
||||
@ -97,6 +109,24 @@ apprise_url_tests = (
|
||||
# user and token correctly specified with webhook
|
||||
'instance': NotifyMatrix,
|
||||
}),
|
||||
('matrix://user:token@localhost:123/#general/?version=3', {
|
||||
# Provide version over-ride (using version=)
|
||||
'instance': NotifyMatrix,
|
||||
# Our response expected server response
|
||||
'requests_response_text': MATRIX_GOOD_RESPONSE,
|
||||
'privacy_url': 'matrix://user:****@localhost:123',
|
||||
}),
|
||||
('matrixs://user:token@localhost/#general?v=2', {
|
||||
# Provide version over-ride (using v=)
|
||||
'instance': NotifyMatrix,
|
||||
# Our response expected server response
|
||||
'requests_response_text': MATRIX_GOOD_RESPONSE,
|
||||
'privacy_url': 'matrixs://user:****@localhost',
|
||||
}),
|
||||
('matrix://user:token@localhost:123/#general/?v=invalid', {
|
||||
# Invalid version specified
|
||||
'instance': TypeError
|
||||
}),
|
||||
('matrix://user:token@localhost?mode=slack&format=text', {
|
||||
# user and token correctly specified with webhook
|
||||
'instance': NotifyMatrix,
|
||||
@ -842,3 +872,205 @@ def test_plugin_matrix_image_errors(mock_post, mock_get):
|
||||
assert obj.access_token is None
|
||||
|
||||
assert obj.notify('test', 'test') is True
|
||||
|
||||
|
||||
@mock.patch('requests.get')
|
||||
@mock.patch('requests.post')
|
||||
def test_plugin_matrix_attachments_api_v3(mock_post, mock_get):
|
||||
"""
|
||||
NotifyMatrix() Attachment Checks (v3)
|
||||
|
||||
"""
|
||||
|
||||
# Prepare a good response
|
||||
response = mock.Mock()
|
||||
response.status_code = requests.codes.ok
|
||||
response.content = MATRIX_GOOD_RESPONSE.encode('utf-8')
|
||||
|
||||
# Prepare a bad response
|
||||
bad_response = mock.Mock()
|
||||
bad_response.status_code = requests.codes.internal_server_error
|
||||
|
||||
# Prepare Mock return object
|
||||
mock_post.return_value = response
|
||||
mock_get.return_value = response
|
||||
|
||||
# Instantiate our object
|
||||
obj = Apprise.instantiate('matrix://user:pass@localhost/#general?v=3')
|
||||
|
||||
# attach our content
|
||||
attach = AppriseAttachment(os.path.join(TEST_VAR_DIR, 'apprise-test.gif'))
|
||||
|
||||
assert obj.notify(
|
||||
body='body', title='title', notify_type=NotifyType.INFO,
|
||||
attach=attach) is True
|
||||
|
||||
attach = AppriseAttachment(os.path.join(TEST_VAR_DIR, 'apprise-test.gif'))
|
||||
|
||||
# Test our call count
|
||||
assert mock_post.call_count == 5
|
||||
assert mock_post.call_args_list[0][0][0] == \
|
||||
'http://localhost/_matrix/client/v3/login'
|
||||
assert mock_post.call_args_list[1][0][0] == \
|
||||
'http://localhost/_matrix/media/v3/upload'
|
||||
assert mock_post.call_args_list[2][0][0] == \
|
||||
'http://localhost/_matrix/client/v3/join/%23general%3Alocalhost'
|
||||
assert mock_post.call_args_list[3][0][0] == \
|
||||
'http://localhost/_matrix/client/v3/rooms/%21abc123%3Alocalhost/' \
|
||||
'send/m.room.message'
|
||||
assert mock_post.call_args_list[4][0][0] == \
|
||||
'http://localhost/_matrix/client/v3/rooms/%21abc123%3Alocalhost/' \
|
||||
'send/m.room.message'
|
||||
|
||||
# Attach an unsupported file type
|
||||
attach = AppriseAttachment(
|
||||
os.path.join(TEST_VAR_DIR, 'apprise-archive.zip'))
|
||||
assert obj.notify(
|
||||
body='body', title='title', notify_type=NotifyType.INFO,
|
||||
attach=attach) is False
|
||||
|
||||
# An invalid attachment will cause a failure
|
||||
path = os.path.join(TEST_VAR_DIR, '/invalid/path/to/an/invalid/file.jpg')
|
||||
attach = AppriseAttachment(path)
|
||||
assert obj.notify(
|
||||
body='body', title='title', notify_type=NotifyType.INFO,
|
||||
attach=path) is False
|
||||
|
||||
# update our attachment to be valid
|
||||
attach = AppriseAttachment(os.path.join(TEST_VAR_DIR, 'apprise-test.gif'))
|
||||
|
||||
mock_post.return_value = None
|
||||
# Throw an exception on the first call to requests.post()
|
||||
for side_effect in (requests.RequestException(), OSError(), bad_response):
|
||||
mock_post.side_effect = [side_effect]
|
||||
|
||||
# We'll fail now because of our error handling
|
||||
assert obj.send(body="test", attach=attach) is False
|
||||
|
||||
# Throw an exception on the second call to requests.post()
|
||||
for side_effect in (requests.RequestException(), OSError(), bad_response):
|
||||
mock_post.side_effect = [response, side_effect]
|
||||
|
||||
# We'll fail now because of our error handling
|
||||
assert obj.send(body="test", attach=attach) is False
|
||||
|
||||
# handle a bad response
|
||||
bad_response = mock.Mock()
|
||||
bad_response.status_code = requests.codes.internal_server_error
|
||||
mock_post.side_effect = [response, bad_response]
|
||||
|
||||
# We'll fail now because of an internal exception
|
||||
assert obj.send(body="test", attach=attach) is False
|
||||
|
||||
|
||||
@mock.patch('requests.get')
|
||||
@mock.patch('requests.post')
|
||||
def test_plugin_matrix_attachments_api_v2(mock_post, mock_get):
|
||||
"""
|
||||
NotifyMatrix() Attachment Checks (v2)
|
||||
|
||||
"""
|
||||
|
||||
# Prepare a good response
|
||||
response = mock.Mock()
|
||||
response.status_code = requests.codes.ok
|
||||
response.content = MATRIX_GOOD_RESPONSE.encode('utf-8')
|
||||
|
||||
# Prepare a bad response
|
||||
bad_response = mock.Mock()
|
||||
bad_response.status_code = requests.codes.internal_server_error
|
||||
|
||||
# Prepare Mock return object
|
||||
mock_post.return_value = response
|
||||
mock_get.return_value = response
|
||||
|
||||
# Instantiate our object
|
||||
obj = Apprise.instantiate('matrix://user:pass@localhost/#general?v=3')
|
||||
|
||||
# attach our content
|
||||
attach = AppriseAttachment(os.path.join(TEST_VAR_DIR, 'apprise-test.gif'))
|
||||
|
||||
assert obj.notify(
|
||||
body='body', title='title', notify_type=NotifyType.INFO,
|
||||
attach=attach) is True
|
||||
|
||||
attach = AppriseAttachment(os.path.join(TEST_VAR_DIR, 'apprise-test.gif'))
|
||||
|
||||
# Attach an unsupported file
|
||||
mock_post.return_value = response
|
||||
mock_get.return_value = response
|
||||
mock_post.side_effect = None
|
||||
mock_get.side_effect = None
|
||||
|
||||
# Force a object removal (thus a logout call)
|
||||
del obj
|
||||
|
||||
# Reset our object
|
||||
mock_post.reset_mock()
|
||||
mock_get.reset_mock()
|
||||
|
||||
# Instantiate our object
|
||||
obj = Apprise.instantiate('matrixs://user:pass@localhost/#general?v=2')
|
||||
|
||||
assert obj.notify(
|
||||
body='body', title='title', notify_type=NotifyType.INFO,
|
||||
attach=attach) is True
|
||||
|
||||
# Test our call count
|
||||
assert mock_post.call_count == 5
|
||||
assert mock_post.call_args_list[0][0][0] == \
|
||||
'https://localhost/_matrix/client/r0/login'
|
||||
assert mock_post.call_args_list[1][0][0] == \
|
||||
'https://localhost/_matrix/media/r0/upload'
|
||||
assert mock_post.call_args_list[2][0][0] == \
|
||||
'https://localhost/_matrix/client/r0/join/%23general%3Alocalhost'
|
||||
assert mock_post.call_args_list[3][0][0] == \
|
||||
'https://localhost/_matrix/client/r0/rooms/%21abc123%3Alocalhost/' \
|
||||
'send/m.room.message'
|
||||
assert mock_post.call_args_list[4][0][0] == \
|
||||
'https://localhost/_matrix/client/r0/rooms/%21abc123%3Alocalhost/' \
|
||||
'send/m.room.message'
|
||||
|
||||
# Attach an unsupported file type
|
||||
attach = AppriseAttachment(
|
||||
os.path.join(TEST_VAR_DIR, 'apprise-archive.zip'))
|
||||
assert obj.notify(
|
||||
body='body', title='title', notify_type=NotifyType.INFO,
|
||||
attach=attach) is False
|
||||
|
||||
# An invalid attachment will cause a failure
|
||||
path = os.path.join(TEST_VAR_DIR, '/invalid/path/to/an/invalid/file.jpg')
|
||||
attach = AppriseAttachment(path)
|
||||
assert obj.notify(
|
||||
body='body', title='title', notify_type=NotifyType.INFO,
|
||||
attach=path) is False
|
||||
|
||||
# update our attachment to be valid
|
||||
attach = AppriseAttachment(os.path.join(TEST_VAR_DIR, 'apprise-test.gif'))
|
||||
|
||||
mock_post.return_value = None
|
||||
mock_get.return_value = None
|
||||
|
||||
# Throw an exception on the first call to requests.post()
|
||||
for side_effect in (requests.RequestException(), OSError(), bad_response):
|
||||
mock_post.side_effect = [side_effect]
|
||||
mock_get.side_effect = [side_effect]
|
||||
|
||||
assert obj.send(body="test", attach=attach) is False
|
||||
|
||||
# Throw an exception on the second call to requests.post()
|
||||
for side_effect in (requests.RequestException(), OSError(), bad_response):
|
||||
mock_post.side_effect = [response, side_effect]
|
||||
mock_get.side_effect = [side_effect]
|
||||
|
||||
# We'll fail now because of our error handling
|
||||
assert obj.send(body="test", attach=attach) is False
|
||||
|
||||
# handle a bad response
|
||||
bad_response = mock.Mock()
|
||||
bad_response.status_code = requests.codes.internal_server_error
|
||||
mock_post.side_effect = [response, bad_response]
|
||||
mock_get.side_effect = [response, bad_response]
|
||||
|
||||
# We'll fail now because of an internal exception
|
||||
assert obj.send(body="test", attach=attach) is False
|
||||
|
Loading…
Reference in New Issue
Block a user