more unit tests; 90% coverage

This commit is contained in:
Chris Caron 2017-12-22 22:47:46 -05:00
parent f8c3d35f8c
commit cc79763b3f
14 changed files with 967 additions and 274 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@ -41,12 +41,14 @@ class NotifyImageSize(object):
A list of pre-defined image sizes to make it easier to work with defined A list of pre-defined image sizes to make it easier to work with defined
plugins. plugins.
""" """
XY_32 = '32x32'
XY_72 = '72x72' XY_72 = '72x72'
XY_128 = '128x128' XY_128 = '128x128'
XY_256 = '256x256' XY_256 = '256x256'
NOTIFY_IMAGE_SIZES = ( NOTIFY_IMAGE_SIZES = (
NotifyImageSize.XY_32,
NotifyImageSize.XY_72, NotifyImageSize.XY_72,
NotifyImageSize.XY_128, NotifyImageSize.XY_128,
NotifyImageSize.XY_256, NotifyImageSize.XY_256,

View File

@ -183,13 +183,9 @@ class NotifyMyAndroid(NotifyBase):
if 'format' in results['qsd'] and len(results['qsd']['format']): if 'format' in results['qsd'] and len(results['qsd']['format']):
# Extract email format (Text/Html) # Extract email format (Text/Html)
try: format = NotifyBase.unquote(results['qsd']['format']).lower()
format = NotifyBase.unquote(results['qsd']['format']).lower() if len(format) > 0 and format[0] == 't':
if len(format) > 0 and format[0] == 't': results['notify_format'] = NotifyFormat.TEXT
results['notify_format'] = NotifyFormat.TEXT
except AttributeError:
pass
if 'priority' in results['qsd'] and len(results['qsd']['priority']): if 'priority' in results['qsd'] and len(results['qsd']['priority']):
_map = { _map = {

View File

@ -31,7 +31,7 @@ VALIDATE_PROVIDERKEY = re.compile(r'[A-Za-z0-9]{40}')
# Priorities # Priorities
class ProwlPriority(object): class ProwlPriority(object):
VERY_LOW = -2 LOW = -2
MODERATE = -1 MODERATE = -1
NORMAL = 0 NORMAL = 0
HIGH = 1 HIGH = 1
@ -39,7 +39,7 @@ class ProwlPriority(object):
PROWL_PRIORITIES = ( PROWL_PRIORITIES = (
ProwlPriority.VERY_LOW, ProwlPriority.LOW,
ProwlPriority.MODERATE, ProwlPriority.MODERATE,
ProwlPriority.NORMAL, ProwlPriority.NORMAL,
ProwlPriority.HIGH, ProwlPriority.HIGH,
@ -65,8 +65,7 @@ class NotifyProwl(NotifyBase):
# Prowl uses the http protocol with JSON requests # Prowl uses the http protocol with JSON requests
notify_url = 'https://api.prowlapp.com/publicapi/add' notify_url = 'https://api.prowlapp.com/publicapi/add'
def __init__(self, apikey, providerkey=None, priority=ProwlPriority.NORMAL, def __init__(self, apikey, providerkey=None, priority=None, **kwargs):
**kwargs):
""" """
Initialize Prowl Object Initialize Prowl Object
""" """
@ -146,7 +145,7 @@ class NotifyProwl(NotifyBase):
PROWL_HTTP_ERROR_MAP[r.status_code], PROWL_HTTP_ERROR_MAP[r.status_code],
r.status_code)) r.status_code))
except IndexError: except KeyError:
self.logger.warning( self.logger.warning(
'Failed to send Prowl notification ' 'Failed to send Prowl notification '
'(error=%s).' % ( '(error=%s).' % (
@ -159,7 +158,7 @@ class NotifyProwl(NotifyBase):
else: else:
self.logger.info('Sent Prowl notification.') self.logger.info('Sent Prowl notification.')
except requests.ConnectionError as e: except requests.RequestException as e:
self.logger.warning( self.logger.warning(
'A Connection error occured sending Prowl notification.') 'A Connection error occured sending Prowl notification.')
self.logger.debug('Socket Exception: %s' % str(e)) self.logger.debug('Socket Exception: %s' % str(e))
@ -186,15 +185,33 @@ class NotifyProwl(NotifyBase):
# optionally find the provider key # optionally find the provider key
try: try:
providerkey = filter( providerkey = [x for x in filter(
bool, NotifyBase.split_path(results['fullpath']))[0] bool, NotifyBase.split_path(results['fullpath']))][0]
if not providerkey:
providerkey = None
except (AttributeError, IndexError): except (AttributeError, IndexError):
providerkey = None providerkey = None
if 'priority' in results['qsd'] and len(results['qsd']['priority']):
_map = {
'l': ProwlPriority.LOW,
'-2': ProwlPriority.LOW,
'm': ProwlPriority.MODERATE,
'-1': ProwlPriority.MODERATE,
'n': ProwlPriority.NORMAL,
'0': ProwlPriority.NORMAL,
'h': ProwlPriority.HIGH,
'1': ProwlPriority.HIGH,
'e': ProwlPriority.EMERGENCY,
'2': ProwlPriority.EMERGENCY,
}
try:
results['priority'] = \
_map[results['qsd']['priority'][0].lower()]
except KeyError:
# No priority was set
pass
results['apikey'] = results['host'] results['apikey'] = results['host']
results['providerkey'] = providerkey results['providerkey'] = providerkey

View File

@ -60,11 +60,10 @@ class NotifyPushBullet(NotifyBase):
self.accesstoken = accesstoken self.accesstoken = accesstoken
if compat_is_basestring(recipients): if compat_is_basestring(recipients):
self.recipients = filter(bool, RECIPIENTS_LIST_DELIM.split( self.recipients = [x for x in filter(
recipients, bool, RECIPIENTS_LIST_DELIM.split(recipients))]
))
elif isinstance(recipients, (tuple, list)): elif isinstance(recipients, (set, tuple, list)):
self.recipients = recipients self.recipients = recipients
else: else:
@ -138,7 +137,7 @@ class NotifyPushBullet(NotifyBase):
PUSHBULLET_HTTP_ERROR_MAP[r.status_code], PUSHBULLET_HTTP_ERROR_MAP[r.status_code],
r.status_code)) r.status_code))
except IndexError: except KeyError:
self.logger.warning( self.logger.warning(
'Failed to send PushBullet notification ' 'Failed to send PushBullet notification '
'(error=%s).' % r.status_code) '(error=%s).' % r.status_code)
@ -148,7 +147,7 @@ class NotifyPushBullet(NotifyBase):
# Return; we're done # Return; we're done
has_error = True has_error = True
except requests.ConnectionError as e: except requests.RequestException as e:
self.logger.warning( self.logger.warning(
'A Connection error occured sending PushBullet ' 'A Connection error occured sending PushBullet '
'notification.' 'notification.'
@ -176,11 +175,7 @@ class NotifyPushBullet(NotifyBase):
return results return results
# Apply our settings now # Apply our settings now
try: recipients = NotifyBase.unquote(results['fullpath'])
recipients = NotifyBase.unquote(results['fullpath'])
except AttributeError:
recipients = ''
results['accesstoken'] = results['host'] results['accesstoken'] = results['host']
results['recipients'] = recipients results['recipients'] = recipients

View File

@ -43,10 +43,7 @@ class NotifyPushalot(NotifyBase):
A wrapper for Pushalot Notifications A wrapper for Pushalot Notifications
""" """
# The default protocol # The default protocol is always secured
protocol = 'palot'
# The default secure protocol
secure_protocol = 'palot' secure_protocol = 'palot'
# Pushalot uses the http protocol with JSON requests # Pushalot uses the http protocol with JSON requests
@ -117,7 +114,7 @@ class NotifyPushalot(NotifyBase):
PUSHALOT_HTTP_ERROR_MAP[r.status_code], PUSHALOT_HTTP_ERROR_MAP[r.status_code],
r.status_code)) r.status_code))
except IndexError: except KeyError:
self.logger.warning( self.logger.warning(
'Failed to send Pushalot notification ' 'Failed to send Pushalot notification '
'(error=%s).' % r.status_code) '(error=%s).' % r.status_code)
@ -128,7 +125,7 @@ class NotifyPushalot(NotifyBase):
else: else:
self.logger.info('Sent Pushalot notification.') self.logger.info('Sent Pushalot notification.')
except requests.ConnectionError as e: except requests.RequestException as e:
self.logger.warning( self.logger.warning(
'A Connection error occured sending Pushalot notification.') 'A Connection error occured sending Pushalot notification.')
self.logger.debug('Socket Exception: %s' % str(e)) self.logger.debug('Socket Exception: %s' % str(e))

