Attachment consistency across supported notification services (#194)

* some test cleanup

* Attachment handling more consistent across plugins
This commit is contained in:
Chris Caron 2020-01-11 18:39:53 -05:00 committed by GitHub
parent 47b4d946ef
commit a235d9d5ab
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 210 additions and 44 deletions

View File

@ -51,6 +51,7 @@ from ..common import NotifyType
from ..utils import parse_bool from ..utils import parse_bool
from ..utils import validate_regex from ..utils import validate_regex
from ..AppriseLocale import gettext_lazy as _ from ..AppriseLocale import gettext_lazy as _
from ..attachment.AttachBase import AttachBase
class NotifyDiscord(NotifyBase): class NotifyDiscord(NotifyBase):
@ -312,6 +313,19 @@ class NotifyDiscord(NotifyBase):
# Always call throttle before any remote server i/o is made # Always call throttle before any remote server i/o is made
self.throttle() self.throttle()
# Perform some simple error checking
if isinstance(attach, AttachBase):
if not attach:
# We could not access the attachment
self.logger.error(
'Could not access attachment {}.'.format(
attach.url(privacy=True)))
return False
self.logger.debug(
'Posting Discord attachment {}'.format(
attach.url(privacy=True)))
# Our attachment path (if specified) # Our attachment path (if specified)
files = None files = None
try: try:

View File

@ -573,21 +573,22 @@ class NotifyEmail(NotifyBase):
# First attach our body to our content as the first element # First attach our body to our content as the first element
base.attach(content) base.attach(content)
attach_error = False
# Now store our attachments # Now store our attachments
for attachment in attach: for attachment in attach:
if not attachment: if not attachment:
# We could not load the attachment; take an early # We could not load the attachment; take an early
# exit since this isn't what the end user wanted # exit since this isn't what the end user wanted
self.logger.warning( # We could not access the attachment
'The specified attachment could not be referenced:' self.logger.error(
' {}.'.format(attachment.url(privacy=True))) 'Could not access attachment {}.'.format(
attachment.url(privacy=True)))
# Mark our failure return False
attach_error = True
break self.logger.debug(
'Preparing Email attachment {}'.format(
attachment.url(privacy=True)))
with open(attachment.path, "rb") as abody: with open(attachment.path, "rb") as abody:
app = MIMEApplication( app = MIMEApplication(
@ -600,11 +601,6 @@ class NotifyEmail(NotifyBase):
base.attach(app) base.attach(app)
if attach_error:
# Mark our error and quit early
has_error = True
break
# bind the socket variable to the current namespace # bind the socket variable to the current namespace
socket = None socket = None

View File

@ -147,6 +147,19 @@ class NotifyPushBullet(NotifyBase):
# We need to upload our payload first so that we can source it # We need to upload our payload first so that we can source it
# in remaining messages # in remaining messages
for attachment in attach: for attachment in attach:
# Perform some simple error checking
if not attachment:
# We could not access the attachment
self.logger.error(
'Could not access attachment {}.'.format(
attachment.url(privacy=True)))
return False
self.logger.debug(
'Preparing PushBullet attachment {}'.format(
attachment.url(privacy=True)))
# prepare payload # prepare payload
payload = { payload = {
'file_name': attachment.name, 'file_name': attachment.name,
@ -253,7 +266,7 @@ class NotifyPushBullet(NotifyBase):
continue continue
self.logger.info( self.logger.info(
'Sent PushBullet attachment (%s) to "%s".' % ( 'Sent PushBullet attachment ({}) to "{}".'.format(
attach_payload['file_name'], recipient)) attach_payload['file_name'], recipient))
return not has_error return not has_error

View File

@ -548,15 +548,22 @@ class NotifyPushSafer(NotifyBase):
# prepare payload # prepare payload
if not attachment: if not attachment:
# We could not access the attachment # We could not access the attachment
self.logger.warning( self.logger.error(
'Could not access {}.'.format( 'Could not access attachment {}.'.format(
attachment.url(privacy=True))) attachment.url(privacy=True)))
return False return False
if not attachment.mimetype.startswith('image/'): if not attachment.mimetype.startswith('image/'):
# Attachment not supported; continue peacefully # Attachment not supported; continue peacefully
self.logger.debug(
'Ignoring unsupported PushSafer attachment {}.'.format(
attachment.url(privacy=True)))
continue continue
self.logger.debug(
'Posting PushSafer attachment {}'.format(
attachment.url(privacy=True)))
try: try:
with io.open(attachment.path, 'rb') as f: with io.open(attachment.path, 'rb') as f:
# Output must be in a DataURL format (that's what # Output must be in a DataURL format (that's what

View File

@ -364,8 +364,8 @@ class NotifyPushover(NotifyBase):
# Perform some simple error checking # Perform some simple error checking
if not attach: if not attach:
# We could not access the attachment # We could not access the attachment
self.logger.warning( self.logger.error(
'Could not access {}.'.format( 'Could not access attachment {}.'.format(
attach.url(privacy=True))) attach.url(privacy=True)))
return False return False
@ -398,6 +398,10 @@ class NotifyPushover(NotifyBase):
return False return False
self.logger.debug(
'Posting Pushover attachment {}'.format(
attach.url(privacy=True)))
# Default Header # Default Header
headers = { headers = {
'User-Agent': self.app_id, 'User-Agent': self.app_id,

View File

@ -435,8 +435,18 @@ class NotifySlack(NotifyBase):
if attach and self.mode is SlackMode.BOT and attach_channel_list: if attach and self.mode is SlackMode.BOT and attach_channel_list:
# Send our attachments (can only be done in bot mode) # Send our attachments (can only be done in bot mode)
for attachment in attach: for attachment in attach:
self.logger.info(
'Posting Slack Attachment {}'.format(attachment.name)) # Perform some simple error checking
if not attachment:
# We could not access the attachment
self.logger.error(
'Could not access attachment {}.'.format(
attachment.url(privacy=True)))
return False
self.logger.debug(
'Posting Slack attachment {}'.format(
attachment.url(privacy=True)))
# Prepare API Upload Payload # Prepare API Upload Payload
_payload = { _payload = {

View File

@ -267,18 +267,22 @@ class NotifyTelegram(NotifyBase):
path = None path = None
if isinstance(attach, AttachBase): if isinstance(attach, AttachBase):
if not attach:
# We could not access the attachment
self.logger.error(
'Could not access attachment {}.'.format(
attach.url(privacy=True)))
return False
self.logger.debug(
'Posting Telegram attachment {}'.format(
attach.url(privacy=True)))
# Store our path to our file # Store our path to our file
path = attach.path path = attach.path
file_name = attach.name file_name = attach.name
mimetype = attach.mimetype mimetype = attach.mimetype
if not path:
# We could not access the attachment
self.logger.warning(
'Could not access {}.'.format(
attach.url(privacy=True)))
return False
# Process our attachment # Process our attachment
function_name, key = \ function_name, key = \
next(((x['function_name'], x['key']) for x in self.mime_lookup next(((x['function_name'], x['key']) for x in self.mime_lookup
@ -642,10 +646,10 @@ class NotifyTelegram(NotifyBase):
if attach: if attach:
# Send our attachments now (if specified and if it exists) # Send our attachments now (if specified and if it exists)
for attachment in attach: for attachment in attach:
sent_attachment = self.send_media( if not self.send_media(
payload['chat_id'], notify_type, attach=attachment) payload['chat_id'], notify_type,
attach=attachment):
if not sent_attachment:
# We failed; don't continue # We failed; don't continue
has_error = True has_error = True
break break

View File

@ -174,11 +174,8 @@ def test_discord_attachments(mock_post):
webhook_token = 'D' * 64 webhook_token = 'D' * 64
# Prepare Mock return object # Prepare Mock return object
response = mock.Mock() mock_post.return_value = requests.Request()
response.status_code = requests.codes.ok mock_post.return_value.status_code = requests.codes.ok
# Throw an exception on the second call to requests.post()
mock_post.side_effect = [response, OSError()]
# Test our markdown # Test our markdown
obj = Apprise.instantiate( obj = Apprise.instantiate(
@ -187,5 +184,26 @@ def test_discord_attachments(mock_post):
# attach our content # attach our content
attach = AppriseAttachment(os.path.join(TEST_VAR_DIR, 'apprise-test.gif')) 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
# 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
# Throw an exception on the second call to requests.post()
mock_post.return_value = None
response = mock.Mock()
response.status_code = requests.codes.ok
mock_post.side_effect = [response, OSError()]
# update our attachment to be valid
attach = AppriseAttachment(os.path.join(TEST_VAR_DIR, 'apprise-test.gif'))
# Test our markdown
# We'll fail now because of an internal exception # We'll fail now because of an internal exception
assert obj.send(body="test", attach=attach) is False assert obj.send(body="test", attach=attach) is False

View File

@ -59,6 +59,7 @@ def test_pushbullet_attachments(mock_post):
"upload_url": "https://upload.pushbullet.com/abcd123", "upload_url": "https://upload.pushbullet.com/abcd123",
}) })
response.status_code = requests.codes.ok response.status_code = requests.codes.ok
mock_post.return_value = response
# prepare our attachment # prepare our attachment
attach = AppriseAttachment(os.path.join(TEST_VAR_DIR, 'apprise-test.gif')) attach = AppriseAttachment(os.path.join(TEST_VAR_DIR, 'apprise-test.gif'))
@ -67,7 +68,23 @@ def test_pushbullet_attachments(mock_post):
obj = Apprise.instantiate( obj = Apprise.instantiate(
'pbul://{}/?format=markdown'.format(access_token)) 'pbul://{}/?format=markdown'.format(access_token))
# Send a good attachment
assert obj.notify(body="test", attach=attach) is True
# Add another attachment so we drop into the area of the PushBullet code
# that sends remaining attachments (if more detected)
attach.add(os.path.join(TEST_VAR_DIR, 'apprise-test.gif'))
# Send our attachments
assert obj.notify(body="test", attach=attach) is True
# 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="test", attach=attach) is False
# Throw an exception on the first call to requests.post() # Throw an exception on the first call to requests.post()
mock_post.return_value = None
mock_post.side_effect = requests.RequestException() mock_post.side_effect = requests.RequestException()
# We'll fail now because of an internal exception # We'll fail now because of an internal exception
@ -99,3 +116,47 @@ def test_pushbullet_attachments(mock_post):
# We'll fail because of an invalid json object # We'll fail because of an invalid json object
assert obj.send(body="test", attach=attach) is False assert obj.send(body="test", attach=attach) is False
#
# Test bad responses
#
# Prepare a bad response
response.content = dumps({
"file_name": "cat.jpg",
"file_type": "image/jpeg",
"file_url": "https://dl.pushb.com/abc/cat.jpg",
"upload_url": "https://upload.pushbullet.com/abcd123",
})
bad_response = mock.Mock()
bad_response.content = response.content
bad_response.status_code = 400
# Throw an exception on the third call to requests.post()
mock_post.return_value = bad_response
# prepare our attachment
attach = AppriseAttachment(os.path.join(TEST_VAR_DIR, 'apprise-test.gif'))
# We'll fail now because we were unable to send the attachment
assert obj.send(body="test", attach=attach) is False
# Throw an exception on the second call
mock_post.side_effect = [response, bad_response, response]
assert obj.send(body="test", attach=attach) is False
# Throw an OSError
mock_post.side_effect = [response, OSError()]
assert obj.send(body="test", attach=attach) is False
# Throw an exception on the third call
mock_post.side_effect = [response, response, bad_response]
assert obj.send(body="test", attach=attach) is False
# Throw an exception on the fourth call
mock_post.side_effect = [response, response, response, bad_response]
assert obj.send(body="test", attach=attach) is False
# A good message
mock_post.side_effect = [response, response, response, response]
assert obj.send(body="test", attach=attach) is True

View File

@ -40,7 +40,7 @@ TEST_VAR_DIR = os.path.join(os.path.dirname(__file__), 'var')
@mock.patch('requests.post') @mock.patch('requests.post')
def test_notify_pushsafer_plugin(mock_post, tmpdir): def test_notify_pushsafer_plugin(mock_post):
""" """
API: NotifyPushSafer() Tests API: NotifyPushSafer() Tests

View File

@ -4210,11 +4210,11 @@ def test_rest_plugins(mock_post, mock_get):
# Check that we were expecting this exception to happen # Check that we were expecting this exception to happen
try: try:
if not isinstance(e, response): if not isinstance(e, response):
raise raise e
except TypeError: except TypeError:
print('%s Unhandled response %s' % (url, type(e))) print('%s Unhandled response %s' % (url, type(e)))
raise raise e
# #
# Stage 2: without title defined # Stage 2: without title defined
@ -4250,7 +4250,7 @@ def test_rest_plugins(mock_post, mock_get):
except Exception as e: except Exception as e:
# Check that we were expecting this exception to happen # Check that we were expecting this exception to happen
if not isinstance(e, response): if not isinstance(e, response):
raise raise e
# Tidy our object and allow any possible defined deconstructors to # Tidy our object and allow any possible defined deconstructors to
# be executed. # be executed.
@ -4265,11 +4265,11 @@ def test_rest_plugins(mock_post, mock_get):
# Handle our exception # Handle our exception
if instance is None: if instance is None:
print('%s %s' % (url, str(e))) print('%s %s' % (url, str(e)))
raise raise e
if not isinstance(e, instance): if not isinstance(e, instance):
print('%s %s' % (url, str(e))) print('%s %s' % (url, str(e)))
raise raise e
@mock.patch('requests.get') @mock.patch('requests.get')

View File

@ -22,12 +22,14 @@
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE. # THE SOFTWARE.
import os
import six import six
import mock import mock
import pytest import pytest
import requests import requests
from apprise import plugins from apprise import plugins
from apprise import NotifyType from apprise import NotifyType
from apprise import AppriseAttachment
from json import dumps from json import dumps
@ -35,6 +37,9 @@ from json import dumps
import logging import logging
logging.disable(logging.CRITICAL) logging.disable(logging.CRITICAL)
# Attachment Directory
TEST_VAR_DIR = os.path.join(os.path.dirname(__file__), 'var')
@mock.patch('requests.post') @mock.patch('requests.post')
def test_slack_oauth_access_token(mock_post): def test_slack_oauth_access_token(mock_post):
@ -52,6 +57,11 @@ def test_slack_oauth_access_token(mock_post):
request.content = dumps({ request.content = dumps({
'ok': True, 'ok': True,
'message': '', 'message': '',
# Attachment support
'file': {
'url_private': 'http://localhost',
}
}) })
request.status_code = requests.codes.ok request.status_code = requests.codes.ok
@ -73,6 +83,34 @@ def test_slack_oauth_access_token(mock_post):
# apprise room was found # apprise room was found
assert obj.send(body="test") is True assert obj.send(body="test") is True
# Test Valid Attachment
path = os.path.join(TEST_VAR_DIR, 'apprise-test.gif')
attach = AppriseAttachment(path)
assert obj.notify(
body='body', title='title', notify_type=NotifyType.INFO,
attach=attach) is True
# Test invalid attachment
path = os.path.join(TEST_VAR_DIR, '/invalid/path/to/an/invalid/file.jpg')
assert obj.notify(
body='body', title='title', notify_type=NotifyType.INFO,
attach=path) is False
# Test case where expected return attachment payload is invalid
request.content = dumps({
'ok': True,
'message': '',
# Attachment support
'file': None
})
path = os.path.join(TEST_VAR_DIR, 'apprise-test.gif')
attach = AppriseAttachment(path)
# We'll fail because of the bad 'file' response
assert obj.notify(
body='body', title='title', notify_type=NotifyType.INFO,
attach=attach) is False
# Slack requests pay close attention to the response to determine # Slack requests pay close attention to the response to determine
# if things go well... this is not a good JSON response: # if things go well... this is not a good JSON response:
request.content = '{' request.content = '{'

View File

@ -43,7 +43,7 @@ TEST_VAR_DIR = os.path.join(os.path.dirname(__file__), 'var')
@mock.patch('requests.get') @mock.patch('requests.get')
@mock.patch('requests.post') @mock.patch('requests.post')
def test_notify_telegram_plugin(mock_post, mock_get, tmpdir): def test_notify_telegram_plugin(mock_post, mock_get):
""" """
API: NotifyTelegram() Tests API: NotifyTelegram() Tests
@ -205,13 +205,14 @@ def test_notify_telegram_plugin(mock_post, mock_get, tmpdir):
assert len(obj.targets) == 1 assert len(obj.targets) == 1
assert obj.targets[0] == '12345' assert obj.targets[0] == '12345'
path = os.path.join(TEST_VAR_DIR, 'apprise-test.gif') attach = AppriseAttachment(os.path.join(TEST_VAR_DIR, 'apprise-test.gif'))
attach = AppriseAttachment(path)
assert obj.notify( assert obj.notify(
body='body', title='title', notify_type=NotifyType.INFO, body='body', title='title', notify_type=NotifyType.INFO,
attach=attach) is True attach=attach) is True
# An invalid attachment will cause a failure
path = os.path.join(TEST_VAR_DIR, '/invalid/path/to/an/invalid/file.jpg') path = os.path.join(TEST_VAR_DIR, '/invalid/path/to/an/invalid/file.jpg')
attach = AppriseAttachment(path)
assert obj.notify( assert obj.notify(
body='body', title='title', notify_type=NotifyType.INFO, body='body', title='title', notify_type=NotifyType.INFO,
attach=path) is False attach=path) is False