diff --git a/apprise/plugins/NotifyBoxcar.py b/apprise/plugins/NotifyBoxcar.py index 2e8c4d07..f22e968b 100644 --- a/apprise/plugins/NotifyBoxcar.py +++ b/apprise/plugins/NotifyBoxcar.py @@ -268,20 +268,11 @@ class NotifyBoxcar(NotifyBase): access = results['host'] # Now fetch the remaining tokens - try: - secret = NotifyBase.split_path(results['fullpath'])[0] + secret = NotifyBase.split_path(results['fullpath'])[0] - except (AttributeError, IndexError): - # Force a bad value that will get caught in parsing later - secret = None - - try: - recipients = ','.join( - NotifyBase.split_path(results['fullpath'])[1:]) - - except (AttributeError, IndexError): - # Default to not having any recipients - recipients = None + # Our recipients + recipients = ','.join( + NotifyBase.split_path(results['fullpath'])[1:]) if not (access and secret): # If we did not recive an access and/or secret code diff --git a/apprise/plugins/NotifyFaast.py b/apprise/plugins/NotifyFaast.py index bd90ec31..8a3959c3 100644 --- a/apprise/plugins/NotifyFaast.py +++ b/apprise/plugins/NotifyFaast.py @@ -88,7 +88,7 @@ class NotifyFaast(NotifyBase): HTTP_ERROR_MAP[r.status_code], r.status_code)) - except IndexError: + except KeyError: self.logger.warning( 'Failed to send Faast notification ' '(error=%s).' % ( @@ -96,10 +96,11 @@ class NotifyFaast(NotifyBase): # Return; we're done return False + else: self.logger.info('Sent Faast notification.') - except requests.ConnectionError as e: + except requests.RequestException as e: self.logger.warning( 'A Connection error occured sending Faast notification.', ) diff --git a/apprise/plugins/NotifyGrowl/NotifyGrowl.py b/apprise/plugins/NotifyGrowl/NotifyGrowl.py index 29209f9d..2607eda6 100644 --- a/apprise/plugins/NotifyGrowl/NotifyGrowl.py +++ b/apprise/plugins/NotifyGrowl/NotifyGrowl.py @@ -18,10 +18,8 @@ import re -from .gntp.notifier import GrowlNotifier -from .gntp.errors import NetworkError as GrowlNetworkError -from .gntp.errors import AuthError as GrowlAuthenticationError - +from .gntp import notifier +from .gntp import errors from ..NotifyBase import NotifyBase from ...common import NotifyImageSize @@ -31,7 +29,7 @@ GROWL_IMAGE_XY = NotifyImageSize.XY_72 # Priorities class GrowlPriority(object): - VERY_LOW = -2 + LOW = -2 MODERATE = -1 NORMAL = 0 HIGH = 1 @@ -39,7 +37,7 @@ class GrowlPriority(object): GROWL_PRIORITIES = ( - GrowlPriority.VERY_LOW, + GrowlPriority.LOW, GrowlPriority.MODERATE, GrowlPriority.NORMAL, GrowlPriority.HIGH, @@ -61,7 +59,7 @@ class NotifyGrowl(NotifyBase): # Default Growl Port default_port = 23053 - def __init__(self, priority=GrowlPriority.NORMAL, version=2, **kwargs): + def __init__(self, priority=None, version=2, **kwargs): """ Initialize Growl Object """ @@ -69,9 +67,6 @@ class NotifyGrowl(NotifyBase): title_maxlen=250, body_maxlen=32768, image_size=GROWL_IMAGE_XY, **kwargs) - # A Global flag that tracks registration - self.is_registered = False - if not self.port: self.port = self.default_port @@ -100,27 +95,37 @@ class NotifyGrowl(NotifyBase): payload['password'] = self.password self.logger.debug('Growl Registration Payload: %s' % str(payload)) - self.growl = GrowlNotifier(**payload) + self.growl = notifier.GrowlNotifier(**payload) try: self.growl.register() - # Toggle our flag - self.is_registered = True self.logger.debug( 'Growl server registration completed successfully.' ) - except GrowlNetworkError: + except errors.NetworkError: self.logger.warning( 'A network error occured sending Growl ' 'notification to %s.' % self.host) - return + raise TypeError( + 'A network error occured sending Growl ' + 'notification to %s.' % self.host) - except GrowlAuthenticationError: + except errors.AuthError: self.logger.warning( 'An authentication error occured sending Growl ' 'notification to %s.' % self.host) - return + raise TypeError( + 'An authentication error occured sending Growl ' + 'notification to %s.' % self.host) + + except errors.UnsupportedError: + self.logger.warning( + 'An unsupported error occured sending Growl ' + 'notification to %s.' % self.host) + raise TypeError( + 'An unsupported error occured sending Growl ' + 'notification to %s.' % self.host) return @@ -129,10 +134,6 @@ class NotifyGrowl(NotifyBase): Perform Growl Notification """ - if not self.is_registered: - # We can't do anything - return None - # Limit results to just the first 2 line otherwise there is just to # much content to display body = re.split('[\r\n]+', body) @@ -158,7 +159,9 @@ class NotifyGrowl(NotifyBase): } self.logger.debug('Growl Payload: %s' % str(payload)) - # Update icon of payload to be raw data + # Update icon of payload to be raw data; this is intentionally done + # here after we spit the debug message above (so we don't try to + # print the binary contents of an image payload['icon'] = icon try: @@ -174,7 +177,7 @@ class NotifyGrowl(NotifyBase): 'Growl notification sent successfully.' ) - except GrowlNetworkError as e: + except errors.BaseError as e: # Since Growl servers listen for UDP broadcasts, it's possible # that you will never get to this part of the code since there is # no acknowledgement as to whether it accepted what was sent to it @@ -221,13 +224,35 @@ class NotifyGrowl(NotifyBase): ) pass + if 'priority' in results['qsd'] and len(results['qsd']['priority']): + _map = { + 'l': GrowlPriority.LOW, + '-2': GrowlPriority.LOW, + 'm': GrowlPriority.MODERATE, + '-1': GrowlPriority.MODERATE, + 'n': GrowlPriority.NORMAL, + '0': GrowlPriority.NORMAL, + 'h': GrowlPriority.HIGH, + '1': GrowlPriority.HIGH, + 'e': GrowlPriority.EMERGENCY, + '2': GrowlPriority.EMERGENCY, + } + try: + results['priority'] = \ + _map[results['qsd']['priority'][0].lower()] + + except KeyError: + # No priority was set + pass + # Because of the URL formatting, the password is actually where the # username field is. For this reason, we just preform this small hack # to make it (the URL) conform correctly. The following strips out the # existing password entry (if exists) so that it can be swapped with # the new one we specify. - results['user'] = None - results['password'] = results.get('user', None) + if results.get('password', None) is None: + results['password'] = results.get('user', None) + if version: results['version'] = version diff --git a/apprise/plugins/NotifyJoin.py b/apprise/plugins/NotifyJoin.py index 258702e2..6e4562af 100644 --- a/apprise/plugins/NotifyJoin.py +++ b/apprise/plugins/NotifyJoin.py @@ -92,9 +92,9 @@ class NotifyJoin(NotifyBase): self.apikey = apikey.strip() 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, - )) + ))] elif isinstance(devices, (set, tuple, list)): self.devices = devices @@ -103,19 +103,24 @@ class NotifyJoin(NotifyBase): self.devices = list() if len(self.devices) == 0: - self.logger.warning('No device(s) were specified.') - raise TypeError('No device(s) were specified.') + # Default to everyone + self.devices.append('group.all') def notify(self, title, body, notify_type, **kwargs): """ Perform Join Notification """ - # Limit results to just the first 2 line otherwise - # there is just to much content to display - body = re.split('[\r\n]+', body) - body[0] = body[0].strip('#').strip() - body = '\r\n'.join(body[0:2]) + try: + # Limit results to just the first 2 line otherwise + # there is just to much content to display + body = re.split('[\r\n]+', body) + body[0] = body[0].strip('#').strip() + body = '\r\n'.join(body[0:2]) + + except (AttributeError, TypeError): + # body was None or not of a type string + body = '' headers = { 'User-Agent': self.app_id, @@ -123,7 +128,7 @@ class NotifyJoin(NotifyBase): } # error tracking (used for function return) - has_error = False + return_status = True # Create a copy of the devices list devices = list(self.devices) @@ -135,7 +140,7 @@ class NotifyJoin(NotifyBase): elif not IS_DEVICE_RE.match(device): self.logger.warning( - "The specified device '%s' is invalid; skipping." % ( + "The specified device/group '%s' is invalid; skipping." % ( device, ) ) @@ -180,7 +185,7 @@ class NotifyJoin(NotifyBase): JOIN_HTTP_ERROR_MAP[r.status_code], r.status_code)) - except IndexError: + except KeyError: self.logger.warning( 'Failed to send Join:%s ' 'notification (error=%s).' % ( @@ -189,22 +194,21 @@ class NotifyJoin(NotifyBase): # self.logger.debug('Response Details: %s' % r.raw.read()) - # Return; we're done - has_error = True + return_status = False - except requests.ConnectionError as e: + except requests.RequestException as e: self.logger.warning( 'A Connection error occured sending Join:%s ' 'notification.' % device ) self.logger.debug('Socket Exception: %s' % str(e)) - has_error = True + return_status = False if len(devices): # Prevent thrashing requests self.throttle() - return has_error + return return_status @staticmethod def parse_url(url): @@ -220,14 +224,8 @@ class NotifyJoin(NotifyBase): return results # Apply our settings now - try: - devices = ' '.join( - filter(bool, NotifyBase.split_path(results['fullpath']))) - - except (AttributeError, IndexError): - # Force some bad values that will get caught - # in parsing later - devices = None + devices = ' '.join( + filter(bool, NotifyBase.split_path(results['fullpath']))) results['apikey'] = results['host'] results['devices'] = devices diff --git a/apprise/plugins/NotifyMatterMost.py b/apprise/plugins/NotifyMatterMost.py index b9821438..dbc2a7d8 100644 --- a/apprise/plugins/NotifyMatterMost.py +++ b/apprise/plugins/NotifyMatterMost.py @@ -176,13 +176,7 @@ class NotifyMatterMost(NotifyBase): return results # Apply our settings now - try: - authtoken = NotifyBase.split_path(results['fullpath'])[0] - - except (AttributeError, IndexError): - # Force some bad values that will get caught - # in parsing later - authtoken = None + authtoken = NotifyBase.split_path(results['fullpath'])[0] channel = None if 'channel' in results['qsd'] and len(results['qsd']['channel']): diff --git a/apprise/plugins/NotifyMyAndroid.py b/apprise/plugins/NotifyMyAndroid.py index 6d0c0665..586ca653 100644 --- a/apprise/plugins/NotifyMyAndroid.py +++ b/apprise/plugins/NotifyMyAndroid.py @@ -37,7 +37,7 @@ VALIDATE_APIKEY = re.compile(r'[A-Za-z0-9]{48}') # Priorities class NotifyMyAndroidPriority(object): - VERY_LOW = -2 + LOW = -2 MODERATE = -1 NORMAL = 0 HIGH = 1 @@ -45,7 +45,7 @@ class NotifyMyAndroidPriority(object): NMA_PRIORITIES = ( - NotifyMyAndroidPriority.VERY_LOW, + NotifyMyAndroidPriority.LOW, NotifyMyAndroidPriority.MODERATE, NotifyMyAndroidPriority.NORMAL, NotifyMyAndroidPriority.HIGH, @@ -64,8 +64,7 @@ class NotifyMyAndroid(NotifyBase): # Notify My Android uses the http protocol with JSON requests notify_url = 'https://www.notifymyandroid.com/publicapi/notify' - def __init__(self, apikey, priority=NotifyMyAndroidPriority.NORMAL, - devapikey=None, **kwargs): + def __init__(self, apikey, priority=None, devapikey=None, **kwargs): """ Initialize Notify My Android Object """ @@ -143,7 +142,7 @@ class NotifyMyAndroid(NotifyBase): NMA_HTTP_ERROR_MAP[r.status_code], r.status_code)) - except IndexError: + except KeyError: self.logger.warning( 'Failed to send NMA notification (error=%s).' % ( r.status_code)) @@ -152,10 +151,10 @@ class NotifyMyAndroid(NotifyBase): return False else: - self.logger.debug('NMA Server Response: %s.' % r.text) + self.logger.debug('NMA Server Response: %s.' % r.raw.read()) self.logger.info('Sent NMA notification.') - except requests.ConnectionError as e: + except requests.RequestException as e: self.logger.warning( 'A Connection error occured sending NMA notification.' ) @@ -192,6 +191,32 @@ class NotifyMyAndroid(NotifyBase): except AttributeError: pass + if 'priority' in results['qsd'] and len(results['qsd']['priority']): + _map = { + 'l': NotifyMyAndroidPriority.LOW, + '-2': NotifyMyAndroidPriority.LOW, + 'm': NotifyMyAndroidPriority.MODERATE, + '-1': NotifyMyAndroidPriority.MODERATE, + 'n': NotifyMyAndroidPriority.NORMAL, + '0': NotifyMyAndroidPriority.NORMAL, + 'h': NotifyMyAndroidPriority.HIGH, + '1': NotifyMyAndroidPriority.HIGH, + 'e': NotifyMyAndroidPriority.EMERGENCY, + '2': NotifyMyAndroidPriority.EMERGENCY, + } + try: + results['priority'] = \ + _map[results['qsd']['priority'][0].lower()] + + except KeyError: + # No priority was set + pass + + # Now fetch devapi if specified + devapi = NotifyBase.split_path(results['fullpath'])[0] + if devapi: + results['devapikey'] = devapi + results['apikey'] = results['host'] return results diff --git a/apprise/plugins/__init__.py b/apprise/plugins/__init__.py index 17ff54c0..b730fa85 100644 --- a/apprise/plugins/__init__.py +++ b/apprise/plugins/__init__.py @@ -23,7 +23,8 @@ from . import NotifyEmail as NotifyEmailBase from .NotifyBoxcar import NotifyBoxcar from .NotifyEmail import NotifyEmail from .NotifyFaast import NotifyFaast -from .NotifyGrowl import NotifyGrowl +from .NotifyGrowl.NotifyGrowl import NotifyGrowl +from .NotifyGrowl import gntp from .NotifyJSON import NotifyJSON from .NotifyMyAndroid import NotifyMyAndroid from .NotifyProwl import NotifyProwl @@ -32,14 +33,14 @@ from .NotifyPushBullet import NotifyPushBullet from .NotifyPushover import NotifyPushover from .NotifyRocketChat import NotifyRocketChat from .NotifyToasty import NotifyToasty -from .NotifyTwitter import NotifyTwitter +from .NotifyTwitter.NotifyTwitter import NotifyTwitter from .NotifyXBMC import NotifyXBMC from .NotifyXML import NotifyXML from .NotifySlack import NotifySlack from .NotifyJoin import NotifyJoin from .NotifyTelegram import NotifyTelegram from .NotifyMatterMost import NotifyMatterMost -from .NotifyPushjet import NotifyPushjet +from .NotifyPushjet.NotifyPushjet import NotifyPushjet from ..common import NotifyImageSize from ..common import NOTIFY_IMAGE_SIZES @@ -59,4 +60,7 @@ __all__ = [ # NotifyEmail Base References (used for Testing) 'NotifyEmailBase', + + # gntp (used for Testing) + 'gntp', ] diff --git a/test/test_email_plugin.py b/test/test_email_plugin.py index 68c92a0b..e4f55430 100644 --- a/test/test_email_plugin.py +++ b/test/test_email_plugin.py @@ -24,7 +24,7 @@ import mock import re -VALID_URLS = ( +TEST_URLS = ( ################################## # NotifyEmail ################################## @@ -138,7 +138,7 @@ def test_email_plugin(mock_smtp): """ # iterate over our dictionary and test it out - for (url, meta) in VALID_URLS: + for (url, meta) in TEST_URLS: # Our expected instance instance = meta.get('instance', None) @@ -152,11 +152,6 @@ def test_email_plugin(mock_smtp): # Our expected Query response (True, False, or exception type) response = meta.get('response', True) - # Allow us to force the server response code to be something other then - # the defaults - smtplib_response_code = meta.get( - 'smtplib_response_code', 200 if response else 404) - test_smtplib_exceptions = meta.get( 'test_smtplib_exceptions', False) @@ -168,16 +163,7 @@ def test_email_plugin(mock_smtp): # Create a mock SMTP Object mock_smtp.return_value = mock_socket - if test_smtplib_exceptions is False: - pass - # Handle our default response - mock_socket.sendmail.return_value = smtplib_response_code - # mock_post.return_value.status_code = smtplib_response_code - # mock_get.return_value.status_code = smtplib_response_code - # mock_post.side_effect = None - # mock_get.side_effect = None - - else: + if test_smtplib_exceptions: # Handle exception testing; first we turn the boolean flag ito # a list of exceptions test_smtplib_exceptions = ( diff --git a/test/test_growl_plugin.py b/test/test_growl_plugin.py new file mode 100644 index 00000000..06efeb26 --- /dev/null +++ b/test/test_growl_plugin.py @@ -0,0 +1,265 @@ +# -*- coding: utf-8 -*- +# +# NotifyGrowl - Unit Tests +# +# Copyright (C) 2017 Chris Caron +# +# This file is part of apprise. +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. + +from apprise import plugins +from apprise import NotifyType +from apprise import Apprise +import mock +import re + + +TEST_URLS = ( + ################################## + # NotifyGrowl + ################################## + ('growl://', { + 'instance': None, + }), + ('growl://:@/', { + 'instance': None + }), + + ('growl://pass@growl.server', { + 'instance': plugins.NotifyGrowl, + }), + ('growl://ignored:pass@growl.server', { + 'instance': plugins.NotifyGrowl, + }), + ('growl://growl.server', { + 'instance': plugins.NotifyGrowl, + # don't include an image by default + 'include_image': False, + }), + ('growl://growl.server?version=1', { + 'instance': plugins.NotifyGrowl, + }), + # Force a failure + ('growl://growl.server?version=1', { + 'instance': plugins.NotifyGrowl, + 'growl_response': None, + }), + ('growl://growl.server?version=2', { + # don't include an image by default + 'include_image': False, + 'instance': plugins.NotifyGrowl, + }), + ('growl://growl.server?version=2', { + # don't include an image by default + 'include_image': False, + 'instance': plugins.NotifyGrowl, + 'growl_response': None, + }), + + # Priorities + ('growl://pass@growl.server?priority=low', { + 'instance': plugins.NotifyGrowl, + }), + ('growl://pass@growl.server?priority=moderate', { + 'instance': plugins.NotifyGrowl, + }), + ('growl://pass@growl.server?priority=normal', { + 'instance': plugins.NotifyGrowl, + }), + ('growl://pass@growl.server?priority=high', { + 'instance': plugins.NotifyGrowl, + }), + ('growl://pass@growl.server?priority=emergency', { + 'instance': plugins.NotifyGrowl, + }), + + # Invalid Priorities + ('growl://pass@growl.server?priority=invalid', { + 'instance': plugins.NotifyGrowl, + }), + ('growl://pass@growl.server?priority=', { + 'instance': plugins.NotifyGrowl, + }), + + # invalid version + ('growl://growl.server?version=', { + 'instance': plugins.NotifyGrowl, + }), + ('growl://growl.server?version=crap', { + 'instance': plugins.NotifyGrowl, + }), + + # Ports + ('growl://growl.changeport:2000', { + 'instance': plugins.NotifyGrowl, + }), + ('growl://growl.garbageport:garbage', { + 'instance': plugins.NotifyGrowl, + }), + ('growl://growl.colon:', { + 'instance': plugins.NotifyGrowl, + }), + # Exceptions + ('growl://growl.exceptions01', { + 'instance': plugins.NotifyGrowl, + # Throws a series of connection and transfer exceptions when this flag + # is set and tests that we gracfully handle them + 'test_growl_notify_exceptions': True, + }), + ('growl://growl.exceptions02', { + 'instance': plugins.NotifyGrowl, + # Throws a series of connection and transfer exceptions when this flag + # is set and tests that we gracfully handle them + 'test_growl_register_exceptions': True, + }), +) + + +@mock.patch('apprise.plugins.gntp.notifier.GrowlNotifier') +def test_growl_plugin(mock_gntp): + """ + API: NotifyGrowl Plugin() + + """ + + # iterate over our dictionary and test it out + for (url, meta) in TEST_URLS: + + # Our expected instance + instance = meta.get('instance', None) + + # Our expected exception + exception = meta.get('exception', None) + + # Our expected server objects + self = meta.get('self', None) + + # Our expected Query response (True, False, or exception type) + response = meta.get('response', True) + + # Allow us to force the server response code to be something other then + # the defaults + growl_response = meta.get( + 'growl_response', True if response else False) + + test_growl_notify_exceptions = meta.get( + 'test_growl_notify_exceptions', False) + + test_growl_register_exceptions = meta.get( + 'test_growl_register_exceptions', False) + + mock_notifier = mock.Mock() + mock_gntp.return_value = mock_notifier + + test_growl_exceptions = ( + plugins.gntp.errors.NetworkError( + 0, 'gntp.ParseError() not handled'), + plugins.gntp.errors.AuthError( + 0, 'gntp.AuthError() not handled'), + plugins.gntp.errors.UnsupportedError( + 'gntp.UnsupportedError() not handled'), + ) + + if test_growl_notify_exceptions is True: + # Store oure exceptions + test_growl_notify_exceptions = test_growl_exceptions + + elif test_growl_register_exceptions is True: + # Store oure exceptions + test_growl_register_exceptions = test_growl_exceptions + + for exception in test_growl_register_exceptions: + mock_notifier.register.side_effect = exception + try: + obj = Apprise.instantiate(url, suppress_exceptions=False) + + except TypeError: + # This is the response we expect + assert True + + except Exception as e: + # We can't handle this exception type + print('%s / %s' % (url, str(e))) + assert False + + # We're done this part of the test + continue + + else: + # Store our response + mock_notifier.notify.return_value = growl_response + + try: + obj = Apprise.instantiate(url, suppress_exceptions=False) + + assert(exception is None) + + if obj is None: + # We're done + continue + + if instance is None: + # Expected None but didn't get it + print('%s instantiated %s' % (url, str(obj))) + assert(False) + + assert(isinstance(obj, instance)) + + if self: + # Iterate over our expected entries inside of our object + for key, val in self.items(): + # Test that our object has the desired key + assert(hasattr(key, obj)) + assert(getattr(key, obj) == val) + + try: + if test_growl_notify_exceptions is False: + # check that we're as expected + assert obj.notify( + title='test', body='body', + notify_type=NotifyType.INFO) == response + + else: + for exception in test_growl_notify_exceptions: + mock_notifier.notify.side_effect = exception + try: + assert obj.notify( + title='test', body='body', + notify_type=NotifyType.INFO) is False + + except AssertionError: + # Don't mess with these entries + raise + + except Exception as e: + # We can't handle this exception type + print('%s / %s' % (url, str(e))) + assert False + + except AssertionError: + # Don't mess with these entries + raise + + except Exception as e: + # Check that we were expecting this exception to happen + assert isinstance(e, response) + + except AssertionError: + # Don't mess with these entries + print('%s AssertionError' % url) + raise + + except Exception as e: + # Handle our exception + print('%s / %s' % (url, str(e))) + assert(exception is not None) + assert(isinstance(e, exception)) diff --git a/test/test_notify_base.py b/test/test_notify_base.py index 68cd5340..f4af0a19 100644 --- a/test/test_notify_base.py +++ b/test/test_notify_base.py @@ -111,6 +111,12 @@ def test_notify_base(): # Test is_hostname assert NotifyBase.is_hostname('example.com') is True + # Test quote + assert NotifyBase.unquote('%20') == ' ' + assert NotifyBase.quote(' ') == '%20' + assert NotifyBase.unquote(None) == '' + assert NotifyBase.quote(None) == '' + def test_notify_base_urls(): """ diff --git a/test/test_rest_plugins.py b/test/test_rest_plugins.py index 28f2defa..7c975353 100644 --- a/test/test_rest_plugins.py +++ b/test/test_rest_plugins.py @@ -87,6 +87,103 @@ TEST_URLS = ( 'test_requests_exceptions': True, }), + ################################## + # NotifyFaast + ################################## + ('faast://', { + 'instance': None, + }), + # Auth Token specified + ('faast://%s' % ('a' * 32), { + 'instance': plugins.NotifyFaast, + }), + ('faast://%s' % ('a' * 32), { + 'instance': plugins.NotifyFaast, + # don't include an image by default + 'include_image': False, + }), + ('faast://:@/', { + 'instance': None, + }), + ('faast://%s' % ('a' * 32), { + 'instance': plugins.NotifyFaast, + # force a failure + 'response': False, + 'requests_response_code': requests.codes.internal_server_error, + }), + ('faast://%s' % ('a' * 32), { + 'instance': plugins.NotifyFaast, + # throw a bizzare code forcing us to fail to look it up + 'response': False, + 'requests_response_code': 999, + }), + ('faast://%s' % ('a' * 32), { + 'instance': plugins.NotifyFaast, + # Throws a series of connection and transfer exceptions when this flag + # is set and tests that we gracfully handle them + 'test_requests_exceptions': True, + }), + + ################################## + # NotifyJoin + ################################## + ('join://', { + 'instance': None, + }), + # APIkey; no device + ('join://%s' % ('a' * 32), { + 'instance': plugins.NotifyJoin, + }), + # Invalid APIKey + ('join://%s' % ('a' * 24), { + 'instance': None, + # Missing a channel + 'exception': TypeError, + }), + # APIKey + device + ('join://%s/%s' % ('a' * 32, 'd' * 32), { + 'instance': plugins.NotifyJoin, + # don't include an image by default + 'include_image': False, + }), + # APIKey + 2 devices + ('join://%s/%s/%s' % ('a' * 32, 'd' * 32, 'e' * 32), { + 'instance': plugins.NotifyJoin, + # don't include an image by default + 'include_image': False, + }), + # APIKey + 1 device and 1 group + ('join://%s/%s/%s' % ('a' * 32, 'd' * 32, 'group.chrome'), { + 'instance': plugins.NotifyJoin, + }), + # APIKey + bad device + ('join://%s/%s' % ('a' * 32, 'd' * 10), { + 'instance': plugins.NotifyJoin, + }), + # APIKey + bad url + ('join://:@/', { + 'instance': None, + }), + ('join://%s' % ('a' * 32), { + 'instance': plugins.NotifyJoin, + # force a failure + 'response': False, + 'requests_response_code': requests.codes.internal_server_error, + }), + ('join://%s' % ('a' * 32), { + 'instance': plugins.NotifyJoin, + # throw a bizzare code forcing us to fail to look it up + 'response': False, + 'requests_response_code': 999, + }), + # apikey = a + ('join://%s' % ('a' * 32), { + 'instance': plugins.NotifyJoin, + # Throws a series of connection and transfer exceptions when this flag + # is set and tests that we gracfully handle them + 'test_requests_exceptions': True, + }), + ################################## # NotifyJSON ################################## @@ -271,6 +368,56 @@ TEST_URLS = ( 'test_requests_exceptions': True, }), + ################################## + # NotifyMyAndroid + ################################## + ('nma://', { + 'instance': None, + }), + # APIkey; no device + ('nma://%s' % ('a' * 48), { + 'instance': plugins.NotifyMyAndroid, + }), + # Invalid APIKey + ('nma://%s' % ('a' * 24), { + 'instance': None, + # Missing a channel + 'exception': TypeError, + }), + # APIKey + ('nma://%s' % ('a' * 48), { + 'instance': plugins.NotifyMyAndroid, + # don't include an image by default + 'include_image': False, + }), + # APIKey + with image + ('nma://%s' % ('a' * 48), { + 'instance': plugins.NotifyMyAndroid, + }), + # bad url + ('nma://:@/', { + 'instance': None, + }), + ('nma://%s' % ('a' * 48), { + 'instance': plugins.NotifyMyAndroid, + # force a failure + 'response': False, + 'requests_response_code': requests.codes.internal_server_error, + }), + ('nma://%s' % ('a' * 48), { + 'instance': plugins.NotifyMyAndroid, + # throw a bizzare code forcing us to fail to look it up + 'response': False, + 'requests_response_code': 999, + }), + # apikey = a + ('nma://%s' % ('a' * 48), { + 'instance': plugins.NotifyMyAndroid, + # 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 ################################## @@ -498,8 +645,14 @@ def test_rest_plugins(mock_post, mock_get): test_requests_exceptions = meta.get( 'test_requests_exceptions', False) - mock_get.return_value = requests.Request() - mock_post.return_value = requests.Request() + # A request + robj = mock.Mock() + setattr(robj, 'raw', mock.Mock()) + # Allow raw.read() calls + robj.raw.read.return_value = '' + mock_get.return_value = robj + mock_post.return_value = robj + if test_requests_exceptions is False: # Handle our default response mock_post.return_value.status_code = requests_response_code @@ -527,10 +680,13 @@ def test_rest_plugins(mock_post, mock_get): obj = Apprise.instantiate( url, asset=asset, suppress_exceptions=False) - assert(exception is None) + # Make sure we weren't expecting an exception and just didn't get + # one. + assert exception is None if obj is None: - # We're done + # We're done (assuming this is what we were expecting) + assert instance is None continue if instance is None: @@ -646,11 +802,44 @@ def test_notify_boxcar_plugin(mock_post, mock_get): mock_post.return_value = requests.Request() mock_post.return_value.status_code = requests.codes.created mock_get.return_value.status_code = requests.codes.created + # Test notifications without a body or a title p = plugins.NotifyBoxcar(access=access, secret=secret, recipients=None) p.notify(body=None, title=None, notify_type=NotifyType.INFO) is True +@mock.patch('requests.get') +@mock.patch('requests.post') +def test_notify_join_plugin(mock_post, mock_get): + """ + API: NotifyJoin() Extra Checks + + """ + # Generate some generic message types + device = 'A' * 32 + group = 'group.chrome' + apikey = 'a' * 32 + + # Initializes the plugin with devices set to a string + plugins.NotifyJoin(apikey=apikey, devices=group) + + # Initializes the plugin with devices set to None + plugins.NotifyJoin(apikey=apikey, devices=None) + + # Initializes the plugin with devices set to a set + p = plugins.NotifyJoin(apikey=apikey, devices=[group, device]) + + # Prepare our mock responses + mock_get.return_value = requests.Request() + mock_post.return_value = requests.Request() + mock_post.return_value.status_code = requests.codes.created + mock_get.return_value.status_code = requests.codes.created + + # Test notifications without a body or a title; nothing to send + # so we return False + p.notify(body=None, title=None, notify_type=NotifyType.INFO) is False + + @mock.patch('requests.get') @mock.patch('requests.post') def test_notify_slack_plugin(mock_post, mock_get):