View File

@ -27,18 +27,18 @@ from .NotifyBase import HTTP_ERROR_MAP
PUSHOVER_SEND_TO_ALL = 'ALL_DEVICES' PUSHOVER_SEND_TO_ALL = 'ALL_DEVICES'
# Used to validate API Key # Used to validate API Key
VALIDATE_TOKEN = re.compile(r'[A-Za-z0-9]{30}') VALIDATE_TOKEN = re.compile(r'^[a-z0-9]{30}$', re.I)
# Used to detect a User and/or Group # Used to detect a User and/or Group
VALIDATE_USERGROUP = re.compile(r'[A-Za-z0-9]{30}') VALIDATE_USERGROUP = re.compile(r'^[a-z0-9]{30}$', re.I)
# Used to detect a User and/or Group # Used to detect a User and/or Group
VALIDATE_DEVICE = re.compile(r'[A-Za-z0-9_]{1,25}') VALIDATE_DEVICE = re.compile(r'^[a-z0-9_]{1,25}$', re.I)
# Priorities # Priorities
class PushoverPriority(object): class PushoverPriority(object):
VERY_LOW = -2 LOW = -2
MODERATE = -1 MODERATE = -1
NORMAL = 0 NORMAL = 0
HIGH = 1 HIGH = 1
@ -46,7 +46,7 @@ class PushoverPriority(object):
PUSHOVER_PRIORITIES = ( PUSHOVER_PRIORITIES = (
PushoverPriority.VERY_LOW, PushoverPriority.LOW,
PushoverPriority.MODERATE, PushoverPriority.MODERATE,
PushoverPriority.NORMAL, PushoverPriority.NORMAL,
PushoverPriority.HIGH, PushoverPriority.HIGH,
@ -68,24 +68,29 @@ class NotifyPushover(NotifyBase):
A wrapper for Pushover Notifications A wrapper for Pushover Notifications
""" """
# The default protocol # All pushover requests are secure
protocol = 'pover'
# The default secure protocol
secure_protocol = 'pover' secure_protocol = 'pover'
# Pushover uses the http protocol with JSON requests # Pushover uses the http protocol with JSON requests
notify_url = 'https://api.pushover.net/1/messages.json' notify_url = 'https://api.pushover.net/1/messages.json'
def __init__(self, token, devices=None, def __init__(self, token, devices=None, priority=None, **kwargs):
priority=PushoverPriority.NORMAL, **kwargs):
""" """
Initialize Pushover Object Initialize Pushover Object
""" """
super(NotifyPushover, self).__init__( super(NotifyPushover, self).__init__(
title_maxlen=250, body_maxlen=512, **kwargs) title_maxlen=250, body_maxlen=512, **kwargs)
if not VALIDATE_TOKEN.match(token.strip()): try:
# The token associated with the account
self.token = token.strip()
except AttributeError:
# Token was None
self.logger.warning('No API Token was specified.')
raise TypeError('No API Token was specified.')
if not VALIDATE_TOKEN.match(self.token):
self.logger.warning( self.logger.warning(
'The API Token specified (%s) is invalid.' % token, 'The API Token specified (%s) is invalid.' % token,
) )
@ -93,13 +98,10 @@ class NotifyPushover(NotifyBase):
'The API Token specified (%s) is invalid.' % token, 'The API Token specified (%s) is invalid.' % token,
) )
# The token associated with the account
self.token = token.strip()
if compat_is_basestring(devices): if compat_is_basestring(devices):
self.devices = filter(bool, DEVICE_LIST_DELIM.split( self.devices = [x for x in filter(bool, DEVICE_LIST_DELIM.split(
devices, devices,
)) ))]
elif isinstance(devices, (set, tuple, list)): elif isinstance(devices, (set, tuple, list)):
self.devices = devices self.devices = devices
@ -121,10 +123,6 @@ class NotifyPushover(NotifyBase):
self.logger.warning('No user was specified.') self.logger.warning('No user was specified.')
raise TypeError('No user was specified.') raise TypeError('No user was specified.')
if not self.token:
self.logger.warning('No token was specified.')
raise TypeError('No token was specified.')
if not VALIDATE_USERGROUP.match(self.user): if not VALIDATE_USERGROUP.match(self.user):
self.logger.warning( self.logger.warning(
'The user/group specified (%s) is invalid.' % self.user, 'The user/group specified (%s) is invalid.' % self.user,
@ -152,6 +150,13 @@ class NotifyPushover(NotifyBase):
while len(devices): while len(devices):
device = devices.pop(0) device = devices.pop(0)
if VALIDATE_DEVICE.match(device) is None:
self.logger.warning(
'The device specified (%s) is invalid.' % device,
)
has_error = True
continue
# prepare JSON Object # prepare JSON Object
payload = { payload = {
'token': self.token, 'token': self.token,
@ -159,18 +164,9 @@ class NotifyPushover(NotifyBase):
'priority': str(self.priority), 'priority': str(self.priority),
'title': title, 'title': title,
'message': body, 'message': body,
'device': device,
} }
if device != PUSHOVER_SEND_TO_ALL:
if not VALIDATE_DEVICE.match(device):
self.logger.warning(
'The device specified (%s) is invalid.' % device,
)
has_error = True
continue
payload['device'] = device
self.logger.debug('Pushover POST URL: %s (cert_verify=%r)' % ( self.logger.debug('Pushover POST URL: %s (cert_verify=%r)' % (
self.notify_url, self.verify_certificate, self.notify_url, self.verify_certificate,
)) ))
@ -193,7 +189,7 @@ class NotifyPushover(NotifyBase):
PUSHOVER_HTTP_ERROR_MAP[r.status_code], PUSHOVER_HTTP_ERROR_MAP[r.status_code],
r.status_code)) r.status_code))
except IndexError: except KeyError:
self.logger.warning( self.logger.warning(
'Failed to send Pushover:%s ' 'Failed to send Pushover:%s '
'notification (error=%s).' % ( 'notification (error=%s).' % (
@ -205,7 +201,7 @@ class NotifyPushover(NotifyBase):
# Return; we're done # Return; we're done
has_error = True has_error = True
except requests.ConnectionError as e: except requests.RequestException as e:
self.logger.warning( self.logger.warning(
'A Connection error occured sending Pushover:%s ' % ( 'A Connection error occured sending Pushover:%s ' % (
device) + 'notification.' device) + 'notification.'
@ -217,7 +213,7 @@ class NotifyPushover(NotifyBase):
# Prevent thrashing requests # Prevent thrashing requests
self.throttle() self.throttle()
return has_error return not has_error
@staticmethod @staticmethod
def parse_url(url): def parse_url(url):
@ -233,11 +229,28 @@ class NotifyPushover(NotifyBase):
return results return results
# Apply our settings now # Apply our settings now
try: devices = NotifyBase.unquote(results['fullpath'])
devices = NotifyBase.unquote(results['fullpath'])
except AttributeError: if 'priority' in results['qsd'] and len(results['qsd']['priority']):
devices = '' _map = {
'l': PushoverPriority.LOW,
'-2': PushoverPriority.LOW,
'm': PushoverPriority.MODERATE,
'-1': PushoverPriority.MODERATE,
'n': PushoverPriority.NORMAL,
'0': PushoverPriority.NORMAL,
'h': PushoverPriority.HIGH,
'1': PushoverPriority.HIGH,
'e': PushoverPriority.EMERGENCY,
'2': PushoverPriority.EMERGENCY,
}
try:
results['priority'] = \
_map[results['qsd']['priority'][0].lower()]
except KeyError:
# No priority was set
pass
results['token'] = results['host'] results['token'] = results['host']
results['devices'] = devices results['devices'] = devices

View File

@ -173,7 +173,7 @@ class NotifyRocketChat(NotifyBase):
'%s (error=%s).' % ( '%s (error=%s).' % (
RC_HTTP_ERROR_MAP[r.status_code], RC_HTTP_ERROR_MAP[r.status_code],
r.status_code)) r.status_code))
except IndexError: except KeyError:
self.logger.warning( self.logger.warning(
'Failed to send Rocket.Chat notification ' + 'Failed to send Rocket.Chat notification ' +
'(error=%s).' % ( '(error=%s).' % (
@ -186,7 +186,7 @@ class NotifyRocketChat(NotifyBase):
self.logger.debug('Rocket.Chat Server Response: %s.' % r.text) self.logger.debug('Rocket.Chat Server Response: %s.' % r.text)
self.logger.info('Sent Rocket.Chat notification.') self.logger.info('Sent Rocket.Chat notification.')
except requests.ConnectionError as e: except requests.RequestException as e:
self.logger.warning( self.logger.warning(
'A Connection error occured sending Rocket.Chat ' + 'A Connection error occured sending Rocket.Chat ' +
'notification.') 'notification.')
@ -277,6 +277,7 @@ class NotifyRocketChat(NotifyBase):
'%s (error=%s).' % ( '%s (error=%s).' % (
RC_HTTP_ERROR_MAP[r.status_code], RC_HTTP_ERROR_MAP[r.status_code],
r.status_code)) r.status_code))
except IndexError: except IndexError:
self.logger.warning( self.logger.warning(
'Failed to log off Rocket.Chat server ' + 'Failed to log off Rocket.Chat server ' +
@ -316,10 +317,6 @@ class NotifyRocketChat(NotifyBase):
return results return results
# Apply our settings now # Apply our settings now
try: results['recipients'] = NotifyBase.unquote(results['fullpath'])
results['recipients'] = NotifyBase.unquote(results['fullpath'])
except AttributeError:
return None
return results return results

View File

@ -49,13 +49,13 @@ from json import dumps
from .NotifyBase import NotifyBase from .NotifyBase import NotifyBase
from .NotifyBase import NotifyFormat from .NotifyBase import NotifyFormat
from .NotifyBase import HTTP_ERROR_MAP from .NotifyBase import HTTP_ERROR_MAP
from ..common import NotifyImageSize
from ..utils import compat_is_basestring from ..utils import compat_is_basestring
# Token required as part of the API request # Token required as part of the API request
# allow the word 'bot' infront # allow the word 'bot' infront
VALIDATE_BOT_TOKEN = re.compile( VALIDATE_BOT_TOKEN = re.compile(
r'(bot)?(?P<key>[0-9]+:[A-Za-z0-9_-]+)/*$', r'^(bot)?(?P<key>[0-9]+:[a-z0-9_-]+)/*$',
re.IGNORECASE, re.IGNORECASE,
) )
@ -63,7 +63,7 @@ VALIDATE_BOT_TOKEN = re.compile(
# If the Chat ID is positive, then it's addressed to a single person # If the Chat ID is positive, then it's addressed to a single person
# If the Chat ID is negative, then it's targeting a group # If the Chat ID is negative, then it's targeting a group
IS_CHAT_ID_RE = re.compile( IS_CHAT_ID_RE = re.compile(
r'(@*(?P<idno>-?[0-9]{1,32})|(?P<name>[a-z_-][a-z0-9_-]*))', r'^(@*(?P<idno>-?[0-9]{1,32})|(?P<name>[a-z_-][a-z0-9_-]+))$',
re.IGNORECASE, re.IGNORECASE,
) )
@ -71,7 +71,7 @@ IS_CHAT_ID_RE = re.compile(
# The stickers/images are kind of big and consume a lot of space # The stickers/images are kind of big and consume a lot of space
# It's not as appealing as just having the post not contain # It's not as appealing as just having the post not contain
# an image at all. # an image at all.
TELEGRAM_IMAGE_XY = None TELEGRAM_IMAGE_XY = NotifyImageSize.XY_32
# Used to break path apart into list of chat identifiers # Used to break path apart into list of chat identifiers
CHAT_ID_LIST_DELIM = re.compile(r'[ \t\r\n,#\\/]+') CHAT_ID_LIST_DELIM = re.compile(r'[ \t\r\n,#\\/]+')
@ -88,32 +88,37 @@ class NotifyTelegram(NotifyBase):
# Telegram uses the http protocol with JSON requests # Telegram uses the http protocol with JSON requests
notify_url = 'https://api.telegram.org/bot' notify_url = 'https://api.telegram.org/bot'
def __init__(self, bot_token, chat_ids, **kwargs): def __init__(self, bot_token, chat_ids, notify_format=NotifyFormat.HTML,
**kwargs):
""" """
Initialize Telegram Object Initialize Telegram Object
""" """
super(NotifyTelegram, self).__init__( super(NotifyTelegram, self).__init__(
title_maxlen=250, body_maxlen=4096, title_maxlen=250, body_maxlen=4096,
image_size=TELEGRAM_IMAGE_XY, **kwargs) image_size=TELEGRAM_IMAGE_XY, notify_format=notify_format,
**kwargs)
if bot_token is None: try:
raise TypeError( self.bot_token = bot_token.strip()
'The Bot Token specified is invalid.'
)
result = VALIDATE_BOT_TOKEN.match(bot_token.strip()) except AttributeError:
# Token was None
self.logger.warning('No Bot Token was specified.')
raise TypeError('No Bot Token was specified.')
result = VALIDATE_BOT_TOKEN.match(self.bot_token)
if not result: if not result:
raise TypeError( raise TypeError(
'The Bot Token specified (%s) is invalid.' % bot_token, 'The Bot Token specified (%s) is invalid.' % bot_token,
) )
# Store our API Key # Store our Bot Token
self.bot_token = result.group('key') self.bot_token = result.group('key')
if compat_is_basestring(chat_ids): if compat_is_basestring(chat_ids):
self.chat_ids = filter(bool, CHAT_ID_LIST_DELIM.split( self.chat_ids = [x for x in filter(bool, CHAT_ID_LIST_DELIM.split(
chat_ids, chat_ids,
)) ))]
elif isinstance(chat_ids, (set, tuple, list)): elif isinstance(chat_ids, (set, tuple, list)):
self.chat_ids = list(chat_ids) self.chat_ids = list(chat_ids)
@ -125,51 +130,54 @@ class NotifyTelegram(NotifyBase):
# Treat this as a channel too # Treat this as a channel too
self.chat_ids.append(self.user) self.chat_ids.append(self.user)
# Bot's can't send messages to themselves which is fair enough
# but if or when they can, this code will allow a default fallback
# solution if no chat_id and/or channel is specified
# if len(self.chat_ids) == 0:
#
# chat_id = self._get_chat_id()
# if chat_id is not None:
# self.logger.warning(
# 'No chat_id or @channel was specified; ' +\
# 'using detected bot_chat_id (%d).' % chat_id,
# )
# self.chat_ids.append(str(chat_id))
if len(self.chat_ids) == 0: if len(self.chat_ids) == 0:
self.logger.warning('No chat_id(s) were specified.') self.logger.warning('No chat_id(s) were specified.')
raise TypeError('No chat_id(s) were specified.') raise TypeError('No chat_id(s) were specified.')
def _get_chat_id(self): def notify_image(self, chat_id, notify_type, **kwargs):
""" """
This function retrieves the chat id belonging to the key specified Sends the notification image based on the specified chat id
"""
headers = {
'User-Agent': self.app_id,
'Content-Type': 'application/json',
}
"""
image_content = self.image_raw(notify_type)
if image_content is None:
# Nothing to do
return True
# prepare our image URL
url = '%s%s/%s' % ( url = '%s%s/%s' % (
self.notify_url, self.notify_url,
self.bot_token, self.bot_token,
'getMe' 'sendPhoto'
) )
self.logger.debug('Telegram (Detection) GET URL: %s' % url) # Set up our upload
files = {'photo': ('%s.png' % notify_type, image_content)}
payload = {
'chat_id': chat_id,
'disable_notification': True,
}
self.logger.debug(
'Telegram (image) POST URL: %s (cert_verify=%r)' % (
url, self.verify_certificate))
self.logger.debug(
'Telegram (image) Payload: %s' % str(payload))
chat_id = None
try: try:
r = requests.post(url, headers=headers) r = requests.post(
if r.status_code == requests.codes.ok: url,
# Extract our chat ID data=payload,
result = loads(r.text) headers={
if result.get('ok', False) is True: 'User-Agent': self.app_id,
chat_id = result['result'].get('id') },
if chat_id <= 0: files=files,
chat_id = None verify=self.verify_certificate,
else: )
if r.status_code != requests.codes.ok:
# We had a problem # We had a problem
try: try:
# Try to get the error message if we can: # Try to get the error message if we can:
@ -181,28 +189,38 @@ class NotifyTelegram(NotifyBase):
try: try:
if error_msg: if error_msg:
self.logger.warning( self.logger.warning(
'Failed to lookup Telegram chat_id from ' 'Failed to send Telegram Image:%s '
'apikey: (%s) %s.' % (r.status_code, error_msg)) 'notification: (%s) %s.' % (
payload['chat_id'],
r.status_code, error_msg))
else: else:
self.logger.warning( self.logger.warning(
'Failed to lookup Telegram chat_id from ' 'Failed to send Telegram Image:%s '
'apikey: %s (error=%s).' % ( 'notification: %s (error=%s).' % (
payload['chat_id'],
HTTP_ERROR_MAP[r.status_code], HTTP_ERROR_MAP[r.status_code],
r.status_code)) r.status_code))
except IndexError: except KeyError:
self.logger.warning( self.logger.warning(
'Failed to lookup Telegram chat_id from ' 'Failed to send Telegram Image:%s '
'apikey: (error=%s).' % r.status_code) 'notification (error=%s).' % (
payload['chat_id'],
r.status_code))
except requests.ConnectionError as e: return False
except requests.RequestException as e:
self.logger.warning( self.logger.warning(
'A Connection error occured looking up Telegram chat_id ' 'A Connection error occured sending Telegram:%s ' % (
'from apikey.') payload['chat_id']) + 'notification.'
)
self.logger.debug('Socket Exception: %s' % str(e)) self.logger.debug('Socket Exception: %s' % str(e))
return False
return chat_id # We were successful
return True
def notify(self, title, body, notify_type, **kwargs): def notify(self, title, body, notify_type, **kwargs):
""" """
@ -217,19 +235,6 @@ class NotifyTelegram(NotifyBase):
# error tracking (used for function return) # error tracking (used for function return)
has_error = False has_error = False
image_url = None
image_content = self.image_raw(notify_type)
if image_content is not None:
# prepare our image URL
image_url = '%s%s/%s' % (
self.notify_url,
self.bot_token,
'sendPhoto'
)
# Set up our upload
files = {'photo': ('%s.png' % notify_type, image_content)}
url = '%s%s/%s' % ( url = '%s%s/%s' % (
self.notify_url, self.notify_url,
self.bot_token, self.bot_token,
@ -263,6 +268,7 @@ class NotifyTelegram(NotifyBase):
chat_id, chat_id,
) )
) )
has_error = True
continue continue
if chat_id.group('name') is not None: if chat_id.group('name') is not None:
@ -273,72 +279,19 @@ class NotifyTelegram(NotifyBase):
# ID # ID
payload['chat_id'] = chat_id.group('idno') payload['chat_id'] = chat_id.group('idno')
if image_url is not None: if not self.notify_image(
image_payload = { chat_id=payload['chat_id'], notify_type=notify_type):
'chat_id': payload['chat_id'], # Uh oh... The image failed to post if we get here
'disable_notification': True,
}
self.logger.debug( if len(chat_ids) > 0:
'Telegram (image) POST URL: %s (cert_verify=%r)' % ( # Prevent thrashing requests
image_url, self.verify_certificate)) self.throttle()
self.logger.debug( # Flag our error
'Telegram (image) Payload: %s' % str(image_payload)) has_error = True
try: # Move along
r = requests.post( continue
image_url,
data=image_payload,
headers={
'User-Agent': self.app_id,
},
files=files,
verify=self.verify_certificate,
)
if r.status_code != requests.codes.ok:
# We had a problem
try:
# Try to get the error message if we can:
error_msg = loads(r.text)['description']
except:
error_msg = None
try:
if error_msg:
self.logger.warning(
'Failed to send Telegram Image:%s '
'notification: (%s) %s.' % (
payload['chat_id'],
r.status_code, error_msg))
else:
self.logger.warning(
'Failed to send Telegram Image:%s '
'notification: %s (error=%s).' % (
payload['chat_id'],
HTTP_ERROR_MAP[r.status_code],
r.status_code))
except IndexError:
self.logger.warning(
'Failed to send Telegram Image:%s '
'notification (error=%s).' % (
payload['chat_id'],
r.status_code))
has_error = True
continue
except requests.ConnectionError as e:
self.logger.warning(
'A Connection error occured sending Telegram:%s ' % (
payload['chat_id']) + 'notification.'
)
self.logger.debug('Socket Exception: %s' % str(e))
has_error = True
continue
self.logger.debug('Telegram POST URL: %s' % url) self.logger.debug('Telegram POST URL: %s' % url)
self.logger.debug('Telegram POST URL: %s (cert_verify=%r)' % ( self.logger.debug('Telegram POST URL: %s (cert_verify=%r)' % (
@ -353,12 +306,14 @@ class NotifyTelegram(NotifyBase):
headers=headers, headers=headers,
verify=self.verify_certificate, verify=self.verify_certificate,
) )
if r.status_code != requests.codes.ok: if r.status_code != requests.codes.ok:
# We had a problem # We had a problem
try: try:
# Try to get the error message if we can: # Try to get the error message if we can:
error_msg = loads(r.text)['description'] error_msg = loads(r.text)['description']
except: except:
error_msg = None error_msg = None
@ -378,7 +333,7 @@ class NotifyTelegram(NotifyBase):
HTTP_ERROR_MAP[r.status_code], HTTP_ERROR_MAP[r.status_code],
r.status_code)) r.status_code))
except IndexError: except KeyError:
self.logger.warning( self.logger.warning(
'Failed to send Telegram:%s ' 'Failed to send Telegram:%s '
'notification (error=%s).' % ( 'notification (error=%s).' % (
@ -386,10 +341,10 @@ class NotifyTelegram(NotifyBase):
# self.logger.debug('Response Details: %s' % r.raw.read()) # self.logger.debug('Response Details: %s' % r.raw.read())
# Return; we're done # Flag our error
has_error = True has_error = True
except requests.ConnectionError as e: except requests.RequestException as e:
self.logger.warning( self.logger.warning(
'A Connection error occured sending Telegram:%s ' % ( 'A Connection error occured sending Telegram:%s ' % (
payload['chat_id']) + 'notification.' payload['chat_id']) + 'notification.'
@ -397,11 +352,12 @@ class NotifyTelegram(NotifyBase):
self.logger.debug('Socket Exception: %s' % str(e)) self.logger.debug('Socket Exception: %s' % str(e))
has_error = True has_error = True
if len(chat_ids): finally:
# Prevent thrashing requests if len(chat_ids):
self.throttle() # Prevent thrashing requests
self.throttle()
return has_error return not has_error
@staticmethod @staticmethod
def parse_url(url): def parse_url(url):
@ -410,14 +366,6 @@ class NotifyTelegram(NotifyBase):
us to substantiate this object. us to substantiate this object.
""" """
# super() is formatted slightly different when dealing with
# static method inheritance
results = NotifyBase.parse_url(url)
if results:
# We're done early
return results
# This is a dirty hack; but it's the only work around to # This is a dirty hack; but it's the only work around to
# tgram:// messages since the bot_token has a colon in it. # tgram:// messages since the bot_token has a colon in it.
# It invalidates an normal URL. # It invalidates an normal URL.
@ -427,11 +375,16 @@ class NotifyTelegram(NotifyBase):
# alternative is to ask users to actually change the colon # alternative is to ask users to actually change the colon
# into a slash (which will work too), but it's more likely # into a slash (which will work too), but it's more likely
# to cause confusion... So this is the next best thing # to cause confusion... So this is the next best thing
tgram = re.match( try:
r'(?P<protocol>%s://)(bot)?(?P<prefix>([a-z0-9_-]+)' tgram = re.match(
r'(:[a-z0-9_-]+)?@)?(?P<btoken_a>[0-9]+):+' r'(?P<protocol>%s://)(bot)?(?P<prefix>([a-z0-9_-]+)'
r'(?P<remaining>.*)$' % 'tgram', r'(:[a-z0-9_-]+)?@)?(?P<btoken_a>[0-9]+):+'
url, re.I) r'(?P<remaining>.*)$' % NotifyTelegram.secure_protocol,
url, re.I)
except (TypeError, AttributeError):
# url is bad; force tgram to be None
tgram = None
if not tgram: if not tgram:
# Content is simply not parseable # Content is simply not parseable
@ -439,7 +392,7 @@ class NotifyTelegram(NotifyBase):
if tgram.group('prefix'): if tgram.group('prefix'):
# Try again # Try again
result = NotifyBase.parse_url( results = NotifyBase.parse_url(
'%s%s%s/%s' % ( '%s%s%s/%s' % (
tgram.group('protocol'), tgram.group('protocol'),
tgram.group('prefix'), tgram.group('prefix'),
@ -450,7 +403,7 @@ class NotifyTelegram(NotifyBase):
else: else:
# Try again # Try again
result = NotifyBase.parse_url( results = NotifyBase.parse_url(
'%s%s/%s' % ( '%s%s/%s' % (
tgram.group('protocol'), tgram.group('protocol'),
tgram.group('btoken_a'), tgram.group('btoken_a'),
@ -459,30 +412,22 @@ class NotifyTelegram(NotifyBase):
) )
# The first token is stored in the hostnamee # The first token is stored in the hostnamee
bot_token_a = result['host'] bot_token_a = results['host']
# Now fetch the remaining tokens # Now fetch the remaining tokens
try: bot_token_b = [x for x in filter(
bot_token_b = filter( bool, NotifyBase.split_path(results['fullpath']))][0]
bool, NotifyBase.split_path(result['fullpath']))[0]
bot_token = '%s:%s' % (bot_token_a, bot_token_b) bot_token = '%s:%s' % (bot_token_a, bot_token_b)
except (AttributeError, IndexError): chat_ids = ','.join(
# Force a bad value that will get caught in parsing later [x for x in filter(
bot_token = None bool, NotifyBase.split_path(results['fullpath']))][1:])
try: # Store our bot token
chat_ids = ','.join( results['bot_token'] = bot_token
filter(bool, NotifyBase.split_path(result['fullpath']))[1:])
except (AttributeError, IndexError): # Store our chat ids
# Force some bad values that will get caught results['chat_ids'] = chat_ids
# in parsing later
chat_ids = None
# Return our results return results
return result + {
'bot_token': bot_token,
'chat_ids': chat_ids,
}.items()

View File

@ -52,14 +52,17 @@ class NotifyToasty(NotifyBase):
**kwargs) **kwargs)
if compat_is_basestring(devices): if compat_is_basestring(devices):
self.devices = filter(bool, DEVICES_LIST_DELIM.split( self.devices = [x for x in filter(bool, DEVICES_LIST_DELIM.split(
devices, devices,
)) ))]
elif isinstance(devices, (tuple, list)): elif isinstance(devices, (set, tuple, list)):
self.devices = devices self.devices = devices
else: else:
self.devices = list()
if len(devices) == 0:
raise TypeError('You must specify at least 1 device.') raise TypeError('You must specify at least 1 device.')
if not self.user: if not self.user:
@ -118,7 +121,7 @@ class NotifyToasty(NotifyBase):
HTTP_ERROR_MAP[r.status_code], HTTP_ERROR_MAP[r.status_code],
r.status_code)) r.status_code))
except IndexError: except KeyError:
self.logger.warning( self.logger.warning(
'Failed to send Toasty:%s ' 'Failed to send Toasty:%s '
'notification (error=%s).' % ( 'notification (error=%s).' % (
@ -130,7 +133,7 @@ class NotifyToasty(NotifyBase):
# Return; we're done # Return; we're done
has_error = True has_error = True
except requests.ConnectionError as e: except requests.RequestException as e:
self.logger.warning( self.logger.warning(
'A Connection error occured sending Toasty:%s ' % ( 'A Connection error occured sending Toasty:%s ' % (
device) + 'notification.' device) + 'notification.'
@ -142,7 +145,7 @@ class NotifyToasty(NotifyBase):
# Prevent thrashing requests # Prevent thrashing requests
self.throttle() self.throttle()
return has_error return not has_error
@staticmethod @staticmethod
def parse_url(url): def parse_url(url):
@ -158,11 +161,7 @@ class NotifyToasty(NotifyBase):
return results return results
# Apply our settings now # Apply our settings now
try: devices = NotifyBase.unquote(results['fullpath'])
devices = NotifyBase.unquote(results['fullpath'])
except AttributeError:
devices = ''
# Store our devices # Store our devices
results['devices'] = '%s/%s' % (results['host'], devices) results['devices'] = '%s/%s' % (results['host'], devices)

View File

@ -20,10 +20,10 @@ from apprise import plugins
from apprise import NotifyType from apprise import NotifyType
from apprise import Apprise from apprise import Apprise
from apprise import AppriseAsset from apprise import AppriseAsset
from json import dumps
import requests import requests
import mock import mock
TEST_URLS = ( TEST_URLS = (
################################## ##################################
# NotifyBoxcar # NotifyBoxcar
@ -176,7 +176,6 @@ TEST_URLS = (
'response': False, 'response': False,
'requests_response_code': 999, 'requests_response_code': 999,
}), }),
# apikey = a
('join://%s' % ('a' * 32), { ('join://%s' % ('a' * 32), {
'instance': plugins.NotifyJoin, 'instance': plugins.NotifyJoin,
# Throws a series of connection and transfer exceptions when this flag # Throws a series of connection and transfer exceptions when this flag
@ -380,8 +379,6 @@ TEST_URLS = (
}), }),
# Invalid APIKey # Invalid APIKey
('nma://%s' % ('a' * 24), { ('nma://%s' % ('a' * 24), {
'instance': None,
# Missing a channel
'exception': TypeError, 'exception': TypeError,
}), }),
# APIKey # APIKey
@ -390,6 +387,42 @@ TEST_URLS = (
# don't include an image by default # don't include an image by default
'include_image': False, 'include_image': False,
}), }),
# APIKey + priority setting
('nma://%s?priority=high' % ('a' * 48), {
'instance': plugins.NotifyMyAndroid,
}),
# APIKey + invalid priority setting
('nma://%s?priority=invalid' % ('a' * 48), {
'instance': plugins.NotifyMyAndroid,
}),
# APIKey + priority setting (empty)
('nma://%s?priority=' % ('a' * 48), {
'instance': plugins.NotifyMyAndroid,
}),
# APIKey + Invalid DevAPI Key
('nma://%s/%s' % ('a' * 48, 'b' * 24), {
'exception': TypeError,
}),
# APIKey + DevAPI Key
('nma://%s/%s' % ('a' * 48, 'b' * 48), {
'instance': plugins.NotifyMyAndroid,
}),
# Testing valid format
('nma://%s?format=text' % ('a' * 48), {
'instance': plugins.NotifyMyAndroid,
}),
# Testing valid format
('nma://%s?format=html' % ('a' * 48), {
'instance': plugins.NotifyMyAndroid,
}),
# Testing invalid format (fall's back to html)
('nma://%s?format=invalid' % ('a' * 48), {
'instance': plugins.NotifyMyAndroid,
}),
# Testing empty format (falls back to html)
('nma://%s?format=' % ('a' * 48), {
'instance': plugins.NotifyMyAndroid,
}),
# APIKey + with image # APIKey + with image
('nma://%s' % ('a' * 48), { ('nma://%s' % ('a' * 48), {
'instance': plugins.NotifyMyAndroid, 'instance': plugins.NotifyMyAndroid,
@ -410,7 +443,6 @@ TEST_URLS = (
'response': False, 'response': False,
'requests_response_code': 999, 'requests_response_code': 999,
}), }),
# apikey = a
('nma://%s' % ('a' * 48), { ('nma://%s' % ('a' * 48), {
'instance': plugins.NotifyMyAndroid, 'instance': plugins.NotifyMyAndroid,
# Throws a series of connection and transfer exceptions when this flag # Throws a series of connection and transfer exceptions when this flag
@ -418,6 +450,262 @@ TEST_URLS = (
'test_requests_exceptions': True, 'test_requests_exceptions': True,
}), }),
##################################
# NotifyProwl
##################################
('prowl://', {
'instance': None,
}),
# APIkey; no device
('prowl://%s' % ('a' * 40), {
'instance': plugins.NotifyProwl,
}),
# Invalid APIKey
('prowl://%s' % ('a' * 24), {
'exception': TypeError,
}),
# APIKey
('prowl://%s' % ('a' * 40), {
'instance': plugins.NotifyProwl,
# don't include an image by default
'include_image': False,
}),
# APIKey + priority setting
('prowl://%s?priority=high' % ('a' * 40), {
'instance': plugins.NotifyProwl,
}),
# APIKey + invalid priority setting
('prowl://%s?priority=invalid' % ('a' * 40), {
'instance': plugins.NotifyProwl,
}),
# APIKey + priority setting (empty)
('prowl://%s?priority=' % ('a' * 40), {
'instance': plugins.NotifyProwl,
}),
# APIKey + Invalid Provider Key
('prowl://%s/%s' % ('a' * 40, 'b' * 24), {
'exception': TypeError,
}),
# APIKey + No Provider Key (empty)
('prowl://%s///' % ('a' * 40), {
'instance': plugins.NotifyProwl,
}),
# APIKey + Provider Key
('prowl://%s/%s' % ('a' * 40, 'b' * 40), {
'instance': plugins.NotifyProwl,
}),
# APIKey + with image
('prowl://%s' % ('a' * 40), {
'instance': plugins.NotifyProwl,
}),
# bad url
('prowl://:@/', {
'instance': None,
}),
('prowl://%s' % ('a' * 40), {
'instance': plugins.NotifyProwl,
# force a failure
'response': False,
'requests_response_code': requests.codes.internal_server_error,
}),
('prowl://%s' % ('a' * 40), {
'instance': plugins.NotifyProwl,
# throw a bizzare code forcing us to fail to look it up
'response': False,
'requests_response_code': 999,
}),
('prowl://%s' % ('a' * 40), {
'instance': plugins.NotifyProwl,
# Throws a series of connection and transfer exceptions when this flag
# is set and tests that we gracfully handle them
'test_requests_exceptions': True,
}),
##################################
# NotifyPushalot
##################################
('palot://', {
'instance': None,
}),
# AuthToken
('palot://%s' % ('a' * 32), {
'instance': plugins.NotifyPushalot,
}),
# AuthToken, no image
('palot://%s' % ('a' * 32), {
'instance': plugins.NotifyPushalot,
# don't include an image by default
'include_image': False,
}),
# Invalid AuthToken
('palot://%s' % ('a' * 24), {
'instance': None,
# Missing a channel
'exception': TypeError,
}),
# AuthToken + bad url
('palot://:@/', {
'instance': None,
}),
('palot://%s' % ('a' * 32), {
'instance': plugins.NotifyPushalot,
# force a failure
'response': False,
'requests_response_code': requests.codes.internal_server_error,
}),
('palot://%s' % ('a' * 32), {
'instance': plugins.NotifyPushalot,
# throw a bizzare code forcing us to fail to look it up
'response': False,
'requests_response_code': 999,
}),
('palot://%s' % ('a' * 32), {
'instance': plugins.NotifyPushalot,
# Throws a series of connection and transfer exceptions when this flag
# is set and tests that we gracfully handle them
'test_requests_exceptions': True,
}),
##################################
# NotifyPushBullet
##################################
('pbul://', {
'instance': None,
}),
# APIkey
('pbul://%s' % ('a' * 32), {
'instance': plugins.NotifyPushBullet,
}),
# APIKey + channel
('pbul://%s/#channel/' % ('a' * 32), {
'instance': plugins.NotifyPushBullet,
}),
# APIKey + 2 channels
('pbul://%s/#channel1/#channel2' % ('a' * 32), {
'instance': plugins.NotifyPushBullet,
}),
# APIKey + device
('pbul://%s/device/' % ('a' * 32), {
'instance': plugins.NotifyPushBullet,
}),
# APIKey + 2 devices
('pbul://%s/device1/device2/' % ('a' * 32), {
'instance': plugins.NotifyPushBullet,
}),
# APIKey + email
('pbul://%s/user@example.com/' % ('a' * 32), {
'instance': plugins.NotifyPushBullet,
}),
# APIKey + 2 emails
('pbul://%s/user@example.com/abc@def.com/' % ('a' * 32), {
'instance': plugins.NotifyPushBullet,
}),
# APIKey + Combo
('pbul://%s/device/#channel/user@example.com/' % ('a' * 32), {
'instance': plugins.NotifyPushBullet,
}),
# APIKey + bad url
('pbul://:@/', {
'instance': None,
}),
('pbul://%s' % ('a' * 32), {
'instance': plugins.NotifyPushBullet,
# force a failure
'response': False,
'requests_response_code': requests.codes.internal_server_error,
}),
('pbul://%s' % ('a' * 32), {
'instance': plugins.NotifyPushBullet,
# throw a bizzare code forcing us to fail to look it up
'response': False,
'requests_response_code': 999,
}),
('pbul://%s' % ('a' * 32), {
'instance': plugins.NotifyPushBullet,
# Throws a series of connection and transfer exceptions when this flag
# is set and tests that we gracfully handle them
'test_requests_exceptions': True,
}),
##################################
# NotifyPushover
##################################
('pover://', {
'instance': None,
}),
# APIkey; no user
('pover://%s' % ('a' * 30), {
'exception': TypeError,
}),
# APIkey; invalid user
('pover://%s@%s' % ('u' * 20, 'a' * 30), {
'exception': TypeError,
}),
# Invalid APIKey; valid User
('pover://%s@%s' % ('u' * 30, 'a' * 24), {
'exception': TypeError,
}),
# APIKey + Valid User
('pover://%s@%s' % ('u' * 30, 'a' * 30), {
'instance': plugins.NotifyPushover,
# don't include an image by default
'include_image': False,
}),
# APIKey + Valid User + 1 Device
('pover://%s@%s/DEVICE' % ('u' * 30, 'a' * 30), {
'instance': plugins.NotifyPushover,
}),
# APIKey + Valid User + 2 Devices
('pover://%s@%s/DEVICE1/DEVICE2/' % ('u' * 30, 'a' * 30), {
'instance': plugins.NotifyPushover,
}),
# APIKey + Valid User + invalid device
('pover://%s@%s/%s/' % ('u' * 30, 'a' * 30, 'd' * 30), {
'instance': plugins.NotifyPushover,
# Notify will return False since there is a bad device in our list
'response': False,
}),
# APIKey + Valid User + device + invalid device
('pover://%s@%s/DEVICE1/%s/' % ('u' * 30, 'a' * 30, 'd' * 30), {
'instance': plugins.NotifyPushover,
# Notify will return False since there is a bad device in our list
'response': False,
}),
# APIKey + priority setting
('pover://%s@%s?priority=high' % ('u' * 30, 'a' * 30), {
'instance': plugins.NotifyPushover,
}),
# APIKey + invalid priority setting
('pover://%s@%s?priority=invalid' % ('u' * 30, 'a' * 30), {
'instance': plugins.NotifyPushover,
}),
# APIKey + priority setting (empty)
('pover://%s@%s?priority=' % ('u' * 30, 'a' * 30), {
'instance': plugins.NotifyPushover,
}),
# bad url
('pover://:@/', {
'instance': None,
}),
('pover://%s@%s' % ('u' * 30, 'a' * 30), {
'instance': plugins.NotifyPushover,
# force a failure
'response': False,
'requests_response_code': requests.codes.internal_server_error,
}),
('pover://%s@%s' % ('u' * 30, 'a' * 30), {
'instance': plugins.NotifyPushover,
# throw a bizzare code forcing us to fail to look it up
'response': False,
'requests_response_code': 999,
}),
('pover://%s@%s' % ('u' * 30, 'a' * 30), {
'instance': plugins.NotifyPushover,
# Throws a series of connection and transfer exceptions when this flag
# is set and tests that we gracfully handle them
'test_requests_exceptions': True,
}),
################################## ##################################
# NotifySlack # NotifySlack
################################## ##################################
@ -486,6 +774,173 @@ TEST_URLS = (
'test_requests_exceptions': True, 'test_requests_exceptions': True,
}), }),
##################################
# NotifyTelegram
##################################
('tgram://', {
'instance': None,
}),
# Simple Message
('tgram://123456789:abcdefg_hijklmnop/lead2gold/', {
'instance': plugins.NotifyTelegram,
}),
# Simple Message (no images)
('tgram://123456789:abcdefg_hijklmnop/lead2gold/', {
'instance': plugins.NotifyTelegram,
# don't include an image by default
'include_image': False,
}),
# Simple Message with multiple chat names
('tgram://123456789:abcdefg_hijklmnop/id1/id2/', {
'instance': plugins.NotifyTelegram,
}),
# Simple Message with an invalid chat ID
('tgram://123456789:abcdefg_hijklmnop/%$/', {
'instance': plugins.NotifyTelegram,
# Notify will fail
'response': False,
}),
# Simple Message with multiple chat ids
('tgram://123456789:abcdefg_hijklmnop/id1/id2/23423/-30/', {
'instance': plugins.NotifyTelegram,
}),
# Simple Message with multiple chat ids (no images)
('tgram://123456789:abcdefg_hijklmnop/id1/id2/23423/-30/', {
'instance': plugins.NotifyTelegram,
# don't include an image by default
'include_image': False,
}),
# Support bot keyword prefix
('tgram://bottest@123456789:abcdefg_hijklmnop/lead2gold/', {
'instance': plugins.NotifyTelegram,
}),
# Testing valid format
('tgram://123456789:abcdefg_hijklmnop/lead2gold/?format=text', {
'instance': plugins.NotifyTelegram,
}),
# Testing valid format
('tgram://123456789:abcdefg_hijklmnop/lead2gold/?format=html', {
'instance': plugins.NotifyTelegram,
}),
# Testing invalid format (fall's back to text)
('tgram://123456789:abcdefg_hijklmnop/lead2gold/?format=invalid', {
'instance': plugins.NotifyTelegram,
}),
# Testing empty format (falls back to text)
('tgram://123456789:abcdefg_hijklmnop/lead2gold/?format=', {
'instance': plugins.NotifyTelegram,
}),
# Simple Message without image
('tgram://123456789:abcdefg_hijklmnop/lead2gold/', {
'instance': plugins.NotifyTelegram,
# don't include an image by default
'include_image': False,
}),
# Invalid Bot Token
('tgram://alpha:abcdefg_hijklmnop/lead2gold/', {
'instance': None,
}),
# AuthToken + bad url
('tgram://:@/', {
'instance': None,
}),
('tgram://123456789:abcdefg_hijklmnop/lead2gold/', {
'instance': plugins.NotifyTelegram,
# force a failure
'response': False,
'requests_response_code': requests.codes.internal_server_error,
}),
('tgram://123456789:abcdefg_hijklmnop/lead2gold/', {
'instance': plugins.NotifyTelegram,
# force a failure without an image specified
'include_image': False,
'response': False,
'requests_response_code': requests.codes.internal_server_error,
}),
('tgram://123456789:abcdefg_hijklmnop/id1/id2/', {
'instance': plugins.NotifyTelegram,
# force a failure with multiple chat_ids
'response': False,
'requests_response_code': requests.codes.internal_server_error,
}),
('tgram://123456789:abcdefg_hijklmnop/id1/id2/', {
'instance': plugins.NotifyTelegram,
# force a failure without an image specified
'include_image': False,
'response': False,
'requests_response_code': requests.codes.internal_server_error,
}),
('tgram://123456789:abcdefg_hijklmnop/lead2gold/', {
'instance': plugins.NotifyTelegram,
# throw a bizzare code forcing us to fail to look it up
'response': False,
'requests_response_code': 999,
}),
('tgram://123456789:abcdefg_hijklmnop/lead2gold/', {
'instance': plugins.NotifyTelegram,
# throw a bizzare code forcing us to fail to look it up without
# having an image included
'include_image': False,
'response': False,
'requests_response_code': 999,
}),
('tgram://123456789:abcdefg_hijklmnop/lead2gold/', {
'instance': plugins.NotifyTelegram,
# Throws a series of connection and transfer exceptions when this flag
# is set and tests that we gracfully handle them
'test_requests_exceptions': True,
}),
('tgram://123456789:abcdefg_hijklmnop/lead2gold/', {
'instance': plugins.NotifyTelegram,
# Throws a series of connection and transfer exceptions when this flag
# is set and tests that we gracfully handle them without images set
'include_image': False,
'test_requests_exceptions': True,
}),
##################################
# NotifyToasty (SuperToasty)
##################################
('toasty://', {
'instance': None,
}),
# No username specified but contains a device
('toasty://%s' % ('d' * 32), {
'exception': TypeError,
}),
# User + 1 device
('toasty://user@device', {
'instance': plugins.NotifyToasty,
}),
# User + 3 devices
('toasty://user@device0/device1/device2/', {
'instance': plugins.NotifyToasty,
# don't include an image by default
'include_image': False,
}),
# bad url
('toasty://:@/', {
'instance': None,
}),
('toasty://user@device', {
'instance': plugins.NotifyToasty,
# force a failure
'response': False,
'requests_response_code': requests.codes.internal_server_error,
}),
('toasty://user@device', {
'instance': plugins.NotifyToasty,
# throw a bizzare code forcing us to fail to look it up
'response': False,
'requests_response_code': 999,
}),
('toasty://user@device', {
'instance': plugins.NotifyToasty,
# Throws a series of connection and transfer exceptions when this flag
# is set and tests that we gracfully handle them
'test_requests_exceptions': True,
}),
################################## ##################################
# NotifyKODI # NotifyKODI
################################## ##################################
@ -696,6 +1151,9 @@ def test_rest_plugins(mock_post, mock_get):
assert(isinstance(obj, instance)) assert(isinstance(obj, instance))
# Disable throttling to speed up unit tests
obj.throttle_attempt = 0
if self: if self:
# Iterate over our expected entries inside of our object # Iterate over our expected entries inside of our object
for key, val in self.items(): for key, val in self.items():
@ -711,9 +1169,9 @@ def test_rest_plugins(mock_post, mock_get):
notify_type=notify_type) == response notify_type=notify_type) == response
else: else:
for exception in test_requests_exceptions: for _exception in test_requests_exceptions:
mock_post.side_effect = exception mock_post.side_effect = _exception
mock_get.side_effect = exception mock_get.side_effect = _exception
try: try:
assert obj.notify( assert obj.notify(
title='test', body='body', title='test', body='body',
@ -805,6 +1263,10 @@ def test_notify_boxcar_plugin(mock_post, mock_get):
# Test notifications without a body or a title # Test notifications without a body or a title
p = plugins.NotifyBoxcar(access=access, secret=secret, recipients=None) p = plugins.NotifyBoxcar(access=access, secret=secret, recipients=None)
# Disable throttling to speed up unit tests
p.throttle_attempt = 0
p.notify(body=None, title=None, notify_type=NotifyType.INFO) is True p.notify(body=None, title=None, notify_type=NotifyType.INFO) is True
@ -835,6 +1297,9 @@ def test_notify_join_plugin(mock_post, mock_get):
mock_post.return_value.status_code = requests.codes.created mock_post.return_value.status_code = requests.codes.created
mock_get.return_value.status_code = requests.codes.created mock_get.return_value.status_code = requests.codes.created
# Disable throttling to speed up unit tests
p.throttle_attempt = 0
# Test notifications without a body or a title; nothing to send # Test notifications without a body or a title; nothing to send
# so we return False # so we return False
p.notify(body=None, title=None, notify_type=NotifyType.INFO) is False p.notify(body=None, title=None, notify_type=NotifyType.INFO) is False
@ -859,6 +1324,8 @@ def test_notify_slack_plugin(mock_post, mock_get):
obj = plugins.NotifySlack( obj = plugins.NotifySlack(
token_a=token_a, token_b=token_b, token_c=token_c, channels=channels) token_a=token_a, token_b=token_b, token_c=token_c, channels=channels)
assert(len(obj.channels) == 4) assert(len(obj.channels) == 4)
# Prepare Mock
mock_get.return_value = requests.Request() mock_get.return_value = requests.Request()
mock_post.return_value = requests.Request() mock_post.return_value = requests.Request()
mock_post.return_value.status_code = requests.codes.ok mock_post.return_value.status_code = requests.codes.ok
@ -880,6 +1347,271 @@ def test_notify_slack_plugin(mock_post, mock_get):
token_a=token_a, token_b=token_b, token_c=token_c, channels=channels, token_a=token_a, token_b=token_b, token_c=token_c, channels=channels,
include_image=True) include_image=True)
# Disable throttling to speed up unit tests
obj.throttle_attempt = 0
# This call includes an image with it's payload: # This call includes an image with it's payload:
assert obj.notify(title='title', body='body', assert obj.notify(title='title', body='body',
notify_type=NotifyType.INFO) is True notify_type=NotifyType.INFO) is True
@mock.patch('requests.get')
@mock.patch('requests.post')
def test_notify_pushbullet_plugin(mock_post, mock_get):
"""
API: NotifyPushBullet() Extra Checks
"""
# Initialize some generic (but valid) tokens
accesstoken = 'a' * 32
# Support strings
recipients = '#chan1,#chan2,device,user@example.com,,,'
# Prepare Mock
mock_get.return_value = requests.Request()
mock_post.return_value = requests.Request()
mock_post.return_value.status_code = requests.codes.ok
mock_get.return_value.status_code = requests.codes.ok
obj = plugins.NotifyPushBullet(
accesstoken=accesstoken, recipients=recipients)
assert(isinstance(obj, plugins.NotifyPushBullet))
assert(len(obj.recipients) == 4)
obj = plugins.NotifyPushBullet(accesstoken=accesstoken)
assert(isinstance(obj, plugins.NotifyPushBullet))
# Default is to send to all devices, so there will be a
# recipient here
assert(len(obj.recipients) == 1)
obj = plugins.NotifyPushBullet(accesstoken=accesstoken, recipients=set())
assert(isinstance(obj, plugins.NotifyPushBullet))
# Default is to send to all devices, so there will be a
# recipient here
assert(len(obj.recipients) == 1)
# Support the handling of an empty and invalid URL strings
assert(plugins.NotifyPushBullet.parse_url(None) is None)
assert(plugins.NotifyPushBullet.parse_url('') is None)
assert(plugins.NotifyPushBullet.parse_url(42) is None)
@mock.patch('requests.get')
@mock.patch('requests.post')
def test_notify_pushover_plugin(mock_post, mock_get):
"""
API: NotifyPushover() Extra Checks
"""
# Initialize some generic (but valid) tokens
token = 'a' * 30
user = 'u' * 30
invalid_device = 'd' * 35
# Support strings
devices = 'device1,device2,,,,%s' % invalid_device
# Prepare Mock
mock_get.return_value = requests.Request()
mock_post.return_value = requests.Request()
mock_post.return_value.status_code = requests.codes.ok
mock_get.return_value.status_code = requests.codes.ok
try:
obj = plugins.NotifyPushover(user=user, token=None)
# No token specified
assert(False)
except TypeError:
# Exception should be thrown about the fact no token was specified
assert(True)
obj = plugins.NotifyPushover(user=user, token=token, devices=devices)
assert(isinstance(obj, plugins.NotifyPushover))
assert(len(obj.devices) == 3)
# Disable throttling to speed up unit tests
obj.throttle_attempt = 0
# This call fails because there is 1 invalid device
assert obj.notify(title='title', body='body',
notify_type=NotifyType.INFO) is False
obj = plugins.NotifyPushover(user=user, token=token)
assert(isinstance(obj, plugins.NotifyPushover))
# Default is to send to all devices, so there will be a
# device defined here
assert(len(obj.devices) == 1)
# Disable throttling to speed up unit tests
obj.throttle_attempt = 0
# This call succeeds because all of the devices are valid
assert obj.notify(title='title', body='body',
notify_type=NotifyType.INFO) is True
obj = plugins.NotifyPushover(user=user, token=token, devices=set())
assert(isinstance(obj, plugins.NotifyPushover))
# Default is to send to all devices, so there will be a
# device defined here
assert(len(obj.devices) == 1)
# Support the handling of an empty and invalid URL strings
assert(plugins.NotifyPushover.parse_url(None) is None)
assert(plugins.NotifyPushover.parse_url('') is None)
assert(plugins.NotifyPushover.parse_url(42) is None)
@mock.patch('requests.get')
@mock.patch('requests.post')
def test_notify_toasty_plugin(mock_post, mock_get):
"""
API: NotifyToasty() Extra Checks
"""
# Support strings
devices = 'device1,device2,,,,'
# User
user = 'l2g'
# Prepare Mock
mock_get.return_value = requests.Request()
mock_post.return_value = requests.Request()
mock_post.return_value.status_code = requests.codes.ok
mock_get.return_value.status_code = requests.codes.ok
try:
obj = plugins.NotifyToasty(user=user, devices=None)
# No devices specified
assert(False)
except TypeError:
# Exception should be thrown about the fact no token was specified
assert(True)
try:
obj = plugins.NotifyToasty(user=user, devices=set())
# No devices specified
assert(False)
except TypeError:
# Exception should be thrown about the fact no token was specified
assert(True)
obj = plugins.NotifyToasty(user=user, devices=devices)
assert(isinstance(obj, plugins.NotifyToasty))
assert(len(obj.devices) == 2)
# Support the handling of an empty and invalid URL strings
assert(plugins.NotifyToasty.parse_url(None) is None)
assert(plugins.NotifyToasty.parse_url('') is None)
assert(plugins.NotifyToasty.parse_url(42) is None)
@mock.patch('requests.get')
@mock.patch('requests.post')
def test_notify_telegram_plugin(mock_post, mock_get):
"""
API: NotifyTelegram() Extra Checks
"""
# Bot Token
bot_token = '123456789:abcdefg_hijklmnop'
invalid_bot_token = 'abcd:123'
# Chat ID
chat_ids = 'l2g, lead2gold'
# Prepare Mock
mock_get.return_value = requests.Request()
mock_post.return_value = requests.Request()
mock_post.return_value.status_code = requests.codes.ok
mock_get.return_value.status_code = requests.codes.ok
try:
obj = plugins.NotifyTelegram(bot_token=None, chat_ids=chat_ids)
# invalid bot token (None)
assert(False)
except TypeError:
# Exception should be thrown about the fact no token was specified
assert(True)
try:
obj = plugins.NotifyTelegram(
bot_token=invalid_bot_token, chat_ids=chat_ids)
# invalid bot token
assert(False)
except TypeError:
# Exception should be thrown about the fact an invalid token was
# specified
assert(True)
try:
obj = plugins.NotifyTelegram(bot_token=bot_token, chat_ids=None)
# No chat_ids specified
assert(False)
except TypeError:
# Exception should be thrown about the fact no token was specified
assert(True)
try:
obj = plugins.NotifyTelegram(bot_token=bot_token, chat_ids=set())
# No chat_ids specified
assert(False)
except TypeError:
# Exception should be thrown about the fact no token was specified
assert(True)
obj = plugins.NotifyTelegram(bot_token=bot_token, chat_ids=chat_ids)
assert(isinstance(obj, plugins.NotifyTelegram))
assert(len(obj.chat_ids) == 2)
# Support the handling of an empty and invalid URL strings
assert(plugins.NotifyTelegram.parse_url(None) is None)
assert(plugins.NotifyTelegram.parse_url('') is None)
assert(plugins.NotifyTelegram.parse_url(42) is None)
# Prepare Mock to fail
response = mock.Mock()
response.status_code = requests.codes.internal_server_error
# a error response
response.text = dumps({
'description': 'test',
})
mock_get.return_value = response
mock_post.return_value = response
# No image asset
nimg_obj = plugins.NotifyTelegram(bot_token=bot_token, chat_ids=chat_ids)
nimg_obj.asset = AppriseAsset(image_path_mask=False, image_url_mask=False)
# Disable throttling to speed up unit tests
nimg_obj.throttle_attempt = 0
obj.throttle_attempt = 0
# This tests erroneous messages involving multiple chat ids
assert obj.notify(
title='title', body='body', notify_type=NotifyType.INFO) is False
assert nimg_obj.notify(
title='title', body='body', notify_type=NotifyType.INFO) is False
# This tests erroneous messages involving a single chat id
obj = plugins.NotifyTelegram(bot_token=bot_token, chat_ids='l2g')
nimg_obj = plugins.NotifyTelegram(bot_token=bot_token, chat_ids='l2g')
nimg_obj.asset = AppriseAsset(image_path_mask=False, image_url_mask=False)
assert obj.notify(
title='title', body='body', notify_type=NotifyType.INFO) is False
assert nimg_obj.notify(
title='title', body='body', notify_type=NotifyType.INFO) is False