From c6922d8f3a791934c9d33eddc68f055756394043 Mon Sep 17 00:00:00 2001 From: Chris Caron Date: Wed, 9 Oct 2019 12:39:31 -0400 Subject: [PATCH] Refactored qa, unit testing, and service init; refs #157 (#160) --- apprise/plugins/NotifyBoxcar.py | 51 +- apprise/plugins/NotifyClickSend.py | 2 +- apprise/plugins/NotifyD7Networks.py | 4 +- apprise/plugins/NotifyDBus.py | 2 + apprise/plugins/NotifyDiscord.py | 19 +- apprise/plugins/NotifyEmby.py | 2 +- apprise/plugins/NotifyFaast.py | 2 + apprise/plugins/NotifyFlock.py | 38 +- apprise/plugins/NotifyGitter.py | 39 +- apprise/plugins/NotifyGnome.py | 2 + apprise/plugins/NotifyGotify.py | 16 +- apprise/plugins/NotifyGrowl/__init__.py | 1 - apprise/plugins/NotifyIFTTT.py | 12 +- apprise/plugins/NotifyJoin.py | 166 +++- apprise/plugins/NotifyKumulos.py | 54 +- apprise/plugins/NotifyMSG91.py | 41 +- apprise/plugins/NotifyMSTeams.py | 75 +- apprise/plugins/NotifyMailgun.py | 17 +- apprise/plugins/NotifyMatterMost.py | 25 +- apprise/plugins/NotifyMessageBird.py | 52 +- apprise/plugins/NotifyNexmo.py | 54 +- apprise/plugins/NotifyProwl.py | 40 +- apprise/plugins/NotifyPushBullet.py | 11 +- apprise/plugins/NotifyPushed.py | 63 +- apprise/plugins/NotifyPushjet.py | 21 +- apprise/plugins/NotifyPushover.py | 78 +- apprise/plugins/NotifyRyver.py | 45 +- apprise/plugins/NotifySNS.py | 45 +- apprise/plugins/NotifySendGrid.py | 21 +- apprise/plugins/NotifySimplePush.py | 30 +- apprise/plugins/NotifySlack.py | 103 +-- apprise/plugins/NotifyTechulusPush.py | 26 +- apprise/plugins/NotifyTelegram.py | 35 +- apprise/plugins/NotifyTwilio.py | 59 +- apprise/plugins/NotifyTwitter.py | 40 +- apprise/plugins/NotifyWebexTeams.py | 19 +- apprise/plugins/NotifyXMPP.py | 2 +- apprise/plugins/NotifyZulip.py | 20 +- apprise/plugins/__init__.py | 10 + apprise/utils.py | 97 +- test/test_api.py | 433 +++++---- test/test_config_base.py | 15 +- test/test_email_plugin.py | 32 +- test/test_gitter_plugin.py | 30 +- test/test_glib_plugin.py | 178 ++-- test/test_growl_plugin.py | 22 +- test/test_notify_base.py | 17 +- test/test_rest_plugins.py | 1082 +++++++++++------------ test/test_sns_plugin.py | 161 ++-- test/test_twitter_plugin.py | 52 +- test/test_utils.py | 561 ++++++------ test/test_windows_plugin.py | 45 +- 52 files changed, 2066 insertions(+), 2001 deletions(-) diff --git a/apprise/plugins/NotifyBoxcar.py b/apprise/plugins/NotifyBoxcar.py index 72c0e8f5..341c5098 100644 --- a/apprise/plugins/NotifyBoxcar.py +++ b/apprise/plugins/NotifyBoxcar.py @@ -40,6 +40,7 @@ except ImportError: from .NotifyBase import NotifyBase from ..URLBase import PrivacyMode from ..utils import parse_bool +from ..utils import validate_regex from ..common import NotifyType from ..common import NotifyImageSize from ..AppriseLocale import gettext_lazy as _ @@ -58,11 +59,6 @@ IS_TAG = re.compile(r'^[@](?P[A-Z0-9]{1,63})$', re.I) # this plugin supports it. IS_DEVICETOKEN = re.compile(r'^[A-Z0-9]{64}$', re.I) -# Both an access key and seret key are created and assigned to each project -# you create on the boxcar website -VALIDATE_ACCESS = re.compile(r'[A-Z0-9_-]{64}', re.I) -VALIDATE_SECRET = re.compile(r'[A-Z0-9_-]{64}', re.I) - # Used to break apart list of potential tags by their delimiter into a useable # list. TAGS_LIST_DELIM = re.compile(r'[ \t\r\n,\\/]+') @@ -105,30 +101,30 @@ class NotifyBoxcar(NotifyBase): 'access_key': { 'name': _('Access Key'), 'type': 'string', - 'regex': (r'[A-Z0-9_-]{64}', 'i'), 'private': True, 'required': True, + 'regex': (r'^[A-Z0-9_-]{64}$', 'i'), 'map_to': 'access', }, 'secret_key': { 'name': _('Secret Key'), 'type': 'string', - 'regex': (r'[A-Z0-9_-]{64}', 'i'), 'private': True, 'required': True, + 'regex': (r'^[A-Z0-9_-]{64}$', 'i'), 'map_to': 'secret', }, 'target_tag': { 'name': _('Target Tag ID'), 'type': 'string', 'prefix': '@', - 'regex': (r'[A-Z0-9]{1,63}', 'i'), + 'regex': (r'^[A-Z0-9]{1,63}$', 'i'), 'map_to': 'targets', }, 'target_device': { 'name': _('Target Device ID'), 'type': 'string', - 'regex': (r'[A-Z0-9]{64}', 'i'), + 'regex': (r'^[A-Z0-9]{64}$', 'i'), 'map_to': 'targets', }, 'targets': { @@ -163,33 +159,21 @@ class NotifyBoxcar(NotifyBase): # Initialize device_token list self.device_tokens = list() - try: - # Access Key (associated with project) - self.access = access.strip() - - except AttributeError: - msg = 'The specified access key is invalid.' + # Access Key (associated with project) + self.access = validate_regex( + access, *self.template_tokens['access_key']['regex']) + if not self.access: + msg = 'An invalid Boxcar Access Key ' \ + '({}) was specified.'.format(access) self.logger.warning(msg) raise TypeError(msg) - try: - # Secret Key (associated with project) - self.secret = secret.strip() - - except AttributeError: - msg = 'The specified secret key is invalid.' - self.logger.warning(msg) - raise TypeError(msg) - - if not VALIDATE_ACCESS.match(self.access): - msg = 'The access key specified ({}) is invalid.'\ - .format(self.access) - self.logger.warning(msg) - raise TypeError(msg) - - if not VALIDATE_SECRET.match(self.secret): - msg = 'The secret key specified ({}) is invalid.'\ - .format(self.secret) + # Secret Key (associated with project) + self.secret = validate_regex( + secret, *self.template_tokens['secret_key']['regex']) + if not self.secret: + msg = 'An invalid Boxcar Secret Key ' \ + '({}) was specified.'.format(secret) self.logger.warning(msg) raise TypeError(msg) @@ -228,7 +212,6 @@ class NotifyBoxcar(NotifyBase): """ Perform Boxcar Notification """ - headers = { 'User-Agent': self.app_id, 'Content-Type': 'application/json' diff --git a/apprise/plugins/NotifyClickSend.py b/apprise/plugins/NotifyClickSend.py index 8535bd27..4bc36dc9 100644 --- a/apprise/plugins/NotifyClickSend.py +++ b/apprise/plugins/NotifyClickSend.py @@ -112,7 +112,7 @@ class NotifyClickSend(NotifyBase): 'name': _('Target Phone No'), 'type': 'string', 'prefix': '+', - 'regex': (r'[0-9\s)(+-]+', 'i'), + 'regex': (r'^[0-9\s)(+-]+$', 'i'), 'map_to': 'targets', }, 'targets': { diff --git a/apprise/plugins/NotifyD7Networks.py b/apprise/plugins/NotifyD7Networks.py index 3596159f..4de5a285 100644 --- a/apprise/plugins/NotifyD7Networks.py +++ b/apprise/plugins/NotifyD7Networks.py @@ -131,7 +131,7 @@ class NotifyD7Networks(NotifyBase): 'name': _('Target Phone No'), 'type': 'string', 'prefix': '+', - 'regex': (r'[0-9\s)(+-]+', 'i'), + 'regex': (r'^[0-9\s)(+-]+$', 'i'), 'map_to': 'targets', }, 'targets': { @@ -227,6 +227,8 @@ class NotifyD7Networks(NotifyBase): self.logger.warning(msg) raise TypeError(msg) + return + def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs): """ Depending on whether we are set to batch mode or single mode this diff --git a/apprise/plugins/NotifyDBus.py b/apprise/plugins/NotifyDBus.py index b82c3e08..c9463e4d 100644 --- a/apprise/plugins/NotifyDBus.py +++ b/apprise/plugins/NotifyDBus.py @@ -240,6 +240,8 @@ class NotifyDBus(NotifyBase): # or not. self.include_image = include_image + return + def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs): """ Perform DBus Notification diff --git a/apprise/plugins/NotifyDiscord.py b/apprise/plugins/NotifyDiscord.py index 1d2981e2..28385917 100644 --- a/apprise/plugins/NotifyDiscord.py +++ b/apprise/plugins/NotifyDiscord.py @@ -49,6 +49,7 @@ from ..common import NotifyImageSize from ..common import NotifyFormat from ..common import NotifyType from ..utils import parse_bool +from ..utils import validate_regex from ..AppriseLocale import gettext_lazy as _ @@ -144,20 +145,22 @@ class NotifyDiscord(NotifyBase): """ super(NotifyDiscord, self).__init__(**kwargs) - if not webhook_id: - msg = 'An invalid Client ID was specified.' + # Webhook ID (associated with project) + self.webhook_id = validate_regex(webhook_id) + if not self.webhook_id: + msg = 'An invalid Discord Webhook ID ' \ + '({}) was specified.'.format(webhook_id) self.logger.warning(msg) raise TypeError(msg) - if not webhook_token: - msg = 'An invalid Webhook Token was specified.' + # Webhook Token (associated with project) + self.webhook_token = validate_regex(webhook_token) + if not self.webhook_token: + msg = 'An invalid Discord Webhook Token ' \ + '({}) was specified.'.format(webhook_token) self.logger.warning(msg) raise TypeError(msg) - # Store our data - self.webhook_id = webhook_id - self.webhook_token = webhook_token - # Text To Speech self.tts = tts diff --git a/apprise/plugins/NotifyEmby.py b/apprise/plugins/NotifyEmby.py index 5fa56122..19afc5ad 100644 --- a/apprise/plugins/NotifyEmby.py +++ b/apprise/plugins/NotifyEmby.py @@ -139,7 +139,7 @@ class NotifyEmby(NotifyBase): if not self.user: # User was not specified - msg = 'No Username was specified.' + msg = 'No Emby username was specified.' self.logger.warning(msg) raise TypeError(msg) diff --git a/apprise/plugins/NotifyFaast.py b/apprise/plugins/NotifyFaast.py index 1a19b8cb..4c7b1ad7 100644 --- a/apprise/plugins/NotifyFaast.py +++ b/apprise/plugins/NotifyFaast.py @@ -91,6 +91,8 @@ class NotifyFaast(NotifyBase): # Associate an image with our post self.include_image = include_image + return + def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs): """ Perform Faast Notification diff --git a/apprise/plugins/NotifyFlock.py b/apprise/plugins/NotifyFlock.py index dadf42f5..9f66034e 100644 --- a/apprise/plugins/NotifyFlock.py +++ b/apprise/plugins/NotifyFlock.py @@ -47,6 +47,7 @@ from ..common import NotifyFormat from ..common import NotifyImageSize from ..utils import parse_list from ..utils import parse_bool +from ..utils import validate_regex from ..AppriseLocale import gettext_lazy as _ @@ -56,12 +57,8 @@ FLOCK_HTTP_ERROR_MAP = { } # Used to detect a channel/user -IS_CHANNEL_RE = re.compile(r'^(#|g:)(?P[A-Z0-9_]{12})$', re.I) -IS_USER_RE = re.compile(r'^(@|u:)?(?P[A-Z0-9_]{12})$', re.I) - -# Token required as part of the API request -# /134b8gh0-eba0-4fa9-ab9c-257ced0e8221 -IS_API_TOKEN = re.compile(r'^[a-z0-9-]{24}$', re.I) +IS_CHANNEL_RE = re.compile(r'^(#|g:)(?P[A-Z0-9_]+)$', re.I) +IS_USER_RE = re.compile(r'^(@|u:)?(?P[A-Z0-9_]+)$', re.I) class NotifyFlock(NotifyBase): @@ -103,7 +100,7 @@ class NotifyFlock(NotifyBase): 'token': { 'name': _('Access Key'), 'type': 'string', - 'regex': (r'[a-z0-9-]{24}', 'i'), + 'regex': (r'^[a-z0-9-]{24}$', 'i'), 'private': True, 'required': True, }, @@ -115,14 +112,14 @@ class NotifyFlock(NotifyBase): 'name': _('To User ID'), 'type': 'string', 'prefix': '@', - 'regex': (r'[A-Z0-9_]{12}', 'i'), + 'regex': (r'^[A-Z0-9_]{12}$', 'i'), 'map_to': 'targets', }, 'to_channel': { 'name': _('To Channel ID'), 'type': 'string', 'prefix': '#', - 'regex': (r'[A-Z0-9_]{12}', 'i'), + 'regex': (r'^[A-Z0-9_]{12}$', 'i'), 'map_to': 'targets', }, 'targets': { @@ -153,15 +150,18 @@ class NotifyFlock(NotifyBase): # Build ourselves a target list self.targets = list() - # Initialize our token object - self.token = token.strip() - - if not IS_API_TOKEN.match(self.token): - msg = 'The Flock API Token specified ({}) is invalid.'.format( - self.token) + self.token = validate_regex( + token, *self.template_tokens['token']['regex']) + if not self.token: + msg = 'An invalid Flock Access Key ' \ + '({}) was specified.'.format(token) self.logger.warning(msg) raise TypeError(msg) + # Track whether or not we want to send an image with our notification + # or not. + self.include_image = include_image + # Track any issues has_error = False @@ -183,15 +183,13 @@ class NotifyFlock(NotifyBase): self.logger.warning( 'Ignoring invalid target ({}) specified.'.format(target)) - if has_error and len(self.targets) == 0: + if has_error and not self.targets: # We have a bot token and no target(s) to message - msg = 'No targets found with specified Flock Bot Token.' + msg = 'No Flock targets to notify.' self.logger.warning(msg) raise TypeError(msg) - # Track whether or not we want to send an image with our notification - # or not. - self.include_image = include_image + return def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs): """ diff --git a/apprise/plugins/NotifyGitter.py b/apprise/plugins/NotifyGitter.py index dc7ee49a..d3632b2f 100644 --- a/apprise/plugins/NotifyGitter.py +++ b/apprise/plugins/NotifyGitter.py @@ -50,14 +50,12 @@ from ..common import NotifyFormat from ..common import NotifyType from ..utils import parse_list from ..utils import parse_bool +from ..utils import validate_regex from ..AppriseLocale import gettext_lazy as _ # API Gitter URL GITTER_API_URL = 'https://api.gitter.im/v1' -# Used to validate your personal access token -VALIDATE_TOKEN = re.compile(r'^[a-z0-9]{40}$', re.I) - # Used to break path apart into list of targets TARGET_LIST_DELIM = re.compile(r'[ \t\r\n,\\/]+') @@ -112,9 +110,9 @@ class NotifyGitter(NotifyBase): 'token': { 'name': _('Token'), 'type': 'string', - 'regex': (r'[a-z0-9]{40}', 'i'), 'private': True, 'required': True, + 'regex': (r'^[a-z0-9]{40}$', 'i'), }, 'targets': { 'name': _('Rooms'), @@ -141,24 +139,21 @@ class NotifyGitter(NotifyBase): """ super(NotifyGitter, self).__init__(**kwargs) - try: - # The personal access token associated with the account - self.token = token.strip() - - except AttributeError: - # Token was None - msg = 'No API Token was specified.' - self.logger.warning(msg) - raise TypeError(msg) - - if not VALIDATE_TOKEN.match(self.token): - msg = 'The Personal Access Token specified ({}) is invalid.' \ - .format(token) + # Secret Key (associated with project) + self.token = validate_regex( + token, *self.template_tokens['token']['regex']) + if not self.token: + msg = 'An invalid Gitter API Token ' \ + '({}) was specified.'.format(token) self.logger.warning(msg) raise TypeError(msg) # Parse our targets self.targets = parse_list(targets) + if not self.targets: + msg = 'There are no valid Gitter targets to notify.' + self.logger.warning(msg) + raise TypeError(msg) # Used to track maping of rooms to their numeric id lookup for # messaging @@ -168,6 +163,8 @@ class NotifyGitter(NotifyBase): # or not. self.include_image = include_image + return + def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs): """ Perform Gitter Notification @@ -183,8 +180,6 @@ class NotifyGitter(NotifyBase): if image_url: body = '![alt]({})\n{}'.format(image_url, body) - # Create a copy of the targets list - targets = list(self.targets) if self._room_mapping is None: # Populate our room mapping self._room_mapping = {} @@ -225,10 +220,8 @@ class NotifyGitter(NotifyBase): 'uri': entry['uri'], } - if len(targets) == 0: - # No targets specified - return False - + # Create a copy of the targets list + targets = list(self.targets) while len(targets): target = targets.pop(0).lower() diff --git a/apprise/plugins/NotifyGnome.py b/apprise/plugins/NotifyGnome.py index 93205ad4..fcdd73d7 100644 --- a/apprise/plugins/NotifyGnome.py +++ b/apprise/plugins/NotifyGnome.py @@ -150,6 +150,8 @@ class NotifyGnome(NotifyBase): # or not. self.include_image = include_image + return + def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs): """ Perform Gnome Notification diff --git a/apprise/plugins/NotifyGotify.py b/apprise/plugins/NotifyGotify.py index 3c2e494c..954a0a86 100644 --- a/apprise/plugins/NotifyGotify.py +++ b/apprise/plugins/NotifyGotify.py @@ -31,12 +31,12 @@ # f2c2688f0b5e6a816bbcec768ca1c0de5af76b88/ADD_MESSAGE_EXAMPLES.md#python # API: https://gotify.net/docs/swagger-docs -import six import requests from json import dumps from .NotifyBase import NotifyBase from ..common import NotifyType +from ..utils import validate_regex from ..AppriseLocale import gettext_lazy as _ @@ -121,9 +121,12 @@ class NotifyGotify(NotifyBase): """ super(NotifyGotify, self).__init__(**kwargs) - if not isinstance(token, six.string_types): - msg = 'An invalid Gotify token was specified.' - self.logger.warning('msg') + # Token (associated with project) + self.token = validate_regex(token) + if not self.token: + msg = 'An invalid Gotify Token ' \ + '({}) was specified.'.format(token) + self.logger.warning(msg) raise TypeError(msg) if priority not in GOTIFY_PRIORITIES: @@ -138,11 +141,6 @@ class NotifyGotify(NotifyBase): else: self.schema = 'http' - # Our access token does not get created until we first - # authenticate with our Gotify server. The same goes for the - # user id below. - self.token = token - return def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs): diff --git a/apprise/plugins/NotifyGrowl/__init__.py b/apprise/plugins/NotifyGrowl/__init__.py index 239b8835..5fa36795 100644 --- a/apprise/plugins/NotifyGrowl/__init__.py +++ b/apprise/plugins/NotifyGrowl/__init__.py @@ -324,7 +324,6 @@ class NotifyGrowl(NotifyBase): # We're done early as we couldn't load the results return results - # Apply our settings now version = None if 'version' in results['qsd'] and len(results['qsd']['version']): # Allow the user to specify the version of the protocol to use. diff --git a/apprise/plugins/NotifyIFTTT.py b/apprise/plugins/NotifyIFTTT.py index d557be7b..120ad610 100644 --- a/apprise/plugins/NotifyIFTTT.py +++ b/apprise/plugins/NotifyIFTTT.py @@ -46,6 +46,7 @@ from json import dumps from .NotifyBase import NotifyBase from ..common import NotifyType from ..utils import parse_list +from ..utils import validate_regex from ..AppriseLocale import gettext_lazy as _ @@ -148,22 +149,21 @@ class NotifyIFTTT(NotifyBase): """ super(NotifyIFTTT, self).__init__(**kwargs) - if not webhook_id: - msg = 'You must specify the Webhooks webhook_id.' + # Webhook ID (associated with project) + self.webhook_id = validate_regex(webhook_id) + if not self.webhook_id: + msg = 'An invalid IFTTT Webhook ID ' \ + '({}) was specified.'.format(webhook_id) self.logger.warning(msg) raise TypeError(msg) # Store our Events we wish to trigger self.events = parse_list(events) - if not self.events: msg = 'You must specify at least one event you wish to trigger on.' self.logger.warning(msg) raise TypeError(msg) - # Store our APIKey - self.webhook_id = webhook_id - # Tokens to include in post self.add_tokens = {} if add_tokens: diff --git a/apprise/plugins/NotifyJoin.py b/apprise/plugins/NotifyJoin.py index 88fcc679..76011d98 100644 --- a/apprise/plugins/NotifyJoin.py +++ b/apprise/plugins/NotifyJoin.py @@ -41,18 +41,16 @@ from ..common import NotifyImageSize from ..common import NotifyType from ..utils import parse_list from ..utils import parse_bool +from ..utils import validate_regex from ..AppriseLocale import gettext_lazy as _ -# Token required as part of the API request -VALIDATE_APIKEY = re.compile(r'[a-z0-9]{32}', re.I) - # Extend HTTP Error Messages JOIN_HTTP_ERROR_MAP = { 401: 'Unauthorized - Invalid Token.', } # Used to detect a device -IS_DEVICE_RE = re.compile(r'([a-z0-9]{32})', re.I) +IS_DEVICE_RE = re.compile(r'^[a-z0-9]{32}$', re.I) # Used to detect a device IS_GROUP_RE = re.compile( @@ -64,6 +62,24 @@ IS_GROUP_RE = re.compile( JOIN_IMAGE_XY = NotifyImageSize.XY_72 +# Priorities +class JoinPriority(object): + LOW = -2 + MODERATE = -1 + NORMAL = 0 + HIGH = 1 + EMERGENCY = 2 + + +JOIN_PRIORITIES = ( + JoinPriority.LOW, + JoinPriority.MODERATE, + JoinPriority.NORMAL, + JoinPriority.HIGH, + JoinPriority.EMERGENCY, +) + + class NotifyJoin(NotifyBase): """ A wrapper for Join Notifications @@ -104,14 +120,14 @@ class NotifyJoin(NotifyBase): 'apikey': { 'name': _('API Key'), 'type': 'string', - 'regex': (r'[a-z0-9]{32}', 'i'), + 'regex': (r'^[a-z0-9]{32}$', 'i'), 'private': True, 'required': True, }, 'device': { 'name': _('Device ID'), 'type': 'string', - 'regex': (r'[a-z0-9]{32}', 'i'), + 'regex': (r'^[a-z0-9]{32}$', 'i'), 'map_to': 'targets', }, 'group': { @@ -136,37 +152,79 @@ class NotifyJoin(NotifyBase): 'default': False, 'map_to': 'include_image', }, + 'priority': { + 'name': _('Priority'), + 'type': 'choice:int', + 'values': JOIN_PRIORITIES, + 'default': JoinPriority.NORMAL, + }, 'to': { 'alias_of': 'targets', }, }) - def __init__(self, apikey, targets, include_image=True, **kwargs): + def __init__(self, apikey, targets=None, include_image=True, priority=None, + **kwargs): """ Initialize Join Object """ super(NotifyJoin, self).__init__(**kwargs) - if not VALIDATE_APIKEY.match(apikey.strip()): - msg = 'The JOIN API Token specified ({}) is invalid.'\ - .format(apikey) - self.logger.warning(msg) - raise TypeError(msg) - - # The token associated with the account - self.apikey = apikey.strip() - - # Parse devices specified - self.devices = parse_list(targets) - - if len(self.devices) == 0: - # Default to everyone - self.devices.append(self.default_join_group) - # Track whether or not we want to send an image with our notification # or not. self.include_image = include_image + # API Key (associated with project) + self.apikey = validate_regex( + apikey, *self.template_tokens['apikey']['regex']) + if not self.apikey: + msg = 'An invalid Join API Key ' \ + '({}) was specified.'.format(apikey) + self.logger.warning(msg) + raise TypeError(msg) + + # The Priority of the message + if priority not in JOIN_PRIORITIES: + self.priority = self.template_args['priority']['default'] + + else: + self.priority = priority + + # Prepare a list of targets to store entries into + self.targets = list() + + # Prepare a parsed list of targets + targets = parse_list(targets) + if len(targets) == 0: + # Default to everyone if our list was empty + self.targets.append(self.default_join_group) + return + + # If we reach here we have some targets to parse + while len(targets): + # Parse our targets + target = targets.pop(0) + group_re = IS_GROUP_RE.match(target) + if group_re: + self.targets.append( + 'group.{}'.format(group_re.group('name').lower())) + continue + + elif IS_DEVICE_RE.match(target): + self.targets.append(target) + continue + + self.logger.warning( + 'Ignoring invalid Join device/group "{}"'.format(target) + ) + + if not self.targets: + msg = 'No Join targets to notify.' + self.logger.warning(msg) + raise TypeError(msg) + + return + def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs): """ Perform Join Notification @@ -180,26 +238,17 @@ class NotifyJoin(NotifyBase): # error tracking (used for function return) has_error = False - # Create a copy of the devices list - devices = list(self.devices) - while len(devices): - device = devices.pop(0) - group_re = IS_GROUP_RE.match(device) - if group_re: - device = 'group.{}'.format(group_re.group('name').lower()) + # Capture a list of our targets to notify + targets = list(self.targets) - elif not IS_DEVICE_RE.match(device): - self.logger.warning( - 'Skipping specified invalid device/group "{}"' - .format(device) - ) - # Mark our failure - has_error = True - continue + while len(targets): + # Pop the first element off of our list + target = targets.pop(0) url_args = { 'apikey': self.apikey, - 'deviceId': device, + 'deviceId': target, + 'priority': str(self.priority), 'title': title, 'text': body, } @@ -242,7 +291,7 @@ class NotifyJoin(NotifyBase): self.logger.warning( 'Failed to send Join notification to {}: ' '{}{}error={}.'.format( - device, + target, status_str, ', ' if status_str else '', r.status_code)) @@ -255,12 +304,12 @@ class NotifyJoin(NotifyBase): continue else: - self.logger.info('Sent Join notification to %s.' % device) + self.logger.info('Sent Join notification to %s.' % target) except requests.RequestException as e: self.logger.warning( 'A Connection error occured sending Join:%s ' - 'notification.' % device + 'notification.' % target ) self.logger.debug('Socket Exception: %s' % str(e)) @@ -274,20 +323,30 @@ class NotifyJoin(NotifyBase): """ Returns the URL built dynamically based on specified arguments. """ + _map = { + JoinPriority.LOW: 'low', + JoinPriority.MODERATE: 'moderate', + JoinPriority.NORMAL: 'normal', + JoinPriority.HIGH: 'high', + JoinPriority.EMERGENCY: 'emergency', + } # Define any arguments set args = { 'format': self.notify_format, 'overflow': self.overflow_mode, + 'priority': + _map[self.template_args['priority']['default']] + if self.priority not in _map else _map[self.priority], 'image': 'yes' if self.include_image else 'no', 'verify': 'yes' if self.verify_certificate else 'no', } - return '{schema}://{apikey}/{devices}/?{args}'.format( + return '{schema}://{apikey}/{targets}/?{args}'.format( schema=self.secure_protocol, apikey=self.pprint(self.apikey, privacy, safe=''), - devices='/'.join([NotifyJoin.quote(x, safe='') - for x in self.devices]), + targets='/'.join([NotifyJoin.quote(x, safe='') + for x in self.targets]), args=NotifyJoin.urlencode(args)) @staticmethod @@ -310,6 +369,23 @@ class NotifyJoin(NotifyBase): # Unquote our API Key results['apikey'] = NotifyJoin.unquote(results['apikey']) + # Set our priority + if 'priority' in results['qsd'] and len(results['qsd']['priority']): + _map = { + 'l': JoinPriority.LOW, + 'm': JoinPriority.MODERATE, + 'n': JoinPriority.NORMAL, + 'h': JoinPriority.HIGH, + 'e': JoinPriority.EMERGENCY, + } + try: + results['priority'] = \ + _map[results['qsd']['priority'][0].lower()] + + except KeyError: + # No priority was set + pass + # Our Devices results['targets'] = list() if results['user']: diff --git a/apprise/plugins/NotifyKumulos.py b/apprise/plugins/NotifyKumulos.py index 723c1674..4833045f 100644 --- a/apprise/plugins/NotifyKumulos.py +++ b/apprise/plugins/NotifyKumulos.py @@ -33,27 +33,14 @@ # The API reference used to build this plugin was documented here: # https://docs.kumulos.com/messaging/api/#sending-in-app-messages # -import re import requests from json import dumps from .NotifyBase import NotifyBase from ..common import NotifyType +from ..utils import validate_regex from ..AppriseLocale import gettext_lazy as _ -# -# API Key is a UUID; below is the regex matching -UUID4_RE = \ - r'[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}' - -# Secret Key Regex Mapping -SERVER_KEY_RE = r'[A-Z0-9+]{36}' - -# API Key -VALIDATE_APIKEY = re.compile(UUID4_RE, re.I) - -VALIDATE_SERVER_KEY = re.compile(SERVER_KEY_RE, re.I) - # Extend HTTP Error Messages KUMULOS_HTTP_ERROR_MAP = { 401: 'Unauthorized - Invalid API and/or Server Key.', @@ -61,9 +48,6 @@ KUMULOS_HTTP_ERROR_MAP = { 400: 'Bad Request - Targeted users do not exist or have unsubscribed.', } -# Used to break path apart into list of channels -TARGET_LIST_DELIM = re.compile(r'[ \t\r\n,#\\/]+') - class NotifyKumulos(NotifyBase): """ @@ -103,14 +87,16 @@ class NotifyKumulos(NotifyBase): 'type': 'string', 'private': True, 'required': True, - 'regex': (UUID4_RE, 'i'), + # UUID4 + 'regex': (r'^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-' + r'[89ab][0-9a-f]{3}-[0-9a-f]{12}$', 'i') }, 'serverkey': { 'name': _('Server Key'), 'type': 'string', 'private': True, 'required': True, - 'regex': (SERVER_KEY_RE, 'i'), + 'regex': (r'^[A-Z0-9+]{36}$', 'i'), }, }) @@ -120,27 +106,21 @@ class NotifyKumulos(NotifyBase): """ super(NotifyKumulos, self).__init__(**kwargs) - if not apikey: - msg = 'The Kumulos API Key is not specified.' + # API Key (associated with project) + self.apikey = validate_regex( + apikey, *self.template_tokens['apikey']['regex']) + if not self.apikey: + msg = 'An invalid Kumulos API Key ' \ + '({}) was specified.'.format(apikey) self.logger.warning(msg) raise TypeError(msg) - self.apikey = apikey.strip() - if not VALIDATE_APIKEY.match(self.apikey): - msg = 'The Kumulos API Key specified ({}) is invalid.'\ - .format(apikey) - self.logger.warning(msg) - raise TypeError(msg) - - if not serverkey: - msg = 'The Kumulos Server Key is not specified.' - self.logger.warning(msg) - raise TypeError(msg) - - self.serverkey = serverkey.strip() - if not VALIDATE_SERVER_KEY.match(self.serverkey): - msg = 'The Kumulos Server Key specified ({}) is invalid.'\ - .format(serverkey) + # Server Key (associated with project) + self.serverkey = validate_regex( + serverkey, *self.template_tokens['serverkey']['regex']) + if not self.serverkey: + msg = 'An invalid Kumulos Server Key ' \ + '({}) was specified.'.format(serverkey) self.logger.warning(msg) raise TypeError(msg) diff --git a/apprise/plugins/NotifyMSG91.py b/apprise/plugins/NotifyMSG91.py index a2e6dc79..1425b8a7 100644 --- a/apprise/plugins/NotifyMSG91.py +++ b/apprise/plugins/NotifyMSG91.py @@ -37,11 +37,9 @@ import requests from .NotifyBase import NotifyBase from ..common import NotifyType from ..utils import parse_list +from ..utils import validate_regex from ..AppriseLocale import gettext_lazy as _ -# Token required as part of the API request -VALIDATE_AUTHKEY = re.compile(r'^[a-z0-9]+$', re.I) - # Some Phone Number Detection IS_PHONE_NO = re.compile(r'^\+?(?P[0-9\s)(+-]+)\s*$') @@ -118,13 +116,14 @@ class NotifyMSG91(NotifyBase): 'name': _('Authentication Key'), 'type': 'string', 'required': True, - 'regex': (r'[a-z0-9]+', 'i'), + 'private': True, + 'regex': (r'^[a-z0-9]+$', 'i'), }, 'target_phone': { 'name': _('Target Phone No'), 'type': 'string', 'prefix': '+', - 'regex': (r'[0-9\s)(+-]+', 'i'), + 'regex': (r'^[0-9\s)(+-]+$', 'i'), 'map_to': 'targets', }, 'targets': { @@ -162,19 +161,12 @@ class NotifyMSG91(NotifyBase): """ super(NotifyMSG91, self).__init__(**kwargs) - try: - # The authentication key associated with the account - self.authkey = authkey.strip() - - except AttributeError: - # Token was None - msg = 'No MSG91 authentication key was specified.' - self.logger.warning(msg) - raise TypeError(msg) - - if not VALIDATE_AUTHKEY.match(self.authkey): - msg = 'The MSG91 authentication key specified ({}) is invalid.'\ - .format(self.authkey) + # Authentication Key (associated with project) + self.authkey = validate_regex( + authkey, *self.template_tokens['authkey']['regex']) + if not self.authkey: + msg = 'An invalid MSG91 Authentication Key ' \ + '({}) was specified.'.format(authkey) self.logger.warning(msg) raise TypeError(msg) @@ -237,16 +229,19 @@ class NotifyMSG91(NotifyBase): '({}) specified.'.format(target), ) + if not self.targets: + # We have a bot token and no target(s) to message + msg = 'No MSG91 targets to notify.' + self.logger.warning(msg) + raise TypeError(msg) + + return + def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs): """ Perform MSG91 Notification """ - if not len(self.targets): - # There were no services to notify - self.logger.warning('There were no MSG91 targets to notify') - return False - # Prepare our headers headers = { 'User-Agent': self.app_id, diff --git a/apprise/plugins/NotifyMSTeams.py b/apprise/plugins/NotifyMSTeams.py index 64b5fd65..69430866 100644 --- a/apprise/plugins/NotifyMSTeams.py +++ b/apprise/plugins/NotifyMSTeams.py @@ -69,24 +69,13 @@ from ..common import NotifyImageSize from ..common import NotifyType from ..common import NotifyFormat from ..utils import parse_bool +from ..utils import validate_regex from ..AppriseLocale import gettext_lazy as _ # Used to prepare our UUID regex matching UUID4_RE = \ r'[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}' -# Token required as part of the API request -# /AAAAAAAAA@AAAAAAAAA/........./......... -VALIDATE_TOKEN_A = re.compile(r'{}@{}'.format(UUID4_RE, UUID4_RE), re.I) - -# Token required as part of the API request -# /................../BBBBBBBBB/.......... -VALIDATE_TOKEN_B = re.compile(r'[A-Za-z0-9]{32}') - -# Token required as part of the API request -# /........./........./CCCCCCCCCCCCCCCCCCCCCCCC -VALIDATE_TOKEN_C = re.compile(UUID4_RE, re.I) - class NotifyMSTeams(NotifyBase): """ @@ -124,26 +113,32 @@ class NotifyMSTeams(NotifyBase): # Define our template tokens template_tokens = dict(NotifyBase.template_tokens, **{ + # Token required as part of the API request + # /AAAAAAAAA@AAAAAAAAA/........./......... 'token_a': { 'name': _('Token A'), 'type': 'string', 'private': True, 'required': True, - 'regex': (r'{}@{}'.format(UUID4_RE, UUID4_RE), 'i'), + 'regex': (r'^{}@{}$'.format(UUID4_RE, UUID4_RE), 'i'), }, + # Token required as part of the API request + # /................../BBBBBBBBB/.......... 'token_b': { 'name': _('Token B'), 'type': 'string', 'private': True, 'required': True, - 'regex': (r'[a-z0-9]{32}', 'i'), + 'regex': (r'^[A-Za-z0-9]{32}$', 'i'), }, + # Token required as part of the API request + # /........./........./CCCCCCCCCCCCCCCCCCCCCCCC 'token_c': { 'name': _('Token C'), 'type': 'string', 'private': True, 'required': True, - 'regex': (UUID4_RE, 'i'), + 'regex': (r'^{}$'.format(UUID4_RE), 'i'), }, }) @@ -164,51 +159,35 @@ class NotifyMSTeams(NotifyBase): """ super(NotifyMSTeams, self).__init__(**kwargs) - if not token_a: - msg = 'The first MSTeams API token is not specified.' + self.token_a = validate_regex( + token_a, *self.template_tokens['token_a']['regex']) + if not self.token_a: + msg = 'An invalid MSTeams (first) Token ' \ + '({}) was specified.'.format(token_a) self.logger.warning(msg) raise TypeError(msg) - if not token_b: - msg = 'The second MSTeams API token is not specified.' + self.token_b = validate_regex( + token_b, *self.template_tokens['token_b']['regex']) + if not self.token_b: + msg = 'An invalid MSTeams (second) Token ' \ + '({}) was specified.'.format(token_b) self.logger.warning(msg) raise TypeError(msg) - if not token_c: - msg = 'The third MSTeams API token is not specified.' + self.token_c = validate_regex( + token_c, *self.template_tokens['token_c']['regex']) + if not self.token_c: + msg = 'An invalid MSTeams (third) Token ' \ + '({}) was specified.'.format(token_c) self.logger.warning(msg) raise TypeError(msg) - if not VALIDATE_TOKEN_A.match(token_a.strip()): - msg = 'The first MSTeams API token specified ({}) is invalid.'\ - .format(token_a) - self.logger.warning(msg) - raise TypeError(msg) - - # The token associated with the account - self.token_a = token_a.strip() - - if not VALIDATE_TOKEN_B.match(token_b.strip()): - msg = 'The second MSTeams API token specified ({}) is invalid.'\ - .format(token_b) - self.logger.warning(msg) - raise TypeError(msg) - - # The token associated with the account - self.token_b = token_b.strip() - - if not VALIDATE_TOKEN_C.match(token_c.strip()): - msg = 'The third MSTeams API token specified ({}) is invalid.'\ - .format(token_c) - self.logger.warning(msg) - raise TypeError(msg) - - # The token associated with the account - self.token_c = token_c.strip() - # Place a thumbnail image inline with the message body self.include_image = include_image + return + def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs): """ Perform Microsoft Teams Notification diff --git a/apprise/plugins/NotifyMailgun.py b/apprise/plugins/NotifyMailgun.py index e315c624..6e2a3b28 100644 --- a/apprise/plugins/NotifyMailgun.py +++ b/apprise/plugins/NotifyMailgun.py @@ -57,6 +57,7 @@ from .NotifyBase import NotifyBase from ..common import NotifyType from ..utils import parse_list from ..utils import is_email +from ..utils import validate_regex from ..AppriseLocale import gettext_lazy as _ # Provide some known codes Mailgun uses and what they translate to: @@ -169,19 +170,17 @@ class NotifyMailgun(NotifyBase): """ super(NotifyMailgun, self).__init__(**kwargs) - try: - # The personal access apikey associated with the account - self.apikey = apikey.strip() - - except AttributeError: - # Token was None - msg = 'No API Key was specified.' + # API Key (associated with project) + self.apikey = validate_regex(apikey) + if not self.apikey: + msg = 'An invalid Mailgun API Key ' \ + '({}) was specified.'.format(apikey) self.logger.warning(msg) raise TypeError(msg) # Validate our username if not self.user: - msg = 'No username was specified.' + msg = 'No Mailgun username was specified.' self.logger.warning(msg) raise TypeError(msg) @@ -198,7 +197,7 @@ class NotifyMailgun(NotifyBase): raise except: # Invalid region specified - msg = 'The region specified ({}) is invalid.' \ + msg = 'The Mailgun region specified ({}) is invalid.' \ .format(region_name) self.logger.warning(msg) raise TypeError(msg) diff --git a/apprise/plugins/NotifyMatterMost.py b/apprise/plugins/NotifyMatterMost.py index 91721149..84bb93ed 100644 --- a/apprise/plugins/NotifyMatterMost.py +++ b/apprise/plugins/NotifyMatterMost.py @@ -23,7 +23,6 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. -import re import six import requests from json import dumps @@ -33,15 +32,13 @@ from ..common import NotifyImageSize from ..common import NotifyType from ..utils import parse_bool from ..utils import parse_list +from ..utils import validate_regex from ..AppriseLocale import gettext_lazy as _ # Some Reference Locations: # - https://docs.mattermost.com/developer/webhooks-incoming.html # - https://docs.mattermost.com/administration/config-settings.html -# Used to validate Authorization Token -VALIDATE_AUTHTOKEN = re.compile(r'[a-z0-9]{24,32}', re.I) - class NotifyMatterMost(NotifyBase): """ @@ -97,7 +94,7 @@ class NotifyMatterMost(NotifyBase): 'authtoken': { 'name': _('Access Key'), 'type': 'string', - 'regex': (r'[a-z0-9]{24,32}', 'i'), + 'regex': (r'^[a-z0-9]{24,32}$', 'i'), 'private': True, 'required': True, }, @@ -152,17 +149,12 @@ class NotifyMatterMost(NotifyBase): self.fullpath = '' if not isinstance( fullpath, six.string_types) else fullpath.strip() - # Our Authorization Token - self.authtoken = authtoken - - # Validate authtoken - if not authtoken: - msg = 'Missing MatterMost Authorization Token.' - self.logger.warning(msg) - raise TypeError(msg) - - if not VALIDATE_AUTHTOKEN.match(authtoken): - msg = 'Invalid MatterMost Authorization Token Specified.' + # Authorization Token (associated with project) + self.authtoken = validate_regex( + authtoken, *self.template_tokens['authtoken']['regex']) + if not self.authtoken: + msg = 'An invalid MatterMost Authorization Token ' \ + '({}) was specified.'.format(authtoken) self.logger.warning(msg) raise TypeError(msg) @@ -340,7 +332,6 @@ class NotifyMatterMost(NotifyBase): # all entries before it will be our path tokens = NotifyMatterMost.split_path(results['fullpath']) - # Apply our settings now results['authtoken'] = None if not tokens else tokens.pop() # Store our path diff --git a/apprise/plugins/NotifyMessageBird.py b/apprise/plugins/NotifyMessageBird.py index 3de85652..b593bc21 100644 --- a/apprise/plugins/NotifyMessageBird.py +++ b/apprise/plugins/NotifyMessageBird.py @@ -35,11 +35,9 @@ import requests from .NotifyBase import NotifyBase from ..common import NotifyType from ..utils import parse_list +from ..utils import validate_regex from ..AppriseLocale import gettext_lazy as _ -# Token required as part of the API request -VALIDATE_APIKEY = re.compile(r'^[a-z0-9]{25}$', re.I) - # Some Phone Number Detection IS_PHONE_NO = re.compile(r'^\+?(?P[0-9\s)(+-]+)\s*$') @@ -83,20 +81,21 @@ class NotifyMessageBird(NotifyBase): 'name': _('API Key'), 'type': 'string', 'required': True, - 'regex': (r'[a-z0-9]{25}', 'i'), + 'private': True, + 'regex': (r'^[a-z0-9]{25}$', 'i'), }, 'source': { 'name': _('Source Phone No'), 'type': 'string', 'prefix': '+', 'required': True, - 'regex': (r'[0-9\s)(+-]+', 'i'), + 'regex': (r'^[0-9\s)(+-]+$', 'i'), }, 'target_phone': { 'name': _('Target Phone No'), 'type': 'string', 'prefix': '+', - 'regex': (r'[0-9\s)(+-]+', 'i'), + 'regex': (r'^[0-9\s)(+-]+$', 'i'), 'map_to': 'targets', }, 'targets': { @@ -121,19 +120,12 @@ class NotifyMessageBird(NotifyBase): """ super(NotifyMessageBird, self).__init__(**kwargs) - try: - # The authentication key associated with the account - self.apikey = apikey.strip() - - except AttributeError: - # Token was None - msg = 'No MessageBird authentication key was specified.' - self.logger.warning(msg) - raise TypeError(msg) - - if not VALIDATE_APIKEY.match(self.apikey): - msg = 'The MessageBird authentication key specified ({}) is ' \ - 'invalid.'.format(self.apikey) + # API Key (associated with project) + self.apikey = validate_regex( + apikey, *self.template_tokens['apikey']['regex']) + if not self.apikey: + msg = 'An invalid MessageBird API Key ' \ + '({}) was specified.'.format(apikey) self.logger.warning(msg) raise TypeError(msg) @@ -158,7 +150,14 @@ class NotifyMessageBird(NotifyBase): # Parse our targets self.targets = list() - for target in parse_list(targets): + targets = parse_list(targets) + if not targets: + # No sources specified, use our own phone no + self.targets.append(self.source) + return + + # otherwise, store all of our target numbers + for target in targets: # Validate targets and drop bad ones: result = IS_PHONE_NO.match(target) if result: @@ -180,6 +179,14 @@ class NotifyMessageBird(NotifyBase): '({}) specified.'.format(target), ) + if not self.targets: + # We have a bot token and no target(s) to message + msg = 'No MessageBird targets to notify.' + self.logger.warning(msg) + raise TypeError(msg) + + return + def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs): """ Perform MessageBird Notification @@ -202,13 +209,10 @@ class NotifyMessageBird(NotifyBase): 'body': body, } + # Create a copy of the targets list targets = list(self.targets) - if len(targets) == 0: - # No sources specified, use our own phone no - targets.append(self.source) - while len(targets): # Get our target to notify target = targets.pop(0) diff --git a/apprise/plugins/NotifyNexmo.py b/apprise/plugins/NotifyNexmo.py index f400f2c4..db19c759 100644 --- a/apprise/plugins/NotifyNexmo.py +++ b/apprise/plugins/NotifyNexmo.py @@ -36,12 +36,9 @@ from .NotifyBase import NotifyBase from ..URLBase import PrivacyMode from ..common import NotifyType from ..utils import parse_list +from ..utils import validate_regex from ..AppriseLocale import gettext_lazy as _ -# Token required as part of the API request -VALIDATE_APIKEY = re.compile(r'^[a-z0-9]{8}$', re.I) -VALIDATE_SECRET = re.compile(r'^[a-z0-9]{16}$', re.I) - # Some Phone Number Detection IS_PHONE_NO = re.compile(r'^\+?(?P[0-9\s)(+-]+)\s*$') @@ -94,27 +91,28 @@ class NotifyNexmo(NotifyBase): 'name': _('API Key'), 'type': 'string', 'required': True, - 'regex': (r'AC[a-z0-9]{8}', 'i'), + 'regex': (r'^AC[a-z0-9]{8}$', 'i'), + 'private': True, }, 'secret': { 'name': _('API Secret'), 'type': 'string', 'private': True, 'required': True, - 'regex': (r'[a-z0-9]{16}', 'i'), + 'regex': (r'^[a-z0-9]{16}$', 'i'), }, 'from_phone': { 'name': _('From Phone No'), 'type': 'string', 'required': True, - 'regex': (r'\+?[0-9\s)(+-]+', 'i'), + 'regex': (r'^\+?[0-9\s)(+-]+$', 'i'), 'map_to': 'source', }, 'target_phone': { 'name': _('Target Phone No'), 'type': 'string', 'prefix': '+', - 'regex': (r'[0-9\s)(+-]+', 'i'), + 'regex': (r'^[0-9\s)(+-]+$', 'i'), 'map_to': 'targets', }, 'targets': { @@ -153,35 +151,21 @@ class NotifyNexmo(NotifyBase): """ super(NotifyNexmo, self).__init__(**kwargs) - try: - # The Account SID associated with the account - self.apikey = apikey.strip() - - except AttributeError: - # Token was None - msg = 'No Nexmo APIKey was specified.' + # API Key (associated with project) + self.apikey = validate_regex( + apikey, *self.template_tokens['apikey']['regex']) + if not self.apikey: + msg = 'An invalid Nexmo API Key ' \ + '({}) was specified.'.format(apikey) self.logger.warning(msg) raise TypeError(msg) - if not VALIDATE_APIKEY.match(self.apikey): - msg = 'The Nexmo API Key specified ({}) is invalid.'\ - .format(self.apikey) - self.logger.warning(msg) - raise TypeError(msg) - - try: - # The Account SID associated with the account - self.secret = secret.strip() - - except AttributeError: - # Token was None - msg = 'No Nexmo API Secret was specified.' - self.logger.warning(msg) - raise TypeError(msg) - - if not VALIDATE_SECRET.match(self.secret): - msg = 'The Nexmo API Secret specified ({}) is invalid.'\ - .format(self.secret) + # API Secret (associated with project) + self.secret = validate_regex( + secret, *self.template_tokens['secret']['regex']) + if not self.secret: + msg = 'An invalid Nexmo API Secret ' \ + '({}) was specified.'.format(secret) self.logger.warning(msg) raise TypeError(msg) @@ -242,6 +226,8 @@ class NotifyNexmo(NotifyBase): '({}) specified.'.format(target), ) + return + def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs): """ Perform Nexmo Notification diff --git a/apprise/plugins/NotifyProwl.py b/apprise/plugins/NotifyProwl.py index bb56787b..3f6ca792 100644 --- a/apprise/plugins/NotifyProwl.py +++ b/apprise/plugins/NotifyProwl.py @@ -23,19 +23,13 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. -import re import requests from .NotifyBase import NotifyBase from ..common import NotifyType +from ..utils import validate_regex from ..AppriseLocale import gettext_lazy as _ -# Used to validate API Key -VALIDATE_APIKEY = re.compile(r'[A-Za-z0-9]{40}') - -# Used to validate Provider Key -VALIDATE_PROVIDERKEY = re.compile(r'[A-Za-z0-9]{40}') - # Priorities class ProwlPriority(object): @@ -104,11 +98,13 @@ class NotifyProwl(NotifyBase): 'type': 'string', 'private': True, 'required': True, + 'regex': (r'^[A-Za-z0-9]{40}$', 'i'), }, 'providerkey': { 'name': _('Provider Key'), 'type': 'string', 'private': True, + 'regex': (r'^[A-Za-z0-9]{40}$', 'i'), }, }) @@ -129,31 +125,35 @@ class NotifyProwl(NotifyBase): super(NotifyProwl, self).__init__(**kwargs) if priority not in PROWL_PRIORITIES: - self.priority = ProwlPriority.NORMAL + self.priority = self.template_args['priority']['default'] else: self.priority = priority - if not VALIDATE_APIKEY.match(apikey): - msg = 'The API key specified ({}) is invalid.'.format(apikey) + # API Key (associated with project) + self.apikey = validate_regex( + apikey, *self.template_tokens['apikey']['regex']) + if not self.apikey: + msg = 'An invalid Prowl API Key ' \ + '({}) was specified.'.format(apikey) self.logger.warning(msg) raise TypeError(msg) - # Store the API key - self.apikey = apikey - # Store the provider key (if specified) if providerkey: - if not VALIDATE_PROVIDERKEY.match(providerkey): - msg = \ - 'The Provider key specified ({}) is invalid.' \ - .format(providerkey) - + self.providerkey = validate_regex( + providerkey, *self.template_tokens['providerkey']['regex']) + if not self.providerkey: + msg = 'An invalid Prowl Provider Key ' \ + '({}) was specified.'.format(providerkey) self.logger.warning(msg) raise TypeError(msg) - # Store the Provider Key - self.providerkey = providerkey + else: + # No provider key was set + self.providerkey = None + + return def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs): """ diff --git a/apprise/plugins/NotifyPushBullet.py b/apprise/plugins/NotifyPushBullet.py index 1bed494b..05defa00 100644 --- a/apprise/plugins/NotifyPushBullet.py +++ b/apprise/plugins/NotifyPushBullet.py @@ -30,6 +30,7 @@ from .NotifyBase import NotifyBase from ..utils import GET_EMAIL_RE from ..common import NotifyType from ..utils import parse_list +from ..utils import validate_regex from ..AppriseLocale import gettext_lazy as _ # Flag used as a placeholder to sending to all devices @@ -110,12 +111,20 @@ class NotifyPushBullet(NotifyBase): """ super(NotifyPushBullet, self).__init__(**kwargs) - self.accesstoken = accesstoken + # Access Token (associated with project) + self.accesstoken = validate_regex(accesstoken) + if not self.accesstoken: + msg = 'An invalid PushBullet Access Token ' \ + '({}) was specified.'.format(accesstoken) + self.logger.warning(msg) + raise TypeError(msg) self.targets = parse_list(targets) if len(self.targets) == 0: self.targets = (PUSHBULLET_SEND_TO_ALL, ) + return + def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs): """ Perform PushBullet Notification diff --git a/apprise/plugins/NotifyPushed.py b/apprise/plugins/NotifyPushed.py index 318443bd..35e390d7 100644 --- a/apprise/plugins/NotifyPushed.py +++ b/apprise/plugins/NotifyPushed.py @@ -32,10 +32,11 @@ from .NotifyBase import NotifyBase from ..URLBase import PrivacyMode from ..common import NotifyType from ..utils import parse_list +from ..utils import validate_regex from ..AppriseLocale import gettext_lazy as _ # Used to detect and parse channels -IS_CHANNEL = re.compile(r'^#(?P[A-Za-z0-9]+)$') +IS_CHANNEL = re.compile(r'^#?(?P[A-Za-z0-9]+)$') # Used to detect and parse a users push id IS_USER_PUSHED_ID = re.compile(r'^@(?P[A-Za-z0-9]+)$') @@ -121,13 +122,19 @@ class NotifyPushed(NotifyBase): """ super(NotifyPushed, self).__init__(**kwargs) - if not app_key: - msg = 'An invalid Application Key was specified.' + # Application Key (associated with project) + self.app_key = validate_regex(app_key) + if not self.app_key: + msg = 'An invalid Pushed Application Key ' \ + '({}) was specified.'.format(app_key) self.logger.warning(msg) raise TypeError(msg) - if not app_secret: - msg = 'An invalid Application Secret was specified.' + # Access Secret (associated with project) + self.app_secret = validate_regex(app_secret) + if not self.app_secret: + msg = 'An invalid Pushed Application Secret ' \ + '({}) was specified.'.format(app_secret) self.logger.warning(msg) raise TypeError(msg) @@ -137,28 +144,34 @@ class NotifyPushed(NotifyBase): # Initialize user list self.users = list() - # Validate recipients and drop bad ones: - for target in parse_list(targets): - result = IS_CHANNEL.match(target) - if result: - # store valid device - self.channels.append(result.group('name')) - continue + # Get our targets + targets = parse_list(targets) + if targets: + # Validate recipients and drop bad ones: + for target in targets: + result = IS_CHANNEL.match(target) + if result: + # store valid device + self.channels.append(result.group('name')) + continue - result = IS_USER_PUSHED_ID.match(target) - if result: - # store valid room - self.users.append(result.group('name')) - continue + result = IS_USER_PUSHED_ID.match(target) + if result: + # store valid room + self.users.append(result.group('name')) + continue - self.logger.warning( - 'Dropped invalid channel/userid ' - '(%s) specified.' % target, - ) + self.logger.warning( + 'Dropped invalid channel/userid ' + '(%s) specified.' % target, + ) - # Store our data - self.app_key = app_key - self.app_secret = app_secret + if len(self.channels) + len(self.users) == 0: + # We have no valid channels or users to notify after + # explicitly identifying at least one. + msg = 'No Pushed targets to notify.' + self.logger.warning(msg) + raise TypeError(msg) return @@ -325,8 +338,6 @@ class NotifyPushed(NotifyBase): # We're done early as we couldn't load the results return results - # Apply our settings now - # The first token is stored in the hostname app_key = NotifyPushed.unquote(results['host']) diff --git a/apprise/plugins/NotifyPushjet.py b/apprise/plugins/NotifyPushjet.py index 74d5a58f..0dcb596d 100644 --- a/apprise/plugins/NotifyPushjet.py +++ b/apprise/plugins/NotifyPushjet.py @@ -29,6 +29,7 @@ from json import dumps from .NotifyBase import NotifyBase from ..URLBase import PrivacyMode from ..common import NotifyType +from ..utils import validate_regex from ..AppriseLocale import gettext_lazy as _ @@ -107,14 +108,15 @@ class NotifyPushjet(NotifyBase): """ super(NotifyPushjet, self).__init__(**kwargs) - if not secret_key: - # You must provide a Pushjet key to work with - msg = 'You must specify a Pushjet Secret Key.' + # Secret Key (associated with project) + self.secret_key = validate_regex(secret_key) + if not self.secret_key: + msg = 'An invalid Pushjet Secret Key ' \ + '({}) was specified.'.format(secret_key) self.logger.warning(msg) raise TypeError(msg) - # store our key - self.secret_key = secret_key + return def url(self, privacy=False, *args, **kwargs): """ @@ -125,9 +127,6 @@ class NotifyPushjet(NotifyBase): args = { 'format': self.notify_format, 'overflow': self.overflow_mode, - 'secret': self.pprint( - self.secret_key, privacy, - mode=PrivacyMode.Secret, quote=False), 'verify': 'yes' if self.verify_certificate else 'no', } @@ -142,12 +141,14 @@ class NotifyPushjet(NotifyBase): self.password, privacy, mode=PrivacyMode.Secret, safe=''), ) - return '{schema}://{auth}{hostname}{port}/?{args}'.format( + return '{schema}://{auth}{hostname}{port}/{secret}/?{args}'.format( schema=self.secure_protocol if self.secure else self.protocol, auth=auth, hostname=NotifyPushjet.quote(self.host, safe=''), port='' if self.port is None or self.port == default_port else ':{}'.format(self.port), + secret=self.pprint( + self.secret_key, privacy, mode=PrivacyMode.Secret, safe=''), args=NotifyPushjet.urlencode(args), ) @@ -273,7 +274,7 @@ class NotifyPushjet(NotifyBase): # through it in addition to supporting the secret key if 'secret' in results['qsd'] and len(results['qsd']['secret']): results['secret_key'] = \ - NotifyPushjet.parse_list(results['qsd']['secret']) + NotifyPushjet.unquote(results['qsd']['secret']) if results.get('secret_key') is None: # Deprication Notice issued for v0.7.9 diff --git a/apprise/plugins/NotifyPushover.py b/apprise/plugins/NotifyPushover.py index df0b7ef4..58fb63cb 100644 --- a/apprise/plugins/NotifyPushover.py +++ b/apprise/plugins/NotifyPushover.py @@ -30,18 +30,13 @@ import requests from .NotifyBase import NotifyBase from ..common import NotifyType from ..utils import parse_list +from ..utils import validate_regex from ..AppriseLocale import gettext_lazy as _ # Flag used as a placeholder to sending to all devices PUSHOVER_SEND_TO_ALL = 'ALL_DEVICES' -# Used to validate API Key -VALIDATE_TOKEN = re.compile(r'^[a-z0-9]{30}$', re.I) - -# Used to detect a User and/or Group -VALIDATE_USER_KEY = re.compile(r'^[a-z0-9]{30}$', re.I) - -# Used to detect a User and/or Group +# Used to detect a Device VALIDATE_DEVICE = re.compile(r'^[a-z0-9_]{1,25}$', re.I) @@ -158,20 +153,19 @@ class NotifyPushover(NotifyBase): 'type': 'string', 'private': True, 'required': True, - 'regex': (r'[a-z0-9]{30}', 'i'), - 'map_to': 'user', + 'regex': (r'^[a-z0-9]{30}$', 'i'), }, 'token': { 'name': _('Access Token'), 'type': 'string', 'private': True, 'required': True, - 'regex': (r'[a-z0-9]{30}', 'i'), + 'regex': (r'^[a-z0-9]{30}$', 'i'), }, 'target_device': { 'name': _('Target Device'), 'type': 'string', - 'regex': (r'[a-z0-9_]{1,25}', 'i'), + 'regex': (r'^[a-z0-9_]{1,25}$', 'i'), 'map_to': 'targets', }, 'targets': { @@ -191,7 +185,7 @@ class NotifyPushover(NotifyBase): 'sound': { 'name': _('Sound'), 'type': 'string', - 'regex': (r'[a-z]{1,12}', 'i'), + 'regex': (r'^[a-z]{1,12}$', 'i'), 'default': PushoverSound.PUSHOVER, }, 'retry': { @@ -212,26 +206,28 @@ class NotifyPushover(NotifyBase): }, }) - def __init__(self, token, targets=None, priority=None, sound=None, - retry=None, expire=None, - **kwargs): + def __init__(self, user_key, token, targets=None, priority=None, + sound=None, retry=None, expire=None, **kwargs): """ Initialize Pushover Object """ super(NotifyPushover, self).__init__(**kwargs) - try: - # The token associated with the account - self.token = token.strip() - - except AttributeError: - # Token was None - msg = 'No API Token was specified.' + # Access Token (associated with project) + self.token = validate_regex( + token, *self.template_tokens['token']['regex']) + if not self.token: + msg = 'An invalid Pushover Access Token ' \ + '({}) was specified.'.format(token) self.logger.warning(msg) raise TypeError(msg) - if not VALIDATE_TOKEN.match(self.token): - msg = 'The API Token specified (%s) is invalid.'.format(token) + # User Key (associated with project) + self.user_key = validate_regex( + user_key, *self.template_tokens['user_key']['regex']) + if not self.user_key: + msg = 'An invalid Pushover User Key ' \ + '({}) was specified.'.format(user_key) self.logger.warning(msg) raise TypeError(msg) @@ -249,7 +245,7 @@ class NotifyPushover(NotifyBase): # The Priority of the message if priority not in PUSHOVER_PRIORITIES: - self.priority = PushoverPriority.NORMAL + self.priority = self.template_args['priority']['default'] else: self.priority = priority @@ -258,7 +254,7 @@ class NotifyPushover(NotifyBase): if self.priority == PushoverPriority.EMERGENCY: # How often to resend notification, in seconds - self.retry = NotifyPushover.template_args['retry']['default'] + self.retry = self.template_args['retry']['default'] try: self.retry = int(retry) except (ValueError, TypeError): @@ -266,7 +262,7 @@ class NotifyPushover(NotifyBase): pass # How often to resend notification, in seconds - self.expire = NotifyPushover.template_args['expire']['default'] + self.expire = self.template_args['expire']['default'] try: self.expire = int(expire) except (ValueError, TypeError): @@ -274,23 +270,16 @@ class NotifyPushover(NotifyBase): pass if self.retry < 30: - msg = 'Retry must be at least 30.' + msg = 'Pushover retry must be at least 30 seconds.' self.logger.warning(msg) raise TypeError(msg) + if self.expire < 0 or self.expire > 10800: - msg = 'Expire has a max value of at most 10800 seconds.' + msg = 'Pushover expire must reside in the range of ' \ + '0 to 10800 seconds.' self.logger.warning(msg) raise TypeError(msg) - - if not self.user: - msg = 'No user key was specified.' - self.logger.warning(msg) - raise TypeError(msg) - - if not VALIDATE_USER_KEY.match(self.user): - msg = 'The user key specified (%s) is invalid.' % self.user - self.logger.warning(msg) - raise TypeError(msg) + return def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs): """ @@ -323,7 +312,7 @@ class NotifyPushover(NotifyBase): # prepare JSON Object payload = { 'token': self.token, - 'user': self.user, + 'user': self.user_key, 'priority': str(self.priority), 'title': title, 'message': body, @@ -406,8 +395,8 @@ class NotifyPushover(NotifyBase): 'format': self.notify_format, 'overflow': self.overflow_mode, 'priority': - _map[PushoverPriority.NORMAL] if self.priority not in _map - else _map[self.priority], + _map[self.template_args['priority']['default']] + if self.priority not in _map else _map[self.priority], 'verify': 'yes' if self.verify_certificate else 'no', } # Only add expire and retry for emergency messages, @@ -426,7 +415,7 @@ class NotifyPushover(NotifyBase): return '{schema}://{user_key}@{token}/{devices}/?{args}'.format( schema=self.secure_protocol, - user_key=self.pprint(self.user, privacy, safe=''), + user_key=self.pprint(self.user_key, privacy, safe=''), token=self.pprint(self.token, privacy, safe=''), devices=devices, args=NotifyPushover.urlencode(args)) @@ -464,6 +453,9 @@ class NotifyPushover(NotifyBase): # Retrieve all of our targets results['targets'] = NotifyPushover.split_path(results['fullpath']) + # User Key is retrieved from the user + results['user_key'] = NotifyPushover.unquote(results['user']) + # Get the sound if 'sound' in results['qsd'] and len(results['qsd']['sound']): results['sound'] = \ diff --git a/apprise/plugins/NotifyRyver.py b/apprise/plugins/NotifyRyver.py index 7380994f..1f537ce0 100644 --- a/apprise/plugins/NotifyRyver.py +++ b/apprise/plugins/NotifyRyver.py @@ -40,14 +40,9 @@ from .NotifyBase import NotifyBase from ..common import NotifyImageSize from ..common import NotifyType from ..utils import parse_bool +from ..utils import validate_regex from ..AppriseLocale import gettext_lazy as _ -# Token required as part of the API request -VALIDATE_TOKEN = re.compile(r'[A-Z0-9]{15}', re.I) - -# Organization required as part of the API request -VALIDATE_ORG = re.compile(r'[A-Z0-9_-]{3,32}', re.I) - class RyverWebhookMode(object): """ @@ -99,12 +94,14 @@ class NotifyRyver(NotifyBase): 'name': _('Organization'), 'type': 'string', 'required': True, + 'regex': (r'^[A-Z0-9_-]{3,32}$', 'i'), }, 'token': { 'name': _('Token'), 'type': 'string', 'required': True, 'private': True, + 'regex': (r'^[A-Z0-9]{15}$', 'i'), }, 'user': { 'name': _('Bot Name'), @@ -135,25 +132,21 @@ class NotifyRyver(NotifyBase): """ super(NotifyRyver, self).__init__(**kwargs) - if not token: - msg = 'No Ryver token was specified.' + # API Token (associated with project) + self.token = validate_regex( + token, *self.template_tokens['token']['regex']) + if not self.token: + msg = 'An invalid Ryver API Token ' \ + '({}) was specified.'.format(token) self.logger.warning(msg) raise TypeError(msg) - if not organization: - msg = 'No Ryver organization was specified.' - self.logger.warning(msg) - raise TypeError(msg) - - if not VALIDATE_TOKEN.match(token.strip()): - msg = 'The Ryver token specified ({}) is invalid.'\ - .format(token) - self.logger.warning(msg) - raise TypeError(msg) - - if not VALIDATE_ORG.match(organization.strip()): - msg = 'The Ryver organization specified ({}) is invalid.'\ - .format(organization) + # Organization (associated with project) + self.organization = validate_regex( + organization, *self.template_tokens['organization']['regex']) + if not self.organization: + msg = 'An invalid Ryver Organization ' \ + '({}) was specified.'.format(organization) self.logger.warning(msg) raise TypeError(msg) @@ -167,12 +160,6 @@ class NotifyRyver(NotifyBase): self.logger.warning(msg) raise TypeError(msg) - # The organization associated with the account - self.organization = organization.strip() - - # The token associated with the account - self.token = token.strip() - # Place an image inline with the message body self.include_image = include_image @@ -193,6 +180,8 @@ class NotifyRyver(NotifyBase): re.IGNORECASE, ) + return + def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs): """ Perform Ryver Notification diff --git a/apprise/plugins/NotifySNS.py b/apprise/plugins/NotifySNS.py index 3774ea70..a547558c 100644 --- a/apprise/plugins/NotifySNS.py +++ b/apprise/plugins/NotifySNS.py @@ -36,6 +36,7 @@ from .NotifyBase import NotifyBase from ..URLBase import PrivacyMode from ..common import NotifyType from ..utils import parse_list +from ..utils import validate_regex from ..AppriseLocale import gettext_lazy as _ # Some Phone Number Detection @@ -117,21 +118,21 @@ class NotifySNS(NotifyBase): 'name': _('Region'), 'type': 'string', 'required': True, - 'regex': (r'[a-z]{2}-[a-z]+-[0-9]+', 'i'), + 'regex': (r'^[a-z]{2}-[a-z]+-[0-9]+$', 'i'), 'map_to': 'region_name', }, 'target_phone_no': { 'name': _('Target Phone No'), 'type': 'string', 'map_to': 'targets', - 'regex': (r'[0-9\s)(+-]+', 'i') + 'regex': (r'^[0-9\s)(+-]+$', 'i') }, 'target_topic': { 'name': _('Target Topic'), 'type': 'string', 'map_to': 'targets', 'prefix': '#', - 'regex': (r'[A-Za-z0-9_-]+', 'i'), + 'regex': (r'^[A-Za-z0-9_-]+$', 'i'), }, 'targets': { 'name': _('Targets'), @@ -153,18 +154,28 @@ class NotifySNS(NotifyBase): """ super(NotifySNS, self).__init__(**kwargs) - if not access_key_id: + # Store our AWS API Access Key + self.aws_access_key_id = validate_regex(access_key_id) + if not self.aws_access_key_id: msg = 'An invalid AWS Access Key ID was specified.' self.logger.warning(msg) raise TypeError(msg) - if not secret_access_key: - msg = 'An invalid AWS Secret Access Key was specified.' + # Store our AWS API Secret Access key + self.aws_secret_access_key = validate_regex(secret_access_key) + if not self.aws_secret_access_key: + msg = 'An invalid AWS Secret Access Key ' \ + '({}) was specified.'.format(secret_access_key) self.logger.warning(msg) raise TypeError(msg) - if not (region_name and IS_REGION.match(region_name)): - msg = 'An invalid AWS Region was specified.' + # Acquire our AWS Region Name: + # eg. us-east-1, cn-north-1, us-west-2, ... + self.aws_region_name = validate_regex( + region_name, *self.template_tokens['region']['regex']) + if not self.aws_region_name: + msg = 'An invalid AWS Region ({}) was specified.'.format( + region_name) self.logger.warning(msg) raise TypeError(msg) @@ -174,16 +185,6 @@ class NotifySNS(NotifyBase): # Initialize numbers list self.phone = list() - # Store our AWS API Key - self.aws_access_key_id = access_key_id - - # Store our AWS API Secret Access key - self.aws_secret_access_key = secret_access_key - - # Acquire our AWS Region Name: - # eg. us-east-1, cn-north-1, us-west-2, ... - self.aws_region_name = region_name - # Set our notify_url based on our region self.notify_url = 'https://sns.{}.amazonaws.com/'\ .format(self.aws_region_name) @@ -231,8 +232,12 @@ class NotifySNS(NotifyBase): ) if len(self.phone) == 0 and len(self.topics) == 0: - self.logger.warning( - 'There are no valid target(s) identified to notify.') + # We have a bot token and no target(s) to message + msg = 'No AWS targets to notify.' + self.logger.warning(msg) + raise TypeError(msg) + + return def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs): """ diff --git a/apprise/plugins/NotifySendGrid.py b/apprise/plugins/NotifySendGrid.py index 4ffa373a..7c0c1a12 100644 --- a/apprise/plugins/NotifySendGrid.py +++ b/apprise/plugins/NotifySendGrid.py @@ -43,7 +43,6 @@ # - https://sendgrid.com/docs/ui/sending-email/\ # how-to-send-an-email-with-dynamic-transactional-templates/ -import re import requests from json import dumps @@ -52,10 +51,9 @@ from ..common import NotifyFormat from ..common import NotifyType from ..utils import parse_list from ..utils import GET_EMAIL_RE +from ..utils import validate_regex from ..AppriseLocale import gettext_lazy as _ -IS_APIKEY_RE = re.compile(r'^([A-Z0-9._-]+)$', re.I) - # Extend HTTP Error Messages SENDGRID_HTTP_ERROR_MAP = { 401: 'Unauthorized - You do not have authorization to make the request.', @@ -109,6 +107,7 @@ class NotifySendGrid(NotifyBase): 'type': 'string', 'private': True, 'required': True, + 'regex': (r'^[A-Z0-9._-]+$', 'i'), }, 'from_email': { 'name': _('Source Email'), @@ -162,16 +161,12 @@ class NotifySendGrid(NotifyBase): """ super(NotifySendGrid, self).__init__(**kwargs) - # The API Key needed to perform all SendMail API i/o - self.apikey = apikey - try: - result = IS_APIKEY_RE.match(self.apikey) - if not result: - # let outer exception handle this - raise TypeError - - except (TypeError, AttributeError): - msg = 'Invalid API Key specified: {}'.format(self.apikey) + # API Key (associated with project) + self.apikey = validate_regex( + apikey, *self.template_tokens['apikey']['regex']) + if not self.apikey: + msg = 'An invalid SendGrid API Key ' \ + '({}) was specified.'.format(apikey) self.logger.warning(msg) raise TypeError(msg) diff --git a/apprise/plugins/NotifySimplePush.py b/apprise/plugins/NotifySimplePush.py index bd0b9c0c..8093d0e4 100644 --- a/apprise/plugins/NotifySimplePush.py +++ b/apprise/plugins/NotifySimplePush.py @@ -29,6 +29,7 @@ import requests from .NotifyBase import NotifyBase from ..URLBase import PrivacyMode from ..common import NotifyType +from ..utils import validate_regex from ..AppriseLocale import gettext_lazy as _ # Default our global support flag @@ -120,11 +121,26 @@ class NotifySimplePush(NotifyBase): """ super(NotifySimplePush, self).__init__(**kwargs) - # Store the API key - self.apikey = apikey + # API Key (associated with project) + self.apikey = validate_regex(apikey) + if not self.apikey: + msg = 'An invalid SimplePush API Key ' \ + '({}) was specified.'.format(apikey) + self.logger.warning(msg) + raise TypeError(msg) - # Event Name - self.event = event + if event: + # Event Name (associated with project) + self.event = validate_regex(event) + if not self.event: + msg = 'An invalid SimplePush Event Name ' \ + '({}) was specified.'.format(event) + self.logger.warning(msg) + raise TypeError(msg) + + else: + # Default Event Name + self.event = None # Encrypt Message (providing support is available) if self.password and self.user and not CRYPTOGRAPHY_AVAILABLE: @@ -182,7 +198,6 @@ class NotifySimplePush(NotifyBase): payload = { 'key': self.apikey, } - event = self.event if self.password and self.user and CRYPTOGRAPHY_AVAILABLE: body = self._encrypt(body) @@ -198,8 +213,9 @@ class NotifySimplePush(NotifyBase): 'title': title, }) - if event: - payload['event'] = event + if self.event: + # Store Event + payload['event'] = self.event self.logger.debug('SimplePush POST URL: %s (cert_verify=%r)' % ( self.notify_url, self.verify_certificate, diff --git a/apprise/plugins/NotifySlack.py b/apprise/plugins/NotifySlack.py index aa0e90a2..4d6f4da1 100644 --- a/apprise/plugins/NotifySlack.py +++ b/apprise/plugins/NotifySlack.py @@ -46,20 +46,9 @@ from ..common import NotifyType from ..common import NotifyFormat from ..utils import parse_bool from ..utils import parse_list +from ..utils import validate_regex from ..AppriseLocale import gettext_lazy as _ -# Token required as part of the API request -# /AAAAAAAAA/........./........................ -VALIDATE_TOKEN_A = re.compile(r'[A-Z0-9]{9}') - -# Token required as part of the API request -# /........./BBBBBBBBB/........................ -VALIDATE_TOKEN_B = re.compile(r'[A-Z0-9]{9}') - -# Token required as part of the API request -# /........./........./CCCCCCCCCCCCCCCCCCCCCCCC -VALIDATE_TOKEN_C = re.compile(r'[A-Za-z0-9]{24}') - # Extend HTTP Error Messages SLACK_HTTP_ERROR_MAP = { 401: 'Unauthorized - Invalid Token.', @@ -68,9 +57,6 @@ SLACK_HTTP_ERROR_MAP = { # Used to break path apart into list of channels CHANNEL_LIST_DELIM = re.compile(r'[ \t\r\n,#\\/]+') -# Used to detect a channel -IS_VALID_TARGET_RE = re.compile(r'[+#@]?([A-Z0-9_]{1,32})', re.I) - class NotifySlack(NotifyBase): """ @@ -116,26 +102,32 @@ class NotifySlack(NotifyBase): 'type': 'string', 'map_to': 'user', }, + # Token required as part of the API request + # /AAAAAAAAA/........./........................ 'token_a': { 'name': _('Token A'), 'type': 'string', 'private': True, 'required': True, - 'regex': (r'[A-Z0-9]{9}', 'i'), + 'regex': (r'^[A-Z0-9]{9}$', 'i'), }, + # Token required as part of the API request + # /........./BBBBBBBBB/........................ 'token_b': { 'name': _('Token B'), 'type': 'string', 'private': True, 'required': True, - 'regex': (r'[A-Z0-9]{9}', 'i'), + 'regex': (r'^[A-Z0-9]{9}$', 'i'), }, + # Token required as part of the API request + # /........./........./CCCCCCCCCCCCCCCCCCCCCCCC 'token_c': { 'name': _('Token C'), 'type': 'string', 'private': True, 'required': True, - 'regex': (r'[A-Za-z0-9]{24}', 'i'), + 'regex': (r'^[A-Za-z0-9]{24}$', 'i'), }, 'target_encoded_id': { 'name': _('Target Encoded ID'), @@ -181,48 +173,30 @@ class NotifySlack(NotifyBase): """ super(NotifySlack, self).__init__(**kwargs) - if not token_a: - msg = 'The first API token is not specified.' + self.token_a = validate_regex( + token_a, *self.template_tokens['token_a']['regex']) + if not self.token_a: + msg = 'An invalid Slack (first) Token ' \ + '({}) was specified.'.format(token_a) self.logger.warning(msg) raise TypeError(msg) - if not token_b: - msg = 'The second API token is not specified.' + self.token_b = validate_regex( + token_b, *self.template_tokens['token_b']['regex']) + if not self.token_b: + msg = 'An invalid Slack (second) Token ' \ + '({}) was specified.'.format(token_b) self.logger.warning(msg) raise TypeError(msg) - if not token_c: - msg = 'The third API token is not specified.' + self.token_c = validate_regex( + token_c, *self.template_tokens['token_c']['regex']) + if not self.token_c: + msg = 'An invalid Slack (third) Token ' \ + '({}) was specified.'.format(token_c) self.logger.warning(msg) raise TypeError(msg) - if not VALIDATE_TOKEN_A.match(token_a.strip()): - msg = 'The first API token specified ({}) is invalid.'\ - .format(token_a) - self.logger.warning(msg) - raise TypeError(msg) - - # The token associated with the account - self.token_a = token_a.strip() - - if not VALIDATE_TOKEN_B.match(token_b.strip()): - msg = 'The second API token specified ({}) is invalid.'\ - .format(token_b) - self.logger.warning(msg) - raise TypeError(msg) - - # The token associated with the account - self.token_b = token_b.strip() - - if not VALIDATE_TOKEN_C.match(token_c.strip()): - msg = 'The third API token specified ({}) is invalid.'\ - .format(token_c) - self.logger.warning(msg) - raise TypeError(msg) - - # The token associated with the account - self.token_c = token_c.strip() - if not self.user: self.logger.warning( 'No user was specified; using "%s".' % self.app_id) @@ -255,6 +229,8 @@ class NotifySlack(NotifyBase): # Place a thumbnail image inline with the message body self.include_image = include_image + return + def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs): """ Perform Slack Notification @@ -303,27 +279,30 @@ class NotifySlack(NotifyBase): channel = channels.pop(0) if channel is not None: - # Channel over-ride was specified - if not IS_VALID_TARGET_RE.match(channel): + _channel = validate_regex( + channel, r'[+#@]?([A-Z0-9_]{1,32})') + + if not _channel: + # Channel over-ride was specified self.logger.warning( "The specified target {} is invalid;" - "skipping.".format(channel)) + "skipping.".format(_channel)) # Mark our failure has_error = True continue - if len(channel) > 1 and channel[0] == '+': + if len(_channel) > 1 and _channel[0] == '+': # Treat as encoded id if prefixed with a + - payload['channel'] = channel[1:] + payload['channel'] = _channel[1:] - elif len(channel) > 1 and channel[0] == '@': + elif len(_channel) > 1 and _channel[0] == '@': # Treat @ value 'as is' - payload['channel'] = channel + payload['channel'] = _channel else: # Prefix with channel hash tag - payload['channel'] = '#%s' % channel + payload['channel'] = '#{}'.format(_channel) # Acquire our to-be footer icon if configured to do so image_url = None if not self.include_image \ @@ -478,9 +457,9 @@ class NotifySlack(NotifyBase): result = re.match( r'^https?://hooks\.slack\.com/services/' - r'(?P[A-Z0-9]{9})/' - r'(?P[A-Z0-9]{9})/' - r'(?P[A-Z0-9]{24})/?' + r'(?P[A-Z0-9]+)/' + r'(?P[A-Z0-9]+)/' + r'(?P[A-Z0-9]+)/?' r'(?P\?[.+])?$', url, re.I) if result: diff --git a/apprise/plugins/NotifyTechulusPush.py b/apprise/plugins/NotifyTechulusPush.py index 10f38b70..6614decd 100644 --- a/apprise/plugins/NotifyTechulusPush.py +++ b/apprise/plugins/NotifyTechulusPush.py @@ -47,12 +47,12 @@ # - https://push.techulus.com/ - Main Website # - https://pushtechulus.docs.apiary.io - API Documentation -import re import requests from json import dumps from .NotifyBase import NotifyBase from ..common import NotifyType +from ..utils import validate_regex from ..AppriseLocale import gettext_lazy as _ # Token required as part of the API request @@ -60,9 +60,6 @@ from ..AppriseLocale import gettext_lazy as _ UUID4_RE = \ r'[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}' -# API Key -VALIDATE_APIKEY = re.compile(UUID4_RE, re.I) - class NotifyTechulusPush(NotifyBase): """ @@ -99,7 +96,7 @@ class NotifyTechulusPush(NotifyBase): 'type': 'string', 'private': True, 'required': True, - 'regex': (UUID4_RE, 'i'), + 'regex': (r'^{}$'.format(UUID4_RE), 'i'), }, }) @@ -109,19 +106,14 @@ class NotifyTechulusPush(NotifyBase): """ super(NotifyTechulusPush, self).__init__(**kwargs) - if not apikey: - msg = 'The Techulus Push apikey is not specified.' - self.logger.warning(msg) - raise TypeError(msg) - - if not VALIDATE_APIKEY.match(apikey.strip()): - msg = 'The Techulus Push apikey specified ({}) is invalid.'\ - .format(apikey) - self.logger.warning(msg) - raise TypeError(msg) - # The apikey associated with the account - self.apikey = apikey.strip() + self.apikey = validate_regex( + apikey, *self.template_tokens['apikey']['regex']) + if not self.apikey: + msg = 'An invalid Techulus Push API key ' \ + '({}) was specified.'.format(apikey) + self.logger.warning(msg) + raise TypeError(msg) def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs): """ diff --git a/apprise/plugins/NotifyTelegram.py b/apprise/plugins/NotifyTelegram.py index b3b5213a..d04bbfd0 100644 --- a/apprise/plugins/NotifyTelegram.py +++ b/apprise/plugins/NotifyTelegram.py @@ -61,17 +61,11 @@ from ..common import NotifyImageSize from ..common import NotifyFormat from ..utils import parse_bool from ..utils import parse_list +from ..utils import validate_regex from ..AppriseLocale import gettext_lazy as _ TELEGRAM_IMAGE_XY = NotifyImageSize.XY_256 -# Token required as part of the API request -# allow the word 'bot' infront -VALIDATE_BOT_TOKEN = re.compile( - r'^(bot)?(?P[0-9]+:[a-z0-9_-]+)/*$', - re.IGNORECASE, -) - # Chat ID is required # 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 @@ -119,14 +113,16 @@ class NotifyTelegram(NotifyBase): 'type': 'string', 'private': True, 'required': True, - 'regex': (r'(bot)?[0-9]+:[a-z0-9_-]+', 'i'), + # Token required as part of the API request, allow the word 'bot' + # infront of it + 'regex': (r'^(bot)?(?P[0-9]+:[a-z0-9_-]+)$', 'i'), }, 'target_user': { 'name': _('Target Chat ID'), 'type': 'string', 'map_to': 'targets', 'map_to': 'targets', - 'regex': (r'((-?[0-9]{1,32})|([a-z_-][a-z0-9_-]+))', 'i'), + 'regex': (r'^((-?[0-9]{1,32})|([a-z_-][a-z0-9_-]+))$', 'i'), }, 'targets': { 'name': _('Targets'), @@ -160,24 +156,15 @@ class NotifyTelegram(NotifyBase): """ super(NotifyTelegram, self).__init__(**kwargs) - try: - self.bot_token = bot_token.strip() - - except AttributeError: - # Token was None - err = 'No Bot Token was specified.' + self.bot_token = validate_regex( + bot_token, *self.template_tokens['bot_token']['regex'], + fmt='{key}') + if not self.bot_token: + err = 'The Telegram Bot Token specified ({}) is invalid.'.format( + bot_token) self.logger.warning(err) raise TypeError(err) - result = VALIDATE_BOT_TOKEN.match(self.bot_token) - if not result: - err = 'The Bot Token specified (%s) is invalid.' % bot_token - self.logger.warning(err) - raise TypeError(err) - - # Store our Bot Token - self.bot_token = result.group('key') - # Parse our list self.targets = parse_list(targets) diff --git a/apprise/plugins/NotifyTwilio.py b/apprise/plugins/NotifyTwilio.py index d60ef0be..0a5fd503 100644 --- a/apprise/plugins/NotifyTwilio.py +++ b/apprise/plugins/NotifyTwilio.py @@ -48,13 +48,10 @@ from .NotifyBase import NotifyBase from ..URLBase import PrivacyMode from ..common import NotifyType from ..utils import parse_list +from ..utils import validate_regex from ..AppriseLocale import gettext_lazy as _ -# Used to validate your personal access apikey -VALIDATE_AUTH_TOKEN = re.compile(r'^[a-f0-9]{32}$', re.I) -VALIDATE_ACCOUNT_SID = re.compile(r'^AC[a-f0-9]{32}$', re.I) - # Some Phone Number Detection IS_PHONE_NO = re.compile(r'^\+?(?P[0-9\s)(+-]+)\s*$') @@ -108,33 +105,33 @@ class NotifyTwilio(NotifyBase): 'type': 'string', 'private': True, 'required': True, - 'regex': (r'AC[a-f0-9]{32}', 'i'), + 'regex': (r'^AC[a-f0-9]+$', 'i'), }, 'auth_token': { 'name': _('Auth Token'), 'type': 'string', 'private': True, 'required': True, - 'regex': (r'[a-f0-9]{32}', 'i'), + 'regex': (r'^[a-f0-9]+$', 'i'), }, 'from_phone': { 'name': _('From Phone No'), 'type': 'string', 'required': True, - 'regex': (r'\+?[0-9\s)(+-]+', 'i'), + 'regex': (r'^\+?[0-9\s)(+-]+$', 'i'), 'map_to': 'source', }, 'target_phone': { 'name': _('Target Phone No'), 'type': 'string', 'prefix': '+', - 'regex': (r'[0-9\s)(+-]+', 'i'), + 'regex': (r'^[0-9\s)(+-]+$', 'i'), 'map_to': 'targets', }, 'short_code': { 'name': _('Target Short Code'), 'type': 'string', - 'regex': (r'[0-9]{5,6}', 'i'), + 'regex': (r'^[0-9]{5,6}$', 'i'), 'map_to': 'targets', }, 'targets': { @@ -166,35 +163,21 @@ class NotifyTwilio(NotifyBase): """ super(NotifyTwilio, self).__init__(**kwargs) - try: - # The Account SID associated with the account - self.account_sid = account_sid.strip() - - except AttributeError: - # Token was None - msg = 'No Account SID was specified.' + # The Account SID associated with the account + self.account_sid = validate_regex( + account_sid, *self.template_tokens['account_sid']['regex']) + if not self.account_sid: + msg = 'An invalid Twilio Account SID ' \ + '({}) was specified.'.format(account_sid) self.logger.warning(msg) raise TypeError(msg) - if not VALIDATE_ACCOUNT_SID.match(self.account_sid): - msg = 'The Account SID specified ({}) is invalid.' \ - .format(account_sid) - self.logger.warning(msg) - raise TypeError(msg) - - try: - # The authentication token associated with the account - self.auth_token = auth_token.strip() - - except AttributeError: - # Token was None - msg = 'No Auth Token was specified.' - self.logger.warning(msg) - raise TypeError(msg) - - if not VALIDATE_AUTH_TOKEN.match(self.auth_token): - msg = 'The Auth Token specified ({}) is invalid.' \ - .format(auth_token) + # The Authentication Token associated with the account + self.auth_token = validate_regex( + auth_token, *self.template_tokens['auth_token']['regex']) + if not self.auth_token: + msg = 'An invalid Twilio Authentication Token ' \ + '({}) was specified.'.format(auth_token) self.logger.warning(msg) raise TypeError(msg) @@ -254,14 +237,16 @@ class NotifyTwilio(NotifyBase): '({}) specified.'.format(target), ) - if len(self.targets) == 0: - msg = 'There are no valid targets identified to notify.' + if not self.targets: if len(self.source) in (5, 6): # raise a warning since we're a short-code. We need # a number to message + msg = 'There are no valid Twilio targets to notify.' self.logger.warning(msg) raise TypeError(msg) + return + def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs): """ Perform Twilio Notification diff --git a/apprise/plugins/NotifyTwitter.py b/apprise/plugins/NotifyTwitter.py index 5287ed12..829d2197 100644 --- a/apprise/plugins/NotifyTwitter.py +++ b/apprise/plugins/NotifyTwitter.py @@ -37,6 +37,7 @@ from ..URLBase import PrivacyMode from ..common import NotifyType from ..utils import parse_list from ..utils import parse_bool +from ..utils import validate_regex from ..AppriseLocale import gettext_lazy as _ IS_USER = re.compile(r'^\s*@?(?P[A-Z0-9_]+)$', re.I) @@ -186,23 +187,27 @@ class NotifyTwitter(NotifyBase): """ super(NotifyTwitter, self).__init__(**kwargs) - if not ckey: - msg = 'An invalid Consumer API Key was specified.' + self.ckey = validate_regex(ckey) + if not self.ckey: + msg = 'An invalid Twitter Consumer Key was specified.' self.logger.warning(msg) raise TypeError(msg) - if not csecret: - msg = 'An invalid Consumer Secret API Key was specified.' + self.csecret = validate_regex(csecret) + if not self.csecret: + msg = 'An invalid Twitter Consumer Secret was specified.' self.logger.warning(msg) raise TypeError(msg) - if not akey: - msg = 'An invalid Access Token API Key was specified.' + self.akey = validate_regex(akey) + if not self.akey: + msg = 'An invalid Twitter Access Key was specified.' self.logger.warning(msg) raise TypeError(msg) - if not asecret: - msg = 'An invalid Access Token Secret API Key was specified.' + self.asecret = validate_regex(asecret) + if not self.asecret: + msg = 'An invalid Access Secret was specified.' self.logger.warning(msg) raise TypeError(msg) @@ -219,6 +224,9 @@ class NotifyTwitter(NotifyBase): self.logger.warning(msg) raise TypeError(msg) + # Track any errors + has_error = False + # Identify our targets self.targets = [] for target in parse_list(targets): @@ -227,15 +235,19 @@ class NotifyTwitter(NotifyBase): self.targets.append(match.group('user')) continue + has_error = True self.logger.warning( 'Dropped invalid user ({}) specified.'.format(target), ) - # Store our data - self.ckey = ckey - self.csecret = csecret - self.akey = akey - self.asecret = asecret + if has_error and not self.targets: + # We have specified that we want to notify one or more individual + # and we failed to load any of them. Since it's also valid to + # notify no one at all (which means we notify ourselves), it's + # important we don't switch from the users original intentions + msg = 'No Twitter targets to notify.' + self.logger.warning(msg) + raise TypeError(msg) return @@ -297,7 +309,7 @@ class NotifyTwitter(NotifyBase): } } - # Lookup our users + # Lookup our users (otherwise we look up ourselves) targets = self._whoami(lazy=self.cache) if not len(self.targets) \ else self._user_lookup(self.targets, lazy=self.cache) diff --git a/apprise/plugins/NotifyWebexTeams.py b/apprise/plugins/NotifyWebexTeams.py index cc79c7ef..db233a58 100644 --- a/apprise/plugins/NotifyWebexTeams.py +++ b/apprise/plugins/NotifyWebexTeams.py @@ -63,11 +63,9 @@ from json import dumps from .NotifyBase import NotifyBase from ..common import NotifyType from ..common import NotifyFormat +from ..utils import validate_regex from ..AppriseLocale import gettext_lazy as _ -# Token required as part of the API request -VALIDATE_TOKEN = re.compile(r'[a-z0-9]{80}', re.I) - # Extend HTTP Error Messages # Based on: https://developer.webex.com/docs/api/basics/rate-limiting WEBEX_HTTP_ERROR_MAP = { @@ -119,7 +117,7 @@ class NotifyWebexTeams(NotifyBase): 'type': 'string', 'private': True, 'required': True, - 'regex': (r'[a-z0-9]{80}', 'i'), + 'regex': (r'^[a-z0-9]{80}$', 'i'), }, }) @@ -129,20 +127,15 @@ class NotifyWebexTeams(NotifyBase): """ super(NotifyWebexTeams, self).__init__(**kwargs) - if not token: - msg = 'The Webex Teams token is not specified.' - self.logger.warning(msg) - raise TypeError(msg) - - if not VALIDATE_TOKEN.match(token.strip()): + # The token associated with the account + self.token = validate_regex( + token, *self.template_tokens['token']['regex']) + if not self.token: msg = 'The Webex Teams token specified ({}) is invalid.'\ .format(token) self.logger.warning(msg) raise TypeError(msg) - # The token associated with the account - self.token = token.strip() - def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs): """ Perform Webex Teams Notification diff --git a/apprise/plugins/NotifyXMPP.py b/apprise/plugins/NotifyXMPP.py index 6d5ee4d4..82623cb4 100644 --- a/apprise/plugins/NotifyXMPP.py +++ b/apprise/plugins/NotifyXMPP.py @@ -157,7 +157,7 @@ class NotifyXMPP(NotifyBase): 'name': _('XEP'), 'type': 'list:string', 'prefix': 'xep-', - 'regex': (r'[1-9][0-9]{0,3}', 'i'), + 'regex': (r'^[1-9][0-9]{0,3}$', 'i'), }, 'jid': { 'name': _('Source JID'), diff --git a/apprise/plugins/NotifyZulip.py b/apprise/plugins/NotifyZulip.py index 93edd0ba..00024218 100644 --- a/apprise/plugins/NotifyZulip.py +++ b/apprise/plugins/NotifyZulip.py @@ -61,15 +61,13 @@ import requests from .NotifyBase import NotifyBase from ..common import NotifyType from ..utils import parse_list +from ..utils import validate_regex from ..utils import GET_EMAIL_RE from ..AppriseLocale import gettext_lazy as _ # A Valid Bot Name VALIDATE_BOTNAME = re.compile(r'(?P[A-Z0-9_]{1,32})(-bot)?', re.I) -# A Valid Bot Token is 32 characters of alpha/numeric -VALIDATE_TOKEN = re.compile(r'[A-Z0-9]{32}', re.I) - # Organization required as part of the API request VALIDATE_ORG = re.compile( r'(?P[A-Z0-9_-]{1,32})(\.(?P[^\s]+))?', re.I) @@ -124,18 +122,20 @@ class NotifyZulip(NotifyBase): 'botname': { 'name': _('Bot Name'), 'type': 'string', + 'regex': (r'^[A-Z0-9_]{1,32}(-bot)?$', 'i'), }, 'organization': { 'name': _('Organization'), 'type': 'string', 'required': True, + 'regex': (r'^[A-Z0-9_-]{1,32})$', 'i') }, 'token': { 'name': _('Token'), 'type': 'string', 'required': True, 'private': True, - 'regex': (r'[A-Z0-9]{32}', 'i'), + 'regex': (r'^[A-Z0-9]{32}$', 'i'), }, 'target_user': { 'name': _('Target User'), @@ -208,20 +208,14 @@ class NotifyZulip(NotifyBase): self.logger.warning(msg) raise TypeError(msg) - try: - if not VALIDATE_TOKEN.match(token.strip()): - # let outer exception handle this - raise TypeError - - except (TypeError, AttributeError): + self.token = validate_regex( + token, *self.template_tokens['token']['regex']) + if not self.token: msg = 'The Zulip token specified ({}) is invalid.'\ .format(token) self.logger.warning(msg) raise TypeError(msg) - # The token associated with the account - self.token = token.strip() - self.targets = parse_list(targets) if len(self.targets) == 0: # No channels identified, use default diff --git a/apprise/plugins/__init__.py b/apprise/plugins/__init__.py index 6faaa201..75035008 100644 --- a/apprise/plugins/__init__.py +++ b/apprise/plugins/__init__.py @@ -378,6 +378,16 @@ def details(plugin): # Argument/Option Handling for key in list(template_args.keys()): + if 'alias_of' in template_args[key]: + # Check if the mapped reference is a list; if it is, then + # we need to store a different delimiter + alias_of = template_tokens.get(template_args[key]['alias_of'], {}) + if alias_of.get('type', '').startswith('list') \ + and 'delim' not in template_args[key]: + # Set a default delimiter of a comma and/or space if one + # hasn't already been specified + template_args[key]['delim'] = (',', ' ') + # _lookup_default looks up what the default value if '_lookup_default' in template_args[key]: template_args[key]['default'] = getattr( diff --git a/apprise/utils.py b/apprise/utils.py index 6b94acdc..b1758c1e 100644 --- a/apprise/utils.py +++ b/apprise/utils.py @@ -28,6 +28,7 @@ import six import contextlib import os from os.path import expanduser +from functools import reduce try: # Python 2.7 @@ -113,10 +114,17 @@ GET_EMAIL_RE = re.compile( re.IGNORECASE, ) +# Regular expression used to extract a phone number +GET_PHONE_NO_RE = re.compile(r'^\+?(?P[0-9\s)(+-]+)\s*$') + # Regular expression used to destinguish between multiple URLs URL_DETECTION_RE = re.compile( r'([a-z0-9]+?:\/\/.*?)[\s,]*(?=$|[a-z0-9]+?:\/\/)', re.I) +# validate_regex() utilizes this mapping to track and re-use pre-complied +# regular expressions +REGEX_VALIDATE_LOOKUP = {} + def is_hostname(hostname): """ @@ -512,14 +520,6 @@ def parse_list(*args): elif isinstance(arg, (set, list, tuple)): result += parse_list(*arg) - elif arg is None: - # Ignore - continue - - else: - # Convert whatever it is to a string and work with it - result += parse_list(str(arg)) - # # filter() eliminates any empty entries # @@ -573,6 +573,11 @@ def is_exclusive_match(logic, data, match_all='all'): # treat these entries as though all elements found # must exist in the notification service entries = set(parse_list(entry)) + if not entries: + # We got a bogus set of tags to parse + # If there is no logic to apply then we're done early; we only + # match if there is also no data to match against + return not data if len(entries.intersection(data.union({match_all}))) == len(entries): # our set contains all of the entries found @@ -587,6 +592,82 @@ def is_exclusive_match(logic, data, match_all='all'): return matched +def validate_regex(value, regex=r'[^\s]+', flags=re.I, strip=True, fmt=None): + """ + A lot of the tokens, secrets, api keys, etc all have some regular + expression validation they support. This hashes the regex after it's + compiled and returns it's content if matched, otherwise it returns None. + + This function greatly increases performance as it prevents apprise modules + from having to pre-compile all of their regular expressions. + + value is the element being tested + regex is the regular expression to be compiled and tested. By default + we extract the first chunk of code while eliminating surrounding + whitespace (if present) + + flags is the regular expression flags that should be applied + format is used to alter the response format if the regular + expression matches. You identify your format using {tags}. + Effectively nesting your ID's between {}. Consider a regex of: + '(?P[0-9]{2})[0-9]+(?P[A-Z])' + to which you could set your format up as '{value}-{year}'. This + would substitute the matched groups and format a response. + """ + + if flags: + # Regex String -> Flag Lookup Map + _map = { + # Ignore Case + 'i': re.I, + # Multi Line + 'm': re.M, + # Dot Matches All + 's': re.S, + # Locale Dependant + 'L': re.L, + # Unicode Matching + 'u': re.U, + # Verbose + 'x': re.X, + } + + if isinstance(flags, six.string_types): + # Convert a string of regular expression flags into their + # respected integer (expected) Python values and perform + # a bit-wise or on each match found: + flags = reduce( + lambda x, y: x | y, + [0] + [_map[f] for f in flags if f in _map]) + + else: + # Handles None/False/'' cases + flags = 0 + + # A key is used to store our compiled regular expression + key = '{}{}'.format(regex, flags) + + if key not in REGEX_VALIDATE_LOOKUP: + REGEX_VALIDATE_LOOKUP[key] = re.compile(regex, flags) + + # Perform our lookup usig our pre-compiled result + try: + result = REGEX_VALIDATE_LOOKUP[key].match(value) + if not result: + # let outer exception handle this + raise TypeError + + if fmt: + # Map our format back to our response + value = fmt.format(**result.groupdict()) + + except (TypeError, AttributeError): + return None + + # Return our response + return value.strip() if strip else value + + @contextlib.contextmanager def environ(*remove, **update): """ diff --git a/test/test_api.py b/test/test_api.py index 1309a40b..5426b73a 100644 --- a/test/test_api.py +++ b/test/test_api.py @@ -68,11 +68,11 @@ def test_apprise(): a = Apprise() # no items - assert(len(a) == 0) + assert len(a) == 0 # Apprise object can also be directly tested with 'if' keyword # No entries results in a False response - assert(not a) + assert not a # Create an Asset object asset = AppriseAsset(theme='default') @@ -89,66 +89,62 @@ def test_apprise(): a = Apprise(servers=servers) # 2 servers loaded - assert(len(a) == 2) + assert len(a) == 2 # Apprise object can also be directly tested with 'if' keyword # At least one entry results in a True response - assert(a) + assert a # We can retrieve our URLs this way: - assert(len(a.urls()) == 2) + assert len(a.urls()) == 2 # We can add another server - assert( - a.add('mmosts://mattermost.server.local/' - '3ccdd113474722377935511fc85d3dd4') is True) - assert(len(a) == 3) + assert a.add('mmosts://mattermost.server.local/' + '3ccdd113474722377935511fc85d3dd4') is True + assert len(a) == 3 # We can pop an object off of our stack by it's indexed value: obj = a.pop(0) - assert(isinstance(obj, NotifyBase) is True) - assert(len(a) == 2) + assert isinstance(obj, NotifyBase) is True + assert len(a) == 2 # We can retrieve elements from our list too by reference: - assert(isinstance(a[0].url(), six.string_types) is True) + assert isinstance(a[0].url(), six.string_types) is True # We can iterate over our list too: count = 0 for o in a: - assert(isinstance(o.url(), six.string_types) is True) + assert isinstance(o.url(), six.string_types) is True count += 1 # verify that we did indeed iterate over each element - assert(len(a) == count) + assert len(a) == count # We can empty our set a.clear() - assert(len(a) == 0) + assert len(a) == 0 # An invalid schema - assert( - a.add('this is not a parseable url at all') is False) - assert(len(a) == 0) + assert a.add('this is not a parseable url at all') is False + assert len(a) == 0 # An unsupported schema - assert( - a.add('invalid://we.just.do.not.support.this.plugin.type') is False) - assert(len(a) == 0) + assert a.add( + 'invalid://we.just.do.not.support.this.plugin.type') is False + assert len(a) == 0 # A poorly formatted URL - assert( - a.add('json://user:@@@:bad?no.good') is False) - assert(len(a) == 0) + assert a.add('json://user:@@@:bad?no.good') is False + assert len(a) == 0 # Add a server with our asset we created earlier - assert( - a.add('mmosts://mattermost.server.local/' - '3ccdd113474722377935511fc85d3dd4', asset=asset) is True) + assert a.add('mmosts://mattermost.server.local/' + '3ccdd113474722377935511fc85d3dd4', asset=asset) is True # Clear our server listings again a.clear() # No servers to notify - assert(a.notify(title="my title", body="my body") is False) + assert a.notify(title="my title", body="my body") is False class BadNotification(NotifyBase): def __init__(self, **kwargs): @@ -183,26 +179,26 @@ def test_apprise(): # Just to explain what is happening here, we would have parsed the # url properly but failed when we went to go and create an instance # of it. - assert(a.add('bad://localhost') is False) - assert(len(a) == 0) + assert a.add('bad://localhost') is False + assert len(a) == 0 - assert(a.add('good://localhost') is True) - assert(len(a) == 1) + assert a.add('good://localhost') is True + assert len(a) == 1 # Bad Notification Type is still allowed as it is presumed the user # know's what their doing - assert(a.notify( - title="my title", body="my body", notify_type='bad') is True) + assert a.notify( + title="my title", body="my body", notify_type='bad') is True # No Title/Body combo's - assert(a.notify(title=None, body=None) is False) - assert(a.notify(title='', body=None) is False) - assert(a.notify(title=None, body='') is False) + assert a.notify(title=None, body=None) is False + assert a.notify(title='', body=None) is False + assert a.notify(title=None, body='') is False # As long as one is present, we're good - assert(a.notify(title=None, body='present') is True) - assert(a.notify(title='present', body=None) is True) - assert(a.notify(title="present", body="present") is True) + assert a.notify(title=None, body='present') is True + assert a.notify(title='present', body=None) is True + assert a.notify(title="present", body="present") is True # Clear our server listings again a.clear() @@ -244,14 +240,14 @@ def test_apprise(): # Store our good notification in our schema map SCHEMA_MAP['runtime'] = RuntimeNotification - assert(a.add('runtime://localhost') is True) - assert(a.add('throw://localhost') is True) - assert(a.add('fail://localhost') is True) - assert(len(a) == 3) + assert a.add('runtime://localhost') is True + assert a.add('throw://localhost') is True + assert a.add('fail://localhost') is True + assert len(a) == 3 # Test when our notify both throws an exception and or just # simply returns False - assert(a.notify(title="present", body="present") is False) + assert a.notify(title="present", body="present") is False # Create a Notification that throws an unexected exception class ThrowInstantiateNotification(NotifyBase): @@ -267,7 +263,7 @@ def test_apprise(): # Reset our object a.clear() - assert(len(a) == 0) + assert len(a) == 0 # Instantiate a bad object plugin = a.instantiate(object, tag="bad_object") @@ -275,40 +271,37 @@ def test_apprise(): # Instantiate a good object plugin = a.instantiate('good://localhost', tag="good") - assert(isinstance(plugin, NotifyBase)) + assert isinstance(plugin, NotifyBase) # Test simple tagging inside of the object - assert("good" in plugin) - assert("bad" not in plugin) + assert "good" in plugin + assert "bad" not in plugin # the in (__contains__ override) is based on or'ed content; so although # 'bad' isn't tagged as being in the plugin, 'good' is, so the return # value of this is True - assert(["bad", "good"] in plugin) - assert(set(["bad", "good"]) in plugin) - assert(("bad", "good") in plugin) + assert ["bad", "good"] in plugin + assert set(["bad", "good"]) in plugin + assert ("bad", "good") in plugin # We an add already substatiated instances into our Apprise object a.add(plugin) - assert(len(a) == 1) + assert len(a) == 1 # We can add entries as a list too (to add more then one) a.add([plugin, plugin, plugin]) - assert(len(a) == 4) + assert len(a) == 4 # Reset our object again a.clear() - try: + with pytest.raises(TypeError): a.instantiate('throw://localhost', suppress_exceptions=False) - assert(False) - except TypeError: - assert(True) - assert(len(a) == 0) + assert len(a) == 0 - assert(a.instantiate( - 'throw://localhost', suppress_exceptions=True) is None) - assert(len(a) == 0) + assert a.instantiate( + 'throw://localhost', suppress_exceptions=True) is None + assert len(a) == 0 # # We rince and repeat the same tests as above, however we do them @@ -317,50 +310,53 @@ def test_apprise(): # Reset our object a.clear() - assert(len(a) == 0) + assert len(a) == 0 # Instantiate a good object plugin = a.instantiate({ 'schema': 'good', 'host': 'localhost'}, tag="good") - assert(isinstance(plugin, NotifyBase)) + assert isinstance(plugin, NotifyBase) # Test simple tagging inside of the object - assert("good" in plugin) - assert("bad" not in plugin) + assert "good" in plugin + assert "bad" not in plugin # the in (__contains__ override) is based on or'ed content; so although # 'bad' isn't tagged as being in the plugin, 'good' is, so the return # value of this is True - assert(["bad", "good"] in plugin) - assert(set(["bad", "good"]) in plugin) - assert(("bad", "good") in plugin) + assert ["bad", "good"] in plugin + assert set(["bad", "good"]) in plugin + assert ("bad", "good") in plugin # We an add already substatiated instances into our Apprise object a.add(plugin) - assert(len(a) == 1) + assert len(a) == 1 # We can add entries as a list too (to add more then one) a.add([plugin, plugin, plugin]) - assert(len(a) == 4) + assert len(a) == 4 # Reset our object again a.clear() - try: + with pytest.raises(TypeError): a.instantiate({ 'schema': 'throw', 'host': 'localhost'}, suppress_exceptions=False) - assert(False) - except TypeError: - assert(True) - assert(len(a) == 0) + assert len(a) == 0 - assert(a.instantiate({ + assert a.instantiate({ 'schema': 'throw', - 'host': 'localhost'}, suppress_exceptions=True) is None) - assert(len(a) == 0) + 'host': 'localhost'}, suppress_exceptions=True) is None + assert len(a) == 0 + +def test_apprise_pretty_print(tmpdir): + """ + API: Apprise() Pretty Print tests + + """ # Privacy Print # PrivacyMode.Secret always returns the same thing to avoid guessing assert URLBase.pprint( @@ -410,6 +406,10 @@ def test_apprise(): assert URLBase.pprint( "abcdefghijk", privacy=True, mode=PrivacyMode.Tail) == '...hijk' + # Quoting settings + assert URLBase.pprint(" ", privacy=False, safe='') == '%20' + assert URLBase.pprint(" ", privacy=False, quote=False, safe='') == ' ' + @mock.patch('requests.get') @mock.patch('requests.post') @@ -437,90 +437,94 @@ def test_apprise_tagging(mock_post, mock_get): a = Apprise() # An invalid addition can't add the tag - assert(a.add('averyinvalidschema://localhost', tag='uhoh') is False) - assert(a.add({ + assert a.add('averyinvalidschema://localhost', tag='uhoh') is False + assert a.add({ 'schema': 'averyinvalidschema', - 'host': 'localhost'}, tag='uhoh') is False) + 'host': 'localhost'}, tag='uhoh') is False # Add entry and assign it to a tag called 'awesome' - assert(a.add('json://localhost/path1/', tag='awesome') is True) - assert(a.add({ + assert a.add('json://localhost/path1/', tag='awesome') is True + assert a.add({ 'schema': 'json', 'host': 'localhost', - 'fullpath': '/path1/'}, tag='awesome') is True) + 'fullpath': '/path1/'}, tag='awesome') is True # Add another notification and assign it to a tag called 'awesome' # and another tag called 'local' - assert(a.add('json://localhost/path2/', tag=['mmost', 'awesome']) is True) + assert a.add('json://localhost/path2/', tag=['mmost', 'awesome']) is True # notify the awesome tag; this would notify both services behind the # scenes - assert(a.notify(title="my title", body="my body", tag='awesome') is True) + assert a.notify(title="my title", body="my body", tag='awesome') is True # notify all of the tags - assert(a.notify( - title="my title", body="my body", tag=['awesome', 'mmost']) is True) + assert a.notify( + title="my title", body="my body", tag=['awesome', 'mmost']) is True # When we query against our loaded notifications for a tag that simply # isn't assigned to anything, we return None. None (different then False) # tells us that we litterally had nothing to query. We didn't fail... # but we also didn't do anything... - assert(a.notify( - title="my title", body="my body", tag='missing') is None) + assert a.notify( + title="my title", body="my body", tag='missing') is None # Now to test the ability to and and/or notifications a = Apprise() # Add a tag by tuple - assert(a.add('json://localhost/tagA/', tag=("TagA", )) is True) + assert a.add('json://localhost/tagA/', tag=("TagA", )) is True # Add 2 tags by string - assert(a.add('json://localhost/tagAB/', tag="TagA, TagB") is True) + assert a.add('json://localhost/tagAB/', tag="TagA, TagB") is True # Add a tag using a set - assert(a.add('json://localhost/tagB/', tag=set(["TagB"])) is True) + assert a.add('json://localhost/tagB/', tag=set(["TagB"])) is True # Add a tag by string (again) - assert(a.add('json://localhost/tagC/', tag="TagC") is True) + assert a.add('json://localhost/tagC/', tag="TagC") is True # Add 2 tags using a list - assert(a.add('json://localhost/tagCD/', tag=["TagC", "TagD"]) is True) + assert a.add('json://localhost/tagCD/', tag=["TagC", "TagD"]) is True # Add a tag by string (again) - assert(a.add('json://localhost/tagD/', tag="TagD") is True) + assert a.add('json://localhost/tagD/', tag="TagD") is True # add a tag set by set (again) - assert(a.add('json://localhost/tagCDE/', - tag=set(["TagC", "TagD", "TagE"])) is True) + assert a.add('json://localhost/tagCDE/', + tag=set(["TagC", "TagD", "TagE"])) is True # Expression: TagC and TagD # Matches the following only: # - json://localhost/tagCD/ # - json://localhost/tagCDE/ - assert(a.notify( - title="my title", body="my body", tag=[('TagC', 'TagD')]) is True) + assert a.notify( + title="my title", body="my body", tag=[('TagC', 'TagD')]) is True # Expression: (TagY and TagZ) or TagX # Matches nothing, None is returned in this case - assert(a.notify( + assert a.notify( title="my title", body="my body", - tag=[('TagY', 'TagZ'), 'TagX']) is None) + tag=[('TagY', 'TagZ'), 'TagX']) is None # Expression: (TagY and TagZ) or TagA # Matches the following only: # - json://localhost/tagAB/ - assert(a.notify( + assert a.notify( title="my title", body="my body", - tag=[('TagY', 'TagZ'), 'TagA']) is True) + tag=[('TagY', 'TagZ'), 'TagA']) is True # Expression: (TagE and TagD) or TagB # Matches the following only: # - json://localhost/tagCDE/ # - json://localhost/tagAB/ # - json://localhost/tagB/ - assert(a.notify( + assert a.notify( title="my title", body="my body", - tag=[('TagE', 'TagD'), 'TagB']) is True) + tag=[('TagE', 'TagD'), 'TagB']) is True - # Garbage Entries; we can't do anything with the tag so we have nothing to - # notify as a result. So we simply return None - assert(a.notify( + # Garbage Entries in tag field just get stripped out. the below + # is the same as notifying no tags at all. Since we have not added + # any entries that do not have tags (that we can match against) + # we fail. None is returned as a way of letting us know that we + # had Notifications to notify, but since none of them matched our tag + # none were notified. + assert a.notify( title="my title", body="my body", - tag=[(object, ), ]) is None) + tag=[(object, ), ]) is None def test_apprise_notify_formats(tmpdir): @@ -536,7 +540,7 @@ def test_apprise_notify_formats(tmpdir): a = Apprise() # no items - assert(len(a) == 0) + assert len(a) == 0 class TextNotification(NotifyBase): # set our default notification format @@ -594,26 +598,29 @@ def test_apprise_notify_formats(tmpdir): # defined plugin above was defined to default to HTML which triggers # a markdown to take place if the body_format specified on the notify # call - assert(a.add('html://localhost') is True) - assert(a.add('html://another.server') is True) - assert(a.add('html://and.another') is True) - assert(a.add('text://localhost') is True) - assert(a.add('text://another.server') is True) - assert(a.add('text://and.another') is True) - assert(a.add('markdown://localhost') is True) - assert(a.add('markdown://another.server') is True) - assert(a.add('markdown://and.another') is True) + assert a.add('html://localhost') is True + assert a.add('html://another.server') is True + assert a.add('html://and.another') is True + assert a.add('text://localhost') is True + assert a.add('text://another.server') is True + assert a.add('text://and.another') is True + assert a.add('markdown://localhost') is True + assert a.add('markdown://another.server') is True + assert a.add('markdown://and.another') is True - assert(len(a) == 9) + assert len(a) == 9 - assert(a.notify(title="markdown", body="## Testing Markdown", - body_format=NotifyFormat.MARKDOWN) is True) + assert a.notify( + title="markdown", body="## Testing Markdown", + body_format=NotifyFormat.MARKDOWN) is True - assert(a.notify(title="text", body="Testing Text", - body_format=NotifyFormat.TEXT) is True) + assert a.notify( + title="text", body="Testing Text", + body_format=NotifyFormat.TEXT) is True - assert(a.notify(title="html", body="HTML", - body_format=NotifyFormat.HTML) is True) + assert a.notify( + title="html", body="HTML", + body_format=NotifyFormat.HTML) is True def test_apprise_asset(tmpdir): @@ -623,7 +630,7 @@ def test_apprise_asset(tmpdir): """ a = AppriseAsset(theme=None) # Default theme - assert(a.theme == 'default') + assert a.theme == 'default' a = AppriseAsset( theme='dark', @@ -634,58 +641,49 @@ def test_apprise_asset(tmpdir): a.default_html_color = '#abcabc' a.html_notify_map[NotifyType.INFO] = '#aaaaaa' - assert(a.color('invalid', tuple) == (171, 202, 188)) - assert(a.color(NotifyType.INFO, tuple) == (170, 170, 170)) + assert a.color('invalid', tuple) == (171, 202, 188) + assert a.color(NotifyType.INFO, tuple) == (170, 170, 170) - assert(a.color('invalid', int) == 11258556) - assert(a.color(NotifyType.INFO, int) == 11184810) + assert a.color('invalid', int) == 11258556 + assert a.color(NotifyType.INFO, int) == 11184810 - assert(a.color('invalid', None) == '#abcabc') - assert(a.color(NotifyType.INFO, None) == '#aaaaaa') + assert a.color('invalid', None) == '#abcabc' + assert a.color(NotifyType.INFO, None) == '#aaaaaa' # None is the default - assert(a.color(NotifyType.INFO) == '#aaaaaa') + assert a.color(NotifyType.INFO) == '#aaaaaa' # Invalid Type - try: - a.color(NotifyType.INFO, dict) - # We should not get here (exception should be thrown) - assert(False) - - except ValueError: + with pytest.raises(ValueError): # The exception we expect since dict is not supported - assert(True) + a.color(NotifyType.INFO, dict) - except Exception: - # Any other exception is not good - assert(False) + assert a.image_url(NotifyType.INFO, NotifyImageSize.XY_256) == \ + 'http://localhost/dark/info-256x256.png' - assert(a.image_url(NotifyType.INFO, NotifyImageSize.XY_256) == - 'http://localhost/dark/info-256x256.png') - - assert(a.image_path( + assert a.image_path( NotifyType.INFO, NotifyImageSize.XY_256, - must_exist=False) == '/dark/info-256x256.png') + must_exist=False) == '/dark/info-256x256.png' # This path doesn't exist so image_raw will fail (since we just # randompyl picked it for testing) - assert(a.image_raw(NotifyType.INFO, NotifyImageSize.XY_256) is None) + assert a.image_raw(NotifyType.INFO, NotifyImageSize.XY_256) is None - assert(a.image_path( + assert a.image_path( NotifyType.INFO, NotifyImageSize.XY_256, - must_exist=True) is None) + must_exist=True) is None # Create a new object (with our default settings) a = AppriseAsset() # Our default configuration can access our file - assert(a.image_path( + assert a.image_path( NotifyType.INFO, NotifyImageSize.XY_256, - must_exist=True) is not None) + must_exist=True) is not None - assert(a.image_raw(NotifyType.INFO, NotifyImageSize.XY_256) is not None) + assert a.image_raw(NotifyType.INFO, NotifyImageSize.XY_256) is not None # Create a temporary directory sub = tmpdir.mkdir("great.theme") @@ -703,14 +701,14 @@ def test_apprise_asset(tmpdir): ) # We'll be able to read file we just created - assert(a.image_raw(NotifyType.INFO, NotifyImageSize.XY_256) is not None) + assert a.image_raw(NotifyType.INFO, NotifyImageSize.XY_256) is not None # We can retrieve the filename at this point even with must_exist set # to True - assert(a.image_path( + assert a.image_path( NotifyType.INFO, NotifyImageSize.XY_256, - must_exist=True) is not None) + must_exist=True) is not None # If we make the file un-readable however, we won't be able to read it # This test is just showing that we won't throw an exception @@ -720,37 +718,37 @@ def test_apprise_asset(tmpdir): pytest.skip('The Root user can not run file permission tests.') chmod(dirname(sub.strpath), 0o000) - assert(a.image_raw(NotifyType.INFO, NotifyImageSize.XY_256) is None) + assert a.image_raw(NotifyType.INFO, NotifyImageSize.XY_256) is None # Our path doesn't exist anymore using this logic - assert(a.image_path( + assert a.image_path( NotifyType.INFO, NotifyImageSize.XY_256, - must_exist=True) is None) + must_exist=True) is None # Return our permission so we don't have any problems with our cleanup chmod(dirname(sub.strpath), 0o700) # Our content is retrivable again - assert(a.image_raw(NotifyType.INFO, NotifyImageSize.XY_256) is not None) + assert a.image_raw(NotifyType.INFO, NotifyImageSize.XY_256) is not None # our file path is accessible again too - assert(a.image_path( + assert a.image_path( NotifyType.INFO, NotifyImageSize.XY_256, - must_exist=True) is not None) + must_exist=True) is not None # We do the same test, but set the permission on the file chmod(a.image_path(NotifyType.INFO, NotifyImageSize.XY_256), 0o000) # our path will still exist in this case - assert(a.image_path( + assert a.image_path( NotifyType.INFO, NotifyImageSize.XY_256, - must_exist=True) is not None) + must_exist=True) is not None # but we will not be able to open it - assert(a.image_raw(NotifyType.INFO, NotifyImageSize.XY_256) is None) + assert a.image_raw(NotifyType.INFO, NotifyImageSize.XY_256) is None # Restore our permissions chmod(a.image_path(NotifyType.INFO, NotifyImageSize.XY_256), 0o640) @@ -759,12 +757,12 @@ def test_apprise_asset(tmpdir): a = AppriseAsset(image_path_mask=False, image_url_mask=False) # We always return none in these calls now - assert(a.image_raw(NotifyType.INFO, NotifyImageSize.XY_256) is None) - assert(a.image_url(NotifyType.INFO, NotifyImageSize.XY_256) is None) - assert(a.image_path(NotifyType.INFO, NotifyImageSize.XY_256, - must_exist=False) is None) - assert(a.image_path(NotifyType.INFO, NotifyImageSize.XY_256, - must_exist=True) is None) + assert a.image_raw(NotifyType.INFO, NotifyImageSize.XY_256) is None + assert a.image_url(NotifyType.INFO, NotifyImageSize.XY_256) is None + assert a.image_path(NotifyType.INFO, NotifyImageSize.XY_256, + must_exist=False) is None + assert a.image_path(NotifyType.INFO, NotifyImageSize.XY_256, + must_exist=True) is None # Test our default extension out a = AppriseAsset( @@ -772,28 +770,28 @@ def test_apprise_asset(tmpdir): image_url_mask='http://localhost/{THEME}/{TYPE}-{XY}{EXTENSION}', default_extension='.jpeg', ) - assert(a.image_path( + assert a.image_path( NotifyType.INFO, NotifyImageSize.XY_256, - must_exist=False) == '/default/info-256x256.jpeg') + must_exist=False) == '/default/info-256x256.jpeg' - assert(a.image_url( + assert a.image_url( NotifyType.INFO, - NotifyImageSize.XY_256) == 'http://localhost/' - 'default/info-256x256.jpeg') + NotifyImageSize.XY_256) == \ + 'http://localhost/default/info-256x256.jpeg' # extension support - assert(a.image_path( + assert a.image_path( NotifyType.INFO, NotifyImageSize.XY_128, must_exist=False, - extension='.ico') == '/default/info-128x128.ico') + extension='.ico') == '/default/info-128x128.ico' - assert(a.image_url( + assert a.image_url( NotifyType.INFO, NotifyImageSize.XY_256, - extension='.test') == 'http://localhost/' - 'default/info-256x256.test') + extension='.test') == \ + 'http://localhost/default/info-256x256.test' def test_apprise_details(): @@ -894,6 +892,11 @@ def test_apprise_details(): # '_exists_if': 'always_true', }, + # alias_of testing + 'test_alias_of': { + 'alias_of': 'mylistB', + 'delim': ('-', ' ') + } }) def url(self): @@ -1116,10 +1119,11 @@ def test_apprise_details_plugin_verification(): assert '{} is an invalid regex'\ .format(arg['regex'][0]) - # Regex should never start and/or end with ^/$; leave - # that up to the user making use of the regex instead - assert re.match(r'^[()\s]*\^', arg['regex'][0]) is None - assert re.match(r'[()\s$]*\$', arg['regex'][0]) is None + # Regex should always start and/or end with ^/$ + assert re.match( + r'^\^.+?$', arg['regex'][0]) is not None + assert re.match( + r'^.+?\$$', arg['regex'][0]) is not None if arg['type'].startswith('list'): # Delimiters MUST be defined @@ -1132,8 +1136,55 @@ def test_apprise_details_plugin_verification(): assert isinstance(arg['alias_of'], six.string_types) # Track our alias_of object map_to_aliases.add(arg['alias_of']) - # 2 entries (name, and alias_of only!) - assert len(entry['details'][section][key]) == 1 + + # Ensure we're not already in the tokens section + # The alias_of object has no value here + assert section != 'tokens' + + # We can't be an alias_of ourselves + if key == arg['alias_of']: + # This is acceptable as long as we exist in the tokens + # table because that is truely what we map back to + assert key in entry['details']['tokens'] + + else: + # Throw the problem into an assert tag for debugging + # purposes... the mapping is not acceptable + assert key != arg['alias_of'] + + # alias_of always references back to tokens + assert \ + arg['alias_of'] in entry['details']['tokens'] or \ + arg['alias_of'] in entry['details']['args'] + + # Find a list directive in our tokens + t_match = entry['details']['tokens']\ + .get(arg['alias_of'], {})\ + .get('type', '').startswith('list') + + a_match = entry['details']['args']\ + .get(arg['alias_of'], {})\ + .get('type', '').startswith('list') + + if not (t_match or a_match): + # Ensure the only token we have is the alias_of + assert len(entry['details'][section][key]) == 1 + + else: + # We're a list, we allow up to 2 variables + # Obviously we have the alias_of entry; that's why + # were at this part of the code. But we can + # additionally provide a 'delim' over-ride. + assert len(entry['details'][section][key]) <= 2 + if len(entry['details'][section][key]) == 2: + # Verify that it is in fact the 'delim' tag + assert 'delim' in entry['details'][section][key] + # If we do have a delim value set, it must be of + # a list/set/tuple type + assert isinstance( + entry['details'][section][key]['delim'], + (tuple, set, list), + ) if six.PY2: # inspect our object diff --git a/test/test_config_base.py b/test/test_config_base.py index 6074224c..c6a6aa2b 100644 --- a/test/test_config_base.py +++ b/test/test_config_base.py @@ -25,6 +25,7 @@ import sys import six +import pytest from apprise.AppriseAsset import AppriseAsset from apprise.config.ConfigBase import ConfigBase from apprise.config import __load_matrix @@ -41,22 +42,12 @@ def test_config_base(): """ # invalid types throw exceptions - try: + with pytest.raises(TypeError): ConfigBase(**{'format': 'invalid'}) - # We should never reach here as an exception should be thrown - assert(False) - - except TypeError: - assert(True) # Config format types are not the same as ConfigBase ones - try: + with pytest.raises(TypeError): ConfigBase(**{'format': 'markdown'}) - # We should never reach here as an exception should be thrown - assert(False) - - except TypeError: - assert(True) cb = ConfigBase(**{'format': 'yaml'}) assert isinstance(cb, ConfigBase) diff --git a/test/test_email_plugin.py b/test/test_email_plugin.py index 55444041..76abbc89 100644 --- a/test/test_email_plugin.py +++ b/test/test_email_plugin.py @@ -274,17 +274,17 @@ def test_email_plugin(mock_smtp, mock_smtpssl): # Expected None but didn't get it print('%s instantiated %s (but expected None)' % ( url, str(obj))) - assert(False) + assert False - assert(isinstance(obj, instance)) + assert isinstance(obj, instance) if isinstance(obj, plugins.NotifyBase): # We loaded okay; now lets make sure we can reverse this url - assert(isinstance(obj.url(), six.string_types) is True) + assert isinstance(obj.url(), six.string_types) is True # Test url() with privacy=True - assert(isinstance( - obj.url(privacy=True), six.string_types) is True) + assert isinstance( + obj.url(privacy=True), six.string_types) is True # Some Simple Invalid Instance Testing assert instance.parse_url(None) is None @@ -307,14 +307,14 @@ def test_email_plugin(mock_smtp, mock_smtpssl): # assertion failure makes things easier to debug later on print('TEST FAIL: {} regenerated as {}'.format( url, obj.url())) - assert(False) + assert False 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) + assert hasattr(key, obj) + assert getattr(key, obj) == val try: if test_smtplib_exceptions is False: @@ -389,7 +389,7 @@ def test_webbase_lookup(mock_smtp, mock_smtpssl): obj = Apprise.instantiate( 'mailto://user:pass@l2g.com', suppress_exceptions=True) - assert(isinstance(obj, plugins.NotifyEmail)) + assert isinstance(obj, plugins.NotifyEmail) assert len(obj.targets) == 1 assert 'user@l2g.com' in obj.targets assert obj.from_addr == 'user@l2g.com' @@ -417,7 +417,7 @@ def test_smtplib_init_fail(mock_smtplib): obj = Apprise.instantiate( 'mailto://user:pass@gmail.com', suppress_exceptions=False) - assert(isinstance(obj, plugins.NotifyEmail)) + assert isinstance(obj, plugins.NotifyEmail) # Support Exception handling of smtplib.SMTP mock_smtplib.side_effect = RuntimeError('Test') @@ -443,7 +443,7 @@ def test_smtplib_send_okay(mock_smtplib): # Defaults to HTML obj = Apprise.instantiate( 'mailto://user:pass@gmail.com', suppress_exceptions=False) - assert(isinstance(obj, plugins.NotifyEmail)) + assert isinstance(obj, plugins.NotifyEmail) # Support an email simulation where we can correctly quit mock_smtplib.starttls.return_value = True @@ -451,16 +451,16 @@ def test_smtplib_send_okay(mock_smtplib): mock_smtplib.sendmail.return_value = True mock_smtplib.quit.return_value = True - assert(obj.notify( - body='body', title='test', notify_type=NotifyType.INFO) is True) + assert obj.notify( + body='body', title='test', notify_type=NotifyType.INFO) is True # Set Text obj = Apprise.instantiate( 'mailto://user:pass@gmail.com?format=text', suppress_exceptions=False) - assert(isinstance(obj, plugins.NotifyEmail)) + assert isinstance(obj, plugins.NotifyEmail) - assert(obj.notify( - body='body', title='test', notify_type=NotifyType.INFO) is True) + assert obj.notify( + body='body', title='test', notify_type=NotifyType.INFO) is True def test_email_url_escaping(): diff --git a/test/test_gitter_plugin.py b/test/test_gitter_plugin.py index 0a8bf779..3765b23a 100644 --- a/test/test_gitter_plugin.py +++ b/test/test_gitter_plugin.py @@ -26,7 +26,7 @@ import six import mock import requests from apprise import plugins -# from apprise import AppriseAsset + from json import dumps from datetime import datetime @@ -87,16 +87,6 @@ def test_notify_gitter_plugin_general(mock_post, mock_get): mock_get.return_value = request mock_post.return_value = request - # Variation Initializations (no token) - try: - obj = plugins.NotifyGitter(token=None, targets='apprise') - # No Token should throw an exception - assert False - - except TypeError: - # We should get here - assert True - # Variation Initializations obj = plugins.NotifyGitter(token=token, targets='apprise') assert isinstance(obj, plugins.NotifyGitter) is True @@ -181,8 +171,22 @@ def test_notify_gitter_plugin_general(mock_post, mock_get): assert obj.send(body="test") is True # Variation Initializations - obj = plugins.NotifyGitter(token=token, targets='missing') + obj = plugins.NotifyGitter(token=token, targets='apprise') assert isinstance(obj, plugins.NotifyGitter) is True assert isinstance(obj.url(), six.string_types) is True - # missing room was found + # apprise room was not found assert obj.send(body="test") is False + + # Test exception handling + mock_post.side_effect = \ + requests.ConnectionError(0, 'requests.ConnectionError()') + + # Create temporary _room_mapping object so we will find the apprise + # channel on our second call to send() + obj._room_mapping = { + 'apprise': { + 'id': '5c981cecd73408ce4fbbad31', + 'uri': 'apprise-notifications/community', + } + } + assert obj.send(body='test body', title='test title') is False diff --git a/test/test_glib_plugin.py b/test/test_glib_plugin.py index 1f86cdc2..e88ad9a7 100644 --- a/test/test_glib_plugin.py +++ b/test/test_glib_plugin.py @@ -136,141 +136,149 @@ def test_dbus_plugin(mock_mainloop, mock_byte, mock_bytearray, # Create our instance (identify all supported types) obj = apprise.Apprise.instantiate('dbus://', suppress_exceptions=False) - assert(isinstance(obj, apprise.plugins.NotifyDBus) is True) + assert isinstance(obj, apprise.plugins.NotifyDBus) is True obj = apprise.Apprise.instantiate('kde://', suppress_exceptions=False) - assert(isinstance(obj, apprise.plugins.NotifyDBus) is True) + assert isinstance(obj, apprise.plugins.NotifyDBus) is True obj = apprise.Apprise.instantiate('qt://', suppress_exceptions=False) - assert(isinstance(obj, apprise.plugins.NotifyDBus) is True) + assert isinstance(obj, apprise.plugins.NotifyDBus) is True obj = apprise.Apprise.instantiate('glib://', suppress_exceptions=False) - assert(isinstance(obj, apprise.plugins.NotifyDBus) is True) + assert isinstance(obj, apprise.plugins.NotifyDBus) is True obj.duration = 0 # Check that it found our mocked environments - assert(obj._enabled is True) + assert obj._enabled is True # Test our class loading using a series of arguments - try: + with pytest.raises(TypeError): apprise.plugins.NotifyDBus(**{'schema': 'invalid'}) - # We should not reach here as the invalid schema - # should force an exception - assert(False) - except TypeError: - # Expected behaviour - assert(True) # Invalid URLs assert apprise.plugins.NotifyDBus.parse_url('') is None # Set our X and Y coordinate and try the notification - assert( - apprise.plugins.NotifyDBus( - x_axis=0, y_axis=0, **{'schema': 'dbus'}) + assert apprise.plugins.NotifyDBus( + x_axis=0, y_axis=0, **{'schema': 'dbus'})\ .notify(title='', body='body', - notify_type=apprise.NotifyType.INFO) is True) + notify_type=apprise.NotifyType.INFO) is True # test notifications - assert(obj.notify(title='title', body='body', - notify_type=apprise.NotifyType.INFO) is True) + assert obj.notify( + title='title', body='body', + notify_type=apprise.NotifyType.INFO) is True # test notification without a title - assert(obj.notify(title='', body='body', - notify_type=apprise.NotifyType.INFO) is True) + assert obj.notify( + title='', body='body', + notify_type=apprise.NotifyType.INFO) is True # Test our arguments through the instantiate call obj = apprise.Apprise.instantiate( 'dbus://_/?image=True', suppress_exceptions=False) - assert(isinstance(obj, apprise.plugins.NotifyDBus) is True) - assert(isinstance(obj.url(), six.string_types) is True) - assert(obj.notify(title='title', body='body', - notify_type=apprise.NotifyType.INFO) is True) + assert isinstance(obj, apprise.plugins.NotifyDBus) is True + assert isinstance(obj.url(), six.string_types) is True + assert obj.notify( + title='title', body='body', + notify_type=apprise.NotifyType.INFO) is True obj = apprise.Apprise.instantiate( 'dbus://_/?image=False', suppress_exceptions=False) - assert(isinstance(obj, apprise.plugins.NotifyDBus) is True) - assert(isinstance(obj.url(), six.string_types) is True) - assert(obj.notify(title='title', body='body', - notify_type=apprise.NotifyType.INFO) is True) + assert isinstance(obj, apprise.plugins.NotifyDBus) is True + assert isinstance(obj.url(), six.string_types) is True + assert obj.notify( + title='title', body='body', + notify_type=apprise.NotifyType.INFO) is True # Test priority (alias to urgency) handling obj = apprise.Apprise.instantiate( 'dbus://_/?priority=invalid', suppress_exceptions=False) - assert(isinstance(obj, apprise.plugins.NotifyDBus) is True) - assert(isinstance(obj.url(), six.string_types) is True) - assert(obj.notify(title='title', body='body', - notify_type=apprise.NotifyType.INFO) is True) + assert isinstance(obj, apprise.plugins.NotifyDBus) is True + assert isinstance(obj.url(), six.string_types) is True + assert obj.notify( + title='title', body='body', + notify_type=apprise.NotifyType.INFO) is True obj = apprise.Apprise.instantiate( 'dbus://_/?priority=high', suppress_exceptions=False) - assert(isinstance(obj, apprise.plugins.NotifyDBus) is True) - assert(isinstance(obj.url(), six.string_types) is True) - assert(obj.notify(title='title', body='body', - notify_type=apprise.NotifyType.INFO) is True) + assert isinstance(obj, apprise.plugins.NotifyDBus) is True + assert isinstance(obj.url(), six.string_types) is True + assert obj.notify( + title='title', body='body', + notify_type=apprise.NotifyType.INFO) is True obj = apprise.Apprise.instantiate( 'dbus://_/?priority=2', suppress_exceptions=False) - assert(isinstance(obj, apprise.plugins.NotifyDBus) is True) - assert(isinstance(obj.url(), six.string_types) is True) - assert(obj.notify(title='title', body='body', - notify_type=apprise.NotifyType.INFO) is True) + assert isinstance(obj, apprise.plugins.NotifyDBus) is True + assert isinstance(obj.url(), six.string_types) is True + assert obj.notify( + title='title', body='body', + notify_type=apprise.NotifyType.INFO) is True # Test urgency handling obj = apprise.Apprise.instantiate( 'dbus://_/?urgency=invalid', suppress_exceptions=False) - assert(isinstance(obj, apprise.plugins.NotifyDBus) is True) - assert(isinstance(obj.url(), six.string_types) is True) - assert(obj.notify(title='title', body='body', - notify_type=apprise.NotifyType.INFO) is True) + assert isinstance(obj, apprise.plugins.NotifyDBus) is True + assert isinstance(obj.url(), six.string_types) is True + assert obj.notify( + title='title', body='body', + notify_type=apprise.NotifyType.INFO) is True obj = apprise.Apprise.instantiate( 'dbus://_/?urgency=high', suppress_exceptions=False) - assert(isinstance(obj, apprise.plugins.NotifyDBus) is True) - assert(isinstance(obj.url(), six.string_types) is True) - assert(obj.notify(title='title', body='body', - notify_type=apprise.NotifyType.INFO) is True) + assert isinstance(obj, apprise.plugins.NotifyDBus) is True + assert isinstance(obj.url(), six.string_types) is True + assert obj.notify( + title='title', body='body', + notify_type=apprise.NotifyType.INFO) is True obj = apprise.Apprise.instantiate( 'dbus://_/?urgency=2', suppress_exceptions=False) - assert(isinstance(obj, apprise.plugins.NotifyDBus) is True) - assert(isinstance(obj.url(), six.string_types) is True) - assert(obj.notify(title='title', body='body', - notify_type=apprise.NotifyType.INFO) is True) + assert isinstance(obj, apprise.plugins.NotifyDBus) is True + assert isinstance(obj.url(), six.string_types) is True + assert obj.notify( + title='title', body='body', + notify_type=apprise.NotifyType.INFO) is True obj = apprise.Apprise.instantiate( 'dbus://_/?urgency=', suppress_exceptions=False) - assert(isinstance(obj, apprise.plugins.NotifyDBus) is True) - assert(isinstance(obj.url(), six.string_types) is True) - assert(obj.notify(title='title', body='body', - notify_type=apprise.NotifyType.INFO) is True) + assert isinstance(obj, apprise.plugins.NotifyDBus) is True + assert isinstance(obj.url(), six.string_types) is True + assert obj.notify( + title='title', body='body', + notify_type=apprise.NotifyType.INFO) is True # Test x/y obj = apprise.Apprise.instantiate( 'dbus://_/?x=5&y=5', suppress_exceptions=False) - assert(isinstance(obj, apprise.plugins.NotifyDBus) is True) - assert(isinstance(obj.url(), six.string_types) is True) - assert(obj.notify(title='title', body='body', - notify_type=apprise.NotifyType.INFO) is True) + assert isinstance(obj, apprise.plugins.NotifyDBus) is True + assert isinstance(obj.url(), six.string_types) is True + assert obj.notify( + title='title', body='body', + notify_type=apprise.NotifyType.INFO) is True obj = apprise.Apprise.instantiate( 'dbus://_/?x=invalid&y=invalid', suppress_exceptions=False) - assert(isinstance(obj, apprise.plugins.NotifyDBus) is True) - assert(isinstance(obj.url(), six.string_types) is True) - assert(obj.notify(title='title', body='body', - notify_type=apprise.NotifyType.INFO) is True) + assert isinstance(obj, apprise.plugins.NotifyDBus) is True + assert isinstance(obj.url(), six.string_types) is True + assert obj.notify( + title='title', body='body', + notify_type=apprise.NotifyType.INFO) is True - # If our underlining object throws for whatever reason, we will + # If our underlining object throws for whatever rea on, we will # gracefully fail mock_notify = mock.Mock() mock_interface.return_value = mock_notify mock_notify.Notify.side_effect = AttributeError() - assert(obj.notify(title='', body='body', - notify_type=apprise.NotifyType.INFO) is False) + assert obj.notify( + title='', body='body', + notify_type=apprise.NotifyType.INFO) is False mock_notify.Notify.side_effect = None # Test our loading of our icon exception; it will still allow the # notification to be sent mock_pixbuf.new_from_file.side_effect = AttributeError() - assert(obj.notify(title='title', body='body', - notify_type=apprise.NotifyType.INFO) is True) + assert obj.notify( + title='title', body='body', + notify_type=apprise.NotifyType.INFO) is True # Undo our change mock_pixbuf.new_from_file.side_effect = None @@ -278,8 +286,9 @@ def test_dbus_plugin(mock_mainloop, mock_byte, mock_bytearray, # Toggle our testing for when we can't send notifications because the # package has been made unavailable to us obj._enabled = False - assert(obj.notify(title='title', body='body', - notify_type=apprise.NotifyType.INFO) is False) + assert obj.notify( + title='title', body='body', + notify_type=apprise.NotifyType.INFO) is False # Test the setting of a the urgency apprise.plugins.NotifyDBus(urgency=0) @@ -306,15 +315,16 @@ def test_dbus_plugin(mock_mainloop, mock_byte, mock_bytearray, # Create our instance obj = apprise.Apprise.instantiate('glib://', suppress_exceptions=False) - assert(isinstance(obj, apprise.plugins.NotifyDBus) is True) + assert isinstance(obj, apprise.plugins.NotifyDBus) is True obj.duration = 0 # Test url() call - assert(isinstance(obj.url(), six.string_types) is True) + assert isinstance(obj.url(), six.string_types) is True # Our notification succeeds even though the gi library was not loaded - assert(obj.notify(title='title', body='body', - notify_type=apprise.NotifyType.INFO) is True) + assert obj.notify( + title='title', body='body', + notify_type=apprise.NotifyType.INFO) is True # Verify this all works in the event a ValueError is also thronw # out of the call to gi.require_version() @@ -336,15 +346,16 @@ def test_dbus_plugin(mock_mainloop, mock_byte, mock_bytearray, # Create our instance obj = apprise.Apprise.instantiate('glib://', suppress_exceptions=False) - assert(isinstance(obj, apprise.plugins.NotifyDBus) is True) + assert isinstance(obj, apprise.plugins.NotifyDBus) is True obj.duration = 0 # Test url() call - assert(isinstance(obj.url(), six.string_types) is True) + assert isinstance(obj.url(), six.string_types) is True # Our notification succeeds even though the gi library was not loaded - assert(obj.notify(title='title', body='body', - notify_type=apprise.NotifyType.INFO) is True) + assert obj.notify( + title='title', body='body', + notify_type=apprise.NotifyType.INFO) is True # Force a global import error _session_bus = sys.modules['dbus'] @@ -358,15 +369,16 @@ def test_dbus_plugin(mock_mainloop, mock_byte, mock_bytearray, # Create our instance obj = apprise.Apprise.instantiate('glib://', suppress_exceptions=False) - assert(isinstance(obj, apprise.plugins.NotifyDBus) is True) + assert isinstance(obj, apprise.plugins.NotifyDBus) is True obj.duration = 0 # Test url() call - assert(isinstance(obj.url(), six.string_types) is True) + assert isinstance(obj.url(), six.string_types) is True # Our notification fail because the dbus library wasn't present - assert(obj.notify(title='title', body='body', - notify_type=apprise.NotifyType.INFO) is False) + assert obj.notify( + title='title', body='body', + notify_type=apprise.NotifyType.INFO) is False # Since playing with the sys.modules is not such a good idea, # let's just put it back now :) diff --git a/test/test_growl_plugin.py b/test/test_growl_plugin.py index 0325f79a..4c520a78 100644 --- a/test/test_growl_plugin.py +++ b/test/test_growl_plugin.py @@ -211,7 +211,7 @@ def test_growl_plugin(mock_gntp): try: obj = Apprise.instantiate(url, suppress_exceptions=False) - assert(exception is None) + assert exception is None if obj is None: # We're done @@ -219,17 +219,17 @@ def test_growl_plugin(mock_gntp): if instance is None: # Expected None but didn't get it - assert(False) + assert False - assert(isinstance(obj, instance) is True) + assert isinstance(obj, instance) is True if isinstance(obj, plugins.NotifyBase): # We loaded okay; now lets make sure we can reverse this url - assert(isinstance(obj.url(), six.string_types) is True) + assert isinstance(obj.url(), six.string_types) is True # Test our privacy=True flag - assert(isinstance( - obj.url(privacy=True), six.string_types) is True) + assert isinstance( + obj.url(privacy=True), six.string_types) is True # Instantiate the exact same object again using the URL from # the one that was already created properly @@ -243,14 +243,14 @@ def test_growl_plugin(mock_gntp): # assertion failure makes things easier to debug later on print('TEST FAIL: {} regenerated as {}'.format( url, obj.url())) - assert(False) + assert False 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) + assert hasattr(key, obj) + assert getattr(key, obj) == val try: if test_growl_notify_exceptions is False: @@ -292,5 +292,5 @@ def test_growl_plugin(mock_gntp): except Exception as e: # Handle our exception print('%s / %s' % (url, str(e))) - assert(exception is not None) - assert(isinstance(e, exception)) + assert exception is not None + assert isinstance(e, exception) diff --git a/test/test_notify_base.py b/test/test_notify_base.py index d483e070..9e779333 100644 --- a/test/test_notify_base.py +++ b/test/test_notify_base.py @@ -23,6 +23,7 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. import six +import pytest from datetime import datetime from datetime import timedelta @@ -43,22 +44,12 @@ def test_notify_base(): """ # invalid types throw exceptions - try: + with pytest.raises(TypeError): NotifyBase(**{'format': 'invalid'}) - # We should never reach here as an exception should be thrown - assert(False) - - except TypeError: - assert(True) # invalid types throw exceptions - try: + with pytest.raises(TypeError): NotifyBase(**{'overflow': 'invalid'}) - # We should never reach here as an exception should be thrown - assert(False) - - except TypeError: - assert(True) # Bad port information nb = NotifyBase(port='invalid') @@ -216,7 +207,7 @@ def test_notify_base(): # Test invalid data assert NotifyBase.parse_list(None) == [] - assert NotifyBase.parse_list(42) == ['42', ] + assert NotifyBase.parse_list(42) == [] result = NotifyBase.parse_list( ',path,?name=Dr%20Disrespect', unquote=False) diff --git a/test/test_rest_plugins.py b/test/test_rest_plugins.py index c8ce4e67..7c769a00 100644 --- a/test/test_rest_plugins.py +++ b/test/test_rest_plugins.py @@ -24,6 +24,7 @@ # THE SOFTWARE. import six +import pytest import requests import mock from json import dumps @@ -76,9 +77,12 @@ TEST_URLS = ( ('boxcar://%s' % ('a' * 64), { 'instance': TypeError, }), - # An invalid access and secret key specified - ('boxcar://access.key/secret.key/', { - # Thrown because there were no recipients specified + # No access specified (whitespace is trimmed) + ('boxcar://%%20/%s' % ('a' * 64), { + 'instance': TypeError, + }), + # No secret specified (whitespace is trimmed) + ('boxcar://%s/%%20' % ('a' * 64), { 'instance': TypeError, }), # Provide both an access and a secret @@ -134,11 +138,11 @@ TEST_URLS = ( # NotifyClickSend ################################## ('clicksend://', { - # No authentication + # We failed to identify any valid authentication 'instance': TypeError, }), ('clicksend://:@/', { - # invalid user/pass + # We failed to identify any valid authentication 'instance': TypeError, }), ('clicksend://user:pass@{}/{}/{}'.format('1' * 10, '2' * 15, 'a' * 13), { @@ -177,15 +181,15 @@ TEST_URLS = ( # NotifyD7Networks ################################## ('d7sms://', { - # No target numbers + # We failed to identify any valid authentication 'instance': TypeError, }), ('d7sms://:@/', { - # invalid user/pass + # We failed to identify any valid authentication 'instance': TypeError, }), ('d7sms://user:pass@{}/{}/{}'.format('1' * 10, '2' * 15, 'a' * 13), { - # invalid target numbers + # No valid targets to notify 'instance': TypeError, }), ('d7sms://user:pass@{}?batch=yes'.format('3' * 14), { @@ -502,14 +506,15 @@ TEST_URLS = ( ('flock://%s/g:%s/u:%s?format=text' % ('i' * 24, 'g' * 12, 'u' * 10), { 'instance': plugins.NotifyFlock, }), - # Bot API presumed if one or more targets are specified - # has all bad entries - ('flock://%s/g:%s/u:%s?format=text' % ('i' * 24, 'g' * 14, 'u' * 10), { + # Invalid user/group defined + ('flock://%s/g:/u:?format=text' % ('i' * 24), { 'instance': TypeError, }), - # Provide invalid token - ('flock://%s?format=text' % ('i' * 10), { - 'instance': TypeError, + # we don't focus on the invalid length of the user/group fields. + # As a result, the following will load and pass the data upstream + ('flock://%s/g:%s/u:%s?format=text' % ('i' * 24, 'g' * 14, 'u' * 10), { + # We will still instantiate the object + 'instance': plugins.NotifyFlock, }), # An invalid url ('flock://:@/', { @@ -550,54 +555,52 @@ TEST_URLS = ( ('gitter://:@/', { 'instance': None, }), - # Token specified but it's invalid + # Invalid Token Length ('gitter://%s' % ('a' * 12), { 'instance': TypeError, }), - # Token specified but no channel - still okay + # Token specified but no channel ('gitter://%s' % ('a' * 40), { - 'instance': plugins.NotifyGitter, - # our notify() will however return a False since it can't - # notify anything - 'response': False, + 'instance': TypeError, }), # Token + channel - ('gitter://%s/apprise' % ('a' * 40), { + ('gitter://%s/apprise' % ('b' * 40), { 'instance': plugins.NotifyGitter, - # don't include an image by default - 'include_image': False, - # This actually fails because the first thing we do is generate a list - # of channel's that we are a part of, and test_rest_plugins() won't - # be able to fulfill this task. Hence we'll get a list of no channels - # and having nothing to notify will give us a failed state: 'response': False, }), # include image in post - ('gitter://%s/apprise?image=Yes' % ('a' * 40), { + ('gitter://%s/apprise?image=Yes' % ('c' * 40), { 'instance': plugins.NotifyGitter, 'response': False, # Our expected url(privacy=True) startswith() response: - 'privacy_url': 'gitter://a...a/apprise', + 'privacy_url': 'gitter://c...c/apprise', }), # Don't include image in post (this is the default anyway) - ('gitter://%s/apprise?image=No' % ('a' * 40), { + ('gitter://%s/apprise?image=Yes' % ('d' * 40), { + 'instance': plugins.NotifyGitter, + 'response': False, + # don't include an image by default + 'include_image': False, + }), + # Don't include image in post (this is the default anyway) + ('gitter://%s/apprise?image=No' % ('e' * 40), { 'instance': plugins.NotifyGitter, 'response': False, }), - ('gitter://%s' % ('a' * 40), { + ('gitter://%s/apprise' % ('f' * 40), { 'instance': plugins.NotifyGitter, # force a failure 'response': False, 'requests_response_code': requests.codes.internal_server_error, }), - ('gitter://%s' % ('a' * 40), { + ('gitter://%s/apprise' % ('g' * 40), { 'instance': plugins.NotifyGitter, # throw a bizzare code forcing us to fail to look it up 'response': False, 'requests_response_code': 999, }), - ('gitter://%s' % ('a' * 40), { + ('gitter://%s/apprise' % ('h' * 40), { 'instance': plugins.NotifyGitter, # Throws a series of connection and transfer exceptions when this flag # is set and tests that we gracfully handle them @@ -722,19 +725,26 @@ TEST_URLS = ( ('join://%s' % ('a' * 32), { 'instance': plugins.NotifyJoin, }), - # Invalid APIKey - ('join://%s' % ('a' * 24), { - # Missing a channel - 'instance': TypeError, - }), - # APIKey + device (using to=) + # API Key + device (using to=) ('join://%s?to=%s' % ('a' * 32, 'd' * 32), { 'instance': plugins.NotifyJoin, # Our expected url(privacy=True) startswith() response: 'privacy_url': 'join://a...a/', }), - # APIKey + device + # API Key + priority setting + ('join://%s?priority=high' % ('a' * 32), { + 'instance': plugins.NotifyJoin, + }), + # API Key + invalid priority setting + ('join://%s?priority=invalid' % ('a' * 32), { + 'instance': plugins.NotifyJoin, + }), + # API Key + priority setting (empty) + ('join://%s?priority=' % ('a' * 32), { + 'instance': plugins.NotifyJoin, + }), + # API Key + device ('join://%s@%s?image=True' % ('a' * 32, 'd' * 32), { 'instance': plugins.NotifyJoin, }), @@ -742,28 +752,27 @@ TEST_URLS = ( ('join://%s@%s?image=False' % ('a' * 32, 'd' * 32), { 'instance': plugins.NotifyJoin, }), - # APIKey + device + # API Key + invalid device + ('join://%s/%s' % ('a' * 32, 'k' * 12), { + 'instance': TypeError, + }), + # API Key + device ('join://%s/%s' % ('a' * 32, 'd' * 32), { 'instance': plugins.NotifyJoin, # don't include an image by default 'include_image': False, }), - # APIKey + 2 devices + # API Key + 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 + # API Key + 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, - 'response': False, - }), - # APIKey + bad url + # API Key + bad url ('join://:@/', { 'instance': None, }), @@ -943,16 +952,8 @@ TEST_URLS = ( # The below errors because a second token wasn't found 'instance': None, }), - ('kumulos://invalid-api-key', { - # Invalid API Key - 'instance': TypeError, - }), - ('kumulos://{}'.format(UUID4), { - # Server Key not specified - 'instance': TypeError, - }), - ('kumulos://{}/{}/'.format(UUID4, 'a' * 26), { - # Invalid Server Key + ('kumulos://{}/'.format(UUID4), { + # No server key was specified 'instance': TypeError, }), ('kumulos://{}/{}/'.format(UUID4, 'w' * 36), { @@ -1179,6 +1180,13 @@ TEST_URLS = ( ('mmosts://', { 'instance': None, }), + ('mmost://:@/', { + 'instance': None, + }), + ('mmosts://localhost', { + # Thrown because there was no webhook id specified + 'instance': TypeError, + }), ('mmost://localhost/3ccdd113474722377935511fc85d3dd4', { 'instance': plugins.NotifyMatterMost, }), @@ -1224,17 +1232,6 @@ TEST_URLS = ( ('mmosts://localhost/////3ccdd113474722377935511fc85d3dd4///', { 'instance': plugins.NotifyMatterMost, }), - ('mmosts://localhost', { - # Thrown because there was no webhook id specified - 'instance': TypeError, - }), - ('mmost://localhost/bad-web-hook', { - # Thrown because the webhook is not in a valid format - 'instance': TypeError, - }), - ('mmost://:@/', { - 'instance': None, - }), ('mmost://localhost/3ccdd113474722377935511fc85d3dd4', { 'instance': plugins.NotifyMatterMost, # force a failure @@ -1279,18 +1276,6 @@ TEST_URLS = ( # Just 2 tokens provided 'instance': TypeError, }), - ('msteams://{}@{}/{}/{}?ta'.format(UUID4, UUID4, 'a' * 20, UUID4), { - # All tokens provided - invalid token 2 - 'instance': TypeError, - }), - ('msteams://{}@{}/{}/{}?tb'.format(UUID4, UUID4, 'a' * 32, 'abcd'), { - # All tokens provided - invalid token 3 - 'instance': TypeError, - }), - ('msteams://{}@{}/{}/{}?tb'.format('garbage', UUID4, 'a' * 32, 'abcd'), { - # All tokens provided - invalid token 1 - 'instance': TypeError, - }), ('msteams://{}@{}/{}/{}?t1'.format(UUID4, UUID4, 'a' * 32, UUID4), { # All tokens provided - we're good 'instance': plugins.NotifyMSTeams, @@ -1344,65 +1329,57 @@ TEST_URLS = ( # invalid Auth key 'instance': TypeError, }), - ('nexmo://{}@12345678'.format('a' * 8), { + ('nexmo://AC{}@12345678'.format('a' * 8), { # Just a key provided 'instance': TypeError, }), - ('nexmo://{}:{}@_'.format('a' * 8, 'b' * 16), { - # key and secret provided but invalid from - 'instance': TypeError, - }), - ('nexmo://{}:{}@{}'.format('a' * 23, 'b' * 16, '1' * 11), { - # key invalid and secret - 'instance': TypeError, - }), - ('nexmo://{}:{}@{}'.format('a' * 8, 'b' * 2, '2' * 11), { - # key and invalid secret - 'instance': TypeError, - }), - ('nexmo://{}:{}@{}'.format('a' * 8, 'b' * 16, '3' * 9), { + ('nexmo://AC{}:{}@{}'.format('a' * 8, 'b' * 16, '3' * 9), { # key and secret provided and from but invalid from no 'instance': TypeError, }), - ('nexmo://{}:{}@{}/?ttl=0'.format('a' * 8, 'b' * 16, '3' * 11), { + ('nexmo://AC{}:{}@{}/?ttl=0'.format('b' * 8, 'c' * 16, '3' * 11), { # Invalid ttl defined 'instance': TypeError, }), - ('nexmo://{}:{}@{}/123/{}/abcd/'.format( - 'a' * 8, 'b' * 16, '3' * 11, '9' * 15), { + ('nexmo://AC{}:{}@{}'.format('d' * 8, 'e' * 16, 'a' * 11), { + # Invalid source number + 'instance': TypeError, + }), + ('nexmo://AC{}:{}@{}/123/{}/abcd/'.format( + 'f' * 8, 'g' * 16, '3' * 11, '9' * 15), { # valid everything but target numbers 'instance': plugins.NotifyNexmo, # Our expected url(privacy=True) startswith() response: - 'privacy_url': 'nexmo://a...a:****@', + 'privacy_url': 'nexmo://A...f:****@', }), - ('nexmo://{}:{}@{}'.format('a' * 8, 'b' * 16, '5' * 11), { + ('nexmo://AC{}:{}@{}'.format('h' * 8, 'i' * 16, '5' * 11), { # using phone no with no target - we text ourselves in # this case 'instance': plugins.NotifyNexmo, }), - ('nexmo://_?key={}&secret={}&from={}'.format( + ('nexmo://_?key=AC{}&secret={}&from={}'.format( 'a' * 8, 'b' * 16, '5' * 11), { # use get args to acomplish the same thing 'instance': plugins.NotifyNexmo, }), - ('nexmo://_?key={}&secret={}&source={}'.format( + ('nexmo://_?key=AC{}&secret={}&source={}'.format( 'a' * 8, 'b' * 16, '5' * 11), { # use get args to acomplish the same thing (use source instead of from) 'instance': plugins.NotifyNexmo, }), - ('nexmo://_?key={}&secret={}&from={}&to={}'.format( + ('nexmo://_?key=AC{}&secret={}&from={}&to={}'.format( 'a' * 8, 'b' * 16, '5' * 11, '7' * 13), { # use to= 'instance': plugins.NotifyNexmo, }), - ('nexmo://{}:{}@{}'.format('a' * 8, 'b' * 16, '6' * 11), { + ('nexmo://AC{}:{}@{}'.format('a' * 8, 'b' * 16, '6' * 11), { 'instance': plugins.NotifyNexmo, # throw a bizzare code forcing us to fail to look it up 'response': False, 'requests_response_code': 999, }), - ('nexmo://{}:{}@{}'.format('a' * 8, 'b' * 16, '6' * 11), { + ('nexmo://AC{}:{}@{}'.format('a' * 8, 'b' * 16, '6' * 11), { 'instance': plugins.NotifyNexmo, # Throws a series of connection and transfer exceptions when this flag # is set and tests that we gracfully handle them @@ -1415,51 +1392,55 @@ TEST_URLS = ( ('prowl://', { 'instance': None, }), + # Invalid API Key + ('prowl://%s' % ('a' * 20), { + 'instance': TypeError, + }), + # Provider Key + ('prowl://%s/%s' % ('a' * 40, 'b' * 40), { + 'instance': plugins.NotifyProwl, + }), + # Invalid Provider Key + ('prowl://%s/%s' % ('a' * 40, 'b' * 20), { + 'instance': TypeError, + }), # APIkey; no device ('prowl://%s' % ('a' * 40), { 'instance': plugins.NotifyProwl, }), - # Invalid APIKey - ('prowl://%s' % ('a' * 24), { - 'instance': TypeError, - }), - # APIKey + # API Key ('prowl://%s' % ('a' * 40), { 'instance': plugins.NotifyProwl, # don't include an image by default 'include_image': False, }), - # APIKey + priority setting + # API Key + priority setting ('prowl://%s?priority=high' % ('a' * 40), { 'instance': plugins.NotifyProwl, }), - # APIKey + invalid priority setting + # API Key + invalid priority setting ('prowl://%s?priority=invalid' % ('a' * 40), { 'instance': plugins.NotifyProwl, }), - # APIKey + priority setting (empty) + # API Key + priority setting (empty) ('prowl://%s?priority=' % ('a' * 40), { 'instance': plugins.NotifyProwl, }), - # APIKey + Invalid Provider Key - ('prowl://%s/%s' % ('a' * 40, 'b' * 24), { - 'instance': TypeError, - }), - # APIKey + No Provider Key (empty) + # API Key + No Provider Key (empty) ('prowl://%s///' % ('w' * 40), { 'instance': plugins.NotifyProwl, # Our expected url(privacy=True) startswith() response: 'privacy_url': 'prowl://w...w/', }), - # APIKey + Provider Key + # API Key + Provider Key ('prowl://%s/%s' % ('a' * 40, 'b' * 40), { 'instance': plugins.NotifyProwl, # Our expected url(privacy=True) startswith() response: 'privacy_url': 'prowl://a...a/b...b', }), - # APIKey + with image + # API Key + with image ('prowl://%s' % ('a' * 40), { 'instance': plugins.NotifyProwl, }), @@ -1496,38 +1477,38 @@ TEST_URLS = ( ('pbul://%s' % ('a' * 32), { 'instance': plugins.NotifyPushBullet, }), - # APIKey + channel + # API Key + channel ('pbul://%s/#channel/' % ('a' * 32), { 'instance': plugins.NotifyPushBullet, }), - # APIKey + channel (via to= + # API Key + channel (via to= ('pbul://%s/?to=#channel' % ('a' * 32), { 'instance': plugins.NotifyPushBullet, }), - # APIKey + 2 channels + # API Key + 2 channels ('pbul://%s/#channel1/#channel2' % ('a' * 32), { 'instance': plugins.NotifyPushBullet, # Our expected url(privacy=True) startswith() response: 'privacy_url': 'pbul://a...a/', }), - # APIKey + device + # API Key + device ('pbul://%s/device/' % ('a' * 32), { 'instance': plugins.NotifyPushBullet, }), - # APIKey + 2 devices + # API Key + 2 devices ('pbul://%s/device1/device2/' % ('a' * 32), { 'instance': plugins.NotifyPushBullet, }), - # APIKey + email + # API Key + email ('pbul://%s/user@example.com/' % ('a' * 32), { 'instance': plugins.NotifyPushBullet, }), - # APIKey + 2 emails + # API Key + 2 emails ('pbul://%s/user@example.com/abc@def.com/' % ('a' * 32), { 'instance': plugins.NotifyPushBullet, }), - # APIKey + Combo + # API Key + Combo ('pbul://%s/device/#channel/user@example.com/' % ('a' * 32), { 'instance': plugins.NotifyPushBullet, }), @@ -1576,10 +1557,10 @@ TEST_URLS = ( # NotifyTechulusPush ################################## ('push://', { - # Missing APIKey + # Missing API Key 'instance': TypeError, }), - # Invalid APIKey + # Invalid API Key ('push://%s' % ('+' * 24), { 'instance': TypeError, }), @@ -1590,7 +1571,7 @@ TEST_URLS = ( # Our expected url(privacy=True) startswith() response: 'privacy_url': 'push://8...2/', }), - # APIKey + bad url + # API Key + bad url ('push://:@/', { 'instance': TypeError, }), @@ -1634,14 +1615,14 @@ TEST_URLS = ( # Application Key+Secret + channel (via to=) ('pushed://%s/%s?to=channel' % ('a' * 32, 'a' * 64), { 'instance': plugins.NotifyPushed, - }), - # Application Key+Secret + dropped entry - ('pushed://%s/%s/dropped/' % ('a' * 32, 'a' * 64), { - 'instance': plugins.NotifyPushed, - # Our expected url(privacy=True) startswith() response: 'privacy_url': 'pushed://a...a/****/', }), + # Application Key+Secret + dropped entry + ('pushed://%s/%s/dropped_value/' % ('a' * 32, 'a' * 64), { + # No entries validated is a fail + 'instance': TypeError, + }), # Application Key+Secret + 2 channels ('pushed://%s/%s/#channel1/#channel2' % ('a' * 32, 'a' * 64), { 'instance': plugins.NotifyPushed, @@ -1785,101 +1766,93 @@ TEST_URLS = ( ('pover://%s' % ('a' * 30), { 'instance': TypeError, }), - # APIkey; invalid user - ('pover://%s@%s' % ('u' * 20, 'a' * 30), { - 'instance': TypeError, - }), - # Invalid APIKey; valid User - ('pover://%s@%s' % ('u' * 30, 'a' * 24), { - 'instance': TypeError, - }), - # APIKey + invalid sound setting + # API Key + invalid sound setting ('pover://%s@%s?sound=invalid' % ('u' * 30, 'a' * 30), { 'instance': TypeError, }), - # APIKey + valid alternate sound picked + # API Key + valid alternate sound picked ('pover://%s@%s?sound=spacealarm' % ('u' * 30, 'a' * 30), { 'instance': plugins.NotifyPushover, }), - # APIKey + Valid User + # API Key + 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 + # API Key + Valid User + 1 Device ('pover://%s@%s/DEVICE' % ('u' * 30, 'a' * 30), { 'instance': plugins.NotifyPushover, }), - # APIKey + Valid User + 1 Device (via to=) + # API Key + Valid User + 1 Device (via to=) ('pover://%s@%s?to=DEVICE' % ('u' * 30, 'a' * 30), { 'instance': plugins.NotifyPushover, }), - # APIKey + Valid User + 2 Devices + # API Key + Valid User + 2 Devices ('pover://%s@%s/DEVICE1/DEVICE2/' % ('u' * 30, 'a' * 30), { 'instance': plugins.NotifyPushover, # Our expected url(privacy=True) startswith() response: 'privacy_url': 'pover://u...u@a...a', }), - # APIKey + Valid User + invalid device + # API Key + 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 + # API Key + 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 + # API Key + priority setting ('pover://%s@%s?priority=high' % ('u' * 30, 'a' * 30), { 'instance': plugins.NotifyPushover, }), - # APIKey + invalid priority setting + # API Key + invalid priority setting ('pover://%s@%s?priority=invalid' % ('u' * 30, 'a' * 30), { 'instance': plugins.NotifyPushover, }), - # APIKey + emergency(2) priority setting + # API Key + emergency(2) priority setting ('pover://%s@%s?priority=emergency' % ('u' * 30, 'a' * 30), { 'instance': plugins.NotifyPushover, }), - # APIKey + emergency priority setting with retry and expire + # API Key + emergency priority setting with retry and expire ('pover://%s@%s?priority=emergency&%s&%s' % ('u' * 30, 'a' * 30, 'retry=30', 'expire=300'), { 'instance': plugins.NotifyPushover, }), - # APIKey + emergency priority setting with text retry + # API Key + emergency priority setting with text retry ('pover://%s@%s?priority=emergency&%s&%s' % ('u' * 30, 'a' * 30, 'retry=invalid', 'expire=300'), { 'instance': plugins.NotifyPushover, }), - # APIKey + emergency priority setting with text expire + # API Key + emergency priority setting with text expire ('pover://%s@%s?priority=emergency&%s&%s' % ('u' * 30, 'a' * 30, 'retry=30', 'expire=invalid'), { 'instance': plugins.NotifyPushover, }), - # APIKey + emergency priority setting with invalid expire + # API Key + emergency priority setting with invalid expire ('pover://%s@%s?priority=emergency&%s' % ('u' * 30, 'a' * 30, 'expire=100000'), { 'instance': TypeError, }), - # APIKey + emergency priority setting with invalid retry + # API Key + emergency priority setting with invalid retry ('pover://%s@%s?priority=emergency&%s' % ('u' * 30, 'a' * 30, 'retry=15'), { 'instance': TypeError, }), - # APIKey + priority setting (empty) + # API Key + priority setting (empty) ('pover://%s@%s?priority=' % ('u' * 30, 'a' * 30), { 'instance': plugins.NotifyPushover, }), @@ -2101,23 +2074,14 @@ TEST_URLS = ( # Just org provided (no token) 'instance': TypeError, }), - ('ryver://abc,#/ckhrjW8w672m6HG', { - # Invalid org provided (this isn't actually even a value url) - # because the hostname has ,# in it - 'instance': None, - }), - ('ryver://a/ckhrjW8w672m6HG', { - # org is too short - 'instance': TypeError, - }), - ('ryver://apprise/ckhrjW8w67HG', { - # Invalid token specified - 'instance': TypeError, - }), ('ryver://apprise/ckhrjW8w672m6HG?webhook=invalid', { # Invalid webhook provided 'instance': TypeError, }), + ('ryver://x/ckhrjW8w672m6HG?mode=slack', { + # Invalid org + 'instance': TypeError, + }), ('ryver://apprise/ckhrjW8w672m6HG?mode=slack', { # No username specified; this is still okay as we use whatever # the user told the webhook to use; set our slack mode @@ -2417,19 +2381,15 @@ TEST_URLS = ( 'instance': TypeError, }), ('sns://T1JJ3T3L2/A1BRTD4JD/TIiajkdnlazkcevi7FQ/us-west-2/12223334444', { - # we have a valid URL here + # we have a valid URL and one number to text 'instance': plugins.NotifySNS, }), ('sns://T1JJ3TD4JD/TIiajkdnlazk7FQ/us-west-2/12223334444/12223334445', { # Multi SNS Suppport 'instance': plugins.NotifySNS, - }), - ('sns://T1JJ3T3L2/A1BRTD4JD/TIiajkdnlazkcOXrIdevi7FQ/us-east-1', { - # Missing a topic and/or phone No - 'instance': plugins.NotifySNS, # Our expected url(privacy=True) startswith() response: - 'privacy_url': 'sns://T...2/****/us-east-1', + 'privacy_url': 'sns://T...D/****/us-west-2', }), ('sns://T1JJ3T3L2/A1BRTD4JD/TIiajkdnlazkcOXrIdevi7FQ/us-east-1' \ '?to=12223334444', { @@ -2611,16 +2571,9 @@ TEST_URLS = ( # sid and token provided but invalid from 'instance': TypeError, }), - ('twilio://AC{}:{}@{}'.format('a' * 23, 'b' * 32, '1' * 11), { - # sid invalid and token - 'instance': TypeError, - }), - ('twilio://AC{}:{}@{}'.format('a' * 32, 'b' * 23, '2' * 11), { - # sid and invalid token - 'instance': TypeError, - }), ('twilio://AC{}:{}@{}'.format('a' * 32, 'b' * 32, '3' * 5), { # using short-code (5 characters) without a target + # We can still instantiate ourselves with a valid short code 'instance': TypeError, }), ('twilio://AC{}:{}@{}'.format('a' * 32, 'b' * 32, '3' * 9), { @@ -2895,25 +2848,16 @@ TEST_URLS = ( 'instance': None, }), ('msg91://{}'.format('a' * 23), { - # valid everything but target numbers - 'instance': plugins.NotifyMSG91, - # Expected notify() response False because we have no numbers to - # notify - 'notify_response': False, + # No number specified + 'instance': TypeError, }), ('msg91://{}/123'.format('a' * 23), { # invalid phone number - 'instance': plugins.NotifyMSG91, - # Expected notify() response False because we have no numbers to - # notify - 'notify_response': False, + 'instance': TypeError, }), ('msg91://{}/abcd'.format('a' * 23), { - # invalid phone number - 'instance': plugins.NotifyMSG91, - # Expected notify() response False because we have no numbers to - # notify - 'notify_response': False, + # No number to notify + 'instance': TypeError, }), ('msg91://{}/15551232000/?country=invalid'.format('a' * 23), { # invalid country @@ -2971,35 +2915,30 @@ TEST_URLS = ( # No hostname/apikey specified 'instance': None, }), - ('msgbird://{}/15551232000'.format('a' * 10), { - # invalid apikey + ('msgbird://{}/abcd'.format('a' * 25), { + # invalid characters in source phone number 'instance': TypeError, }), ('msgbird://{}/123'.format('a' * 25), { - # invalid phone number - 'instance': TypeError, - }), - ('msgbird://{}/abc'.format('a' * 25), { - # invalid phone number + # invalid source phone number 'instance': TypeError, }), ('msgbird://{}/15551232000'.format('a' * 25), { # target phone number becomes who we text too; all is good 'instance': plugins.NotifyMessageBird, - }), - ('msgbird://{}/15551232000/abcd'.format('a' * 25), { - # invalid target phone number; we fall back to texting ourselves - 'instance': plugins.NotifyMessageBird, - # Our expected url(privacy=True) startswith() response: 'privacy_url': 'msgbird://a...a/15551232000', }), + ('msgbird://{}/15551232000/abcd'.format('a' * 25), { + # invalid target phone number; we have no one to notify + 'instance': TypeError, + }), ('msgbird://{}/15551232000/123'.format('a' * 25), { - # invalid target phone number; we fall back to texting ourselves - 'instance': plugins.NotifyMessageBird, + # invalid target phone number + 'instance': TypeError, }), ('msgbird://{}/?from=15551233000&to=15551232000'.format('a' * 25), { - # reference to to= and frome= + # reference to to= and from= 'instance': plugins.NotifyMessageBird, }), ('msgbird://{}/15551232000'.format('a' * 25), { @@ -3034,10 +2973,6 @@ TEST_URLS = ( # The below errors because a second token wasn't found 'instance': TypeError, }), - ('wxteams://{}'.format('a' * 40), { - # Just half of one token 1 provided - 'instance': TypeError, - }), ('wxteams://{}'.format('a' * 80), { # token provided - we're good 'instance': plugins.NotifyWebexTeams, @@ -3544,7 +3479,7 @@ def test_rest_plugins(mock_post, mock_get): except Exception as e: # Handle our exception - if(instance is None): + if instance is None: print('%s %s' % (url, str(e))) raise @@ -3574,31 +3509,12 @@ def test_notify_boxcar_plugin(mock_post, mock_get): plugins.NotifyBoxcar(access=access, secret=secret, targets=None) # Initializes the plugin with a valid access, but invalid access key - try: + with pytest.raises(TypeError): plugins.NotifyBoxcar(access=None, secret=secret, targets=None) - assert False - - except TypeError: - # We should throw an exception for knowingly having an invalid - assert True - - # Initializes the plugin with a valid access, but invalid secret key - try: - plugins.NotifyBoxcar(access=access, secret='invalid', targets=None) - assert False - - except TypeError: - # We should throw an exception for knowingly having an invalid key - assert True # Initializes the plugin with a valid access, but invalid secret - try: + with pytest.raises(TypeError): plugins.NotifyBoxcar(access=access, secret=None, targets=None) - assert False - - except TypeError: - # We should throw an exception for knowingly having an invalid - assert True # Initializes the plugin with recipients list # the below also tests our the variation of recipient types @@ -3644,14 +3560,19 @@ def test_notify_discord_plugin(mock_post, mock_get): mock_post.return_value.status_code = requests.codes.ok mock_get.return_value.status_code = requests.codes.ok - # Empty Channel list - try: + # Invalid webhook id + with pytest.raises(TypeError): plugins.NotifyDiscord(webhook_id=None, webhook_token=webhook_token) - assert False + # Invalid webhook id (whitespace) + with pytest.raises(TypeError): + plugins.NotifyDiscord(webhook_id=" ", webhook_token=webhook_token) - except TypeError: - # we'll thrown because no webhook_id was specified - assert True + # Invalid webhook token + with pytest.raises(TypeError): + plugins.NotifyDiscord(webhook_id=webhook_id, webhook_token=None) + # Invalid webhook token (whitespace) + with pytest.raises(TypeError): + plugins.NotifyDiscord(webhook_id=webhook_id, webhook_token=" ") obj = plugins.NotifyDiscord( webhook_id=webhook_id, @@ -3953,6 +3874,124 @@ def test_notify_emby_plugin_sessions(mock_post, mock_get, mock_logout, assert len(sessions) == 0 +@mock.patch('requests.get') +@mock.patch('requests.post') +def test_notify_flock_plugin(mock_post, mock_get): + """ + API: NotifyFlock() Extra Checks + + """ + # Disable Throttling to speed testing + plugins.NotifyBase.request_rate_per_sec = 0 + + # Initializes the plugin with an invalid token + with pytest.raises(TypeError): + plugins.NotifyFlock(token=None) + # Whitespace also acts as an invalid token value + with pytest.raises(TypeError): + plugins.NotifyFlock(token=" ") + + +def test_notify_gitter_plugin(): + """ + API: NotifyGitter() Extra Checks + + """ + # Define our channels + targets = ['apprise'] + + # Initializes the plugin with an invalid token + with pytest.raises(TypeError): + plugins.NotifyGitter(token=None, targets=targets) + # Whitespace also acts as an invalid token value + with pytest.raises(TypeError): + plugins.NotifyGitter(token=" ", targets=targets) + + +def test_notify_gotify_plugin(): + """ + API: NotifyGotify() Extra Checks + + """ + # Initializes the plugin with an invalid token + with pytest.raises(TypeError): + plugins.NotifyGotify(token=None) + # Whitespace also acts as an invalid token value + with pytest.raises(TypeError): + plugins.NotifyGotify(token=" ") + + +@mock.patch('requests.post') +def test_notify_msg91_plugin(mock_post): + """ + API: NotifyMSG91() Extra Checks + + """ + # Disable Throttling to speed testing + plugins.NotifyBase.request_rate_per_sec = 0 + + # Prepare our response + response = requests.Request() + response.status_code = requests.codes.ok + + # Prepare Mock + mock_post.return_value = response + + # Initialize some generic (but valid) tokens + # authkey = '{}'.format('a' * 24) + target = '+1 (555) 123-3456' + + # No authkey specified + with pytest.raises(TypeError): + plugins.NotifyMSG91(authkey=None, targets=target) + with pytest.raises(TypeError): + plugins.NotifyMSG91(authkey=" ", targets=target) + + +def test_notify_msteams_plugin(): + """ + API: NotifyMSTeams() Extra Checks + + """ + # Initializes the plugin with an invalid token + with pytest.raises(TypeError): + plugins.NotifyMSTeams(token_a=None, token_b='abcd', token_c='abcd') + # Whitespace also acts as an invalid token value + with pytest.raises(TypeError): + plugins.NotifyMSTeams(token_a=' ', token_b='abcd', token_c='abcd') + + with pytest.raises(TypeError): + plugins.NotifyMSTeams(token_a='abcd', token_b=None, token_c='abcd') + # Whitespace also acts as an invalid token value + with pytest.raises(TypeError): + plugins.NotifyMSTeams(token_a='abcd', token_b=' ', token_c='abcd') + + with pytest.raises(TypeError): + plugins.NotifyMSTeams(token_a='abcd', token_b='abcd', token_c=None) + # Whitespace also acts as an invalid token value + with pytest.raises(TypeError): + plugins.NotifyMSTeams(token_a='abcd', token_b='abcd', token_c=' ') + + +def test_notify_prowl_plugin(): + """ + API: NotifyProwl() Extra Checks + + """ + # Initializes the plugin with an invalid apikey + with pytest.raises(TypeError): + plugins.NotifyProwl(apikey=None) + # Whitespace also acts as an invalid apikey value + with pytest.raises(TypeError): + plugins.NotifyProwl(apikey=' ') + + # Whitespace also acts as an invalid provider key + with pytest.raises(TypeError): + plugins.NotifyProwl(apikey='abcd', providerkey=object()) + with pytest.raises(TypeError): + plugins.NotifyProwl(apikey='abcd', providerkey=' ') + + @mock.patch('requests.post') def test_notify_twilio_plugin(mock_post): """ @@ -3974,27 +4013,15 @@ def test_notify_twilio_plugin(mock_post): auth_token = '{}'.format('b' * 32) source = '+1 (555) 123-3456' - try: + # No account_sid specified + with pytest.raises(TypeError): plugins.NotifyTwilio( account_sid=None, auth_token=auth_token, source=source) - # No account_sid specified - assert False - except TypeError: - # Exception should be thrown about the fact the account_sid was not - # specified - assert True - - try: + # No auth_token specified + with pytest.raises(TypeError): plugins.NotifyTwilio( account_sid=account_sid, auth_token=None, source=source) - # No account_sid specified - assert False - - except TypeError: - # Exception should be thrown about the fact the auth_token was not - # specified - assert True # a error response response.status_code = 400 @@ -4029,31 +4056,23 @@ def test_notify_nexmo_plugin(mock_post): mock_post.return_value = response # Initialize some generic (but valid) tokens - apikey = '{}'.format('b' * 8) + apikey = 'AC{}'.format('b' * 8) secret = '{}'.format('b' * 16) source = '+1 (555) 123-3456' - try: - plugins.NotifyNexmo( - apikey=None, secret=secret, source=source) - # No apikey specified - assert False + # No apikey specified + with pytest.raises(TypeError): + plugins.NotifyNexmo(apikey=None, secret=secret, source=source) - except TypeError: - # Exception should be thrown about the fact the apikey was not - # specified - assert True + with pytest.raises(TypeError): + plugins.NotifyNexmo(apikey=" ", secret=secret, source=source) - try: - plugins.NotifyNexmo( - apikey=apikey, secret=None, source=source) - # No secret specified - assert False + # No secret specified + with pytest.raises(TypeError): + plugins.NotifyNexmo(apikey=apikey, secret=None, source=source) - except TypeError: - # Exception should be thrown about the fact the secret was not - # specified - assert True + with pytest.raises(TypeError): + plugins.NotifyNexmo(apikey=apikey, secret=" ", source=source) # a error response response.status_code = 400 @@ -4071,78 +4090,6 @@ def test_notify_nexmo_plugin(mock_post): assert obj.notify('title', 'body', 'info') is False -@mock.patch('requests.post') -def test_notify_msg91_plugin(mock_post): - """ - API: NotifyMSG91() Extra Checks - - """ - # Disable Throttling to speed testing - plugins.NotifyBase.request_rate_per_sec = 0 - - # Prepare our response - response = requests.Request() - response.status_code = requests.codes.ok - - # Prepare Mock - mock_post.return_value = response - - # Initialize some generic (but valid) tokens - # authkey = '{}'.format('a' * 24) - target = '+1 (555) 123-3456' - - try: - # No authkey specified - plugins.NotifyMSG91(authkey=None, targets=target) - assert False - - except TypeError: - # Exception should be thrown about the fact the authkey was not - # specified - assert True - - try: - # invalid authkey - plugins.NotifyMSG91(authkey='!#$%', targets=target) - assert False - - except TypeError: - # Exception should be thrown about the fact the authkey was - # invalid - assert True - - -@mock.patch('requests.post') -def test_notify_messagebird_plugin(mock_post): - """ - API: NotifyMessageBird() Extra Checks - - """ - # Disable Throttling to speed testing - plugins.NotifyBase.request_rate_per_sec = 0 - - # Prepare our response - response = requests.Request() - response.status_code = requests.codes.ok - - # Prepare Mock - mock_post.return_value = response - - # Initialize some generic (but valid) tokens - # authkey = '{}'.format('a' * 24) - source = '+1 (555) 123-3456' - - try: - # No authkey specified - plugins.NotifyMessageBird(apikey=None, source=source) - assert False - - except TypeError: - # Exception should be thrown about the fact authkey was not - # specified - assert True - - @mock.patch('apprise.plugins.NotifyEmby.login') @mock.patch('requests.get') @mock.patch('requests.post') @@ -4322,24 +4269,24 @@ def test_notify_ifttt_plugin(mock_post, mock_get): mock_get.return_value.content = '{}' mock_post.return_value.content = '{}' - try: - obj = plugins.NotifyIFTTT(webhook_id=None, events=None) - # No webhook_id specified - assert False + # No webhook_id specified + with pytest.raises(TypeError): + plugins.NotifyIFTTT(webhook_id=None, events=None) - except TypeError: - # Exception should be thrown about the fact the webhook_id was - # specified - assert True + # Disable Throttling to speed testing + plugins.NotifyBase.request_rate_per_sec = 0 - try: - obj = plugins.NotifyIFTTT(webhook_id=webhook_id, events=None) - # No events specified - assert False + # Initializes the plugin with an invalid webhook id + with pytest.raises(TypeError): + plugins.NotifyIFTTT(webhook_id=None, events=events) - except TypeError: - # Exception should be thrown about the fact no events were specified - assert True + # Whitespace also acts as an invalid webhook id + with pytest.raises(TypeError): + plugins.NotifyIFTTT(webhook_id=" ", events=events) + + # No events specified + with pytest.raises(TypeError): + plugins.NotifyIFTTT(webhook_id=webhook_id, events=None) obj = plugins.NotifyIFTTT(webhook_id=webhook_id, events=events) assert isinstance(obj, plugins.NotifyIFTTT) is True @@ -4357,20 +4304,12 @@ def test_notify_ifttt_plugin(mock_post, mock_get): assert obj.notify( body='body', title='title', notify_type=NotifyType.INFO) is True - try: - # Invalid del_tokens entry - obj = plugins.NotifyIFTTT( + # Invalid del_tokens entry + with pytest.raises(TypeError): + plugins.NotifyIFTTT( webhook_id=webhook_id, events=events, del_tokens=plugins.NotifyIFTTT.ifttt_default_title_key) - # we shouldn't reach here - assert False - - except TypeError: - # del_tokens must be a list, so passing a string will throw - # an exception. - assert True - assert isinstance(obj, plugins.NotifyIFTTT) is True assert obj.notify( @@ -4406,24 +4345,6 @@ def test_notify_ifttt_plugin(mock_post, mock_get): assert isinstance(obj, plugins.NotifyIFTTT) is True -def test_notify_kumulos_plugin(): - """ - API: NotifyKumulos() Extra Checks - - """ - # Disable Throttling to speed testing - plugins.NotifyBase.request_rate_per_sec = 0 - - # Invalid API Key - try: - plugins.NotifyKumulos(None, None) - assert False - - except TypeError: - # we'll thrown because an empty list of channels was provided - assert True - - @mock.patch('requests.get') @mock.patch('requests.post') def test_notify_join_plugin(mock_post, mock_get): @@ -4445,6 +4366,14 @@ def test_notify_join_plugin(mock_post, mock_get): # Initializes the plugin with devices set to None plugins.NotifyJoin(apikey=apikey, targets=None) + # Initializes the plugin with an invalid apikey + with pytest.raises(TypeError): + plugins.NotifyJoin(apikey=None) + + # Whitespace also acts as an invalid apikey + with pytest.raises(TypeError): + plugins.NotifyJoin(apikey=" ") + # Initializes the plugin with devices set to a set p = plugins.NotifyJoin(apikey=apikey, targets=[group, device]) @@ -4460,6 +4389,69 @@ def test_notify_join_plugin(mock_post, mock_get): p.notify(body=None, title=None, notify_type=NotifyType.INFO) is False +def test_notify_kumulos_plugin(): + """ + API: NotifyKumulos() Extra Checks + + """ + # Disable Throttling to speed testing + plugins.NotifyBase.request_rate_per_sec = 0 + + # Invalid API Key + with pytest.raises(TypeError): + plugins.NotifyKumulos(None, None) + with pytest.raises(TypeError): + plugins.NotifyKumulos(" ", None) + + # Invalid Server Key + with pytest.raises(TypeError): + plugins.NotifyKumulos("abcd", None) + with pytest.raises(TypeError): + plugins.NotifyKumulos("abcd", " ") + + +def test_notify_mattermost_plugin(): + """ + API: NotifyMatterMost() Extra Checks + + """ + # Disable Throttling to speed testing + plugins.NotifyBase.request_rate_per_sec = 0 + + # Invalid Authorization Token + with pytest.raises(TypeError): + plugins.NotifyMatterMost(None) + with pytest.raises(TypeError): + plugins.NotifyMatterMost(" ") + + +@mock.patch('requests.post') +def test_notify_messagebird_plugin(mock_post): + """ + API: NotifyMessageBird() Extra Checks + + """ + # Disable Throttling to speed testing + plugins.NotifyBase.request_rate_per_sec = 0 + + # Prepare our response + response = requests.Request() + response.status_code = requests.codes.ok + + # Prepare Mock + mock_post.return_value = response + + # Initialize some generic (but valid) tokens + # authkey = '{}'.format('a' * 24) + source = '+1 (555) 123-3456' + + # No apikey specified + with pytest.raises(TypeError): + plugins.NotifyMessageBird(apikey=None, source=source) + with pytest.raises(TypeError): + plugins.NotifyMessageBird(apikey=" ", source=source) + + def test_notify_pover_plugin(): """ API: NotifyPushover() Extra Checks @@ -4469,13 +4461,8 @@ def test_notify_pover_plugin(): plugins.NotifyBase.request_rate_per_sec = 0 # No token - try: + with pytest.raises(TypeError): plugins.NotifyPushover(token=None) - assert False - - except TypeError: - # we'll thrown because we provided no token - assert True def test_notify_ryver_plugin(): @@ -4486,17 +4473,42 @@ def test_notify_ryver_plugin(): # Disable Throttling to speed testing plugins.NotifyBase.request_rate_per_sec = 0 - # must be 15 characters long - token = 'a' * 15 + # No token + with pytest.raises(TypeError): + plugins.NotifyRyver(organization="abc", token=None) + + with pytest.raises(TypeError): + plugins.NotifyRyver(organization="abc", token=" ") # No organization - try: - plugins.NotifyRyver(organization=None, token=token) - assert False + with pytest.raises(TypeError): + plugins.NotifyRyver(organization=None, token="abc") - except TypeError: - # we'll thrown because an empty list of channels was provided - assert True + with pytest.raises(TypeError): + plugins.NotifyRyver(organization=" ", token="abc") + + +def test_notify_simplepush_plugin(): + """ + API: NotifySimplePush() Extra Checks + + """ + # Disable Throttling to speed testing + plugins.NotifyBase.request_rate_per_sec = 0 + + # No token + with pytest.raises(TypeError): + plugins.NotifySimplePush(apikey=None) + + with pytest.raises(TypeError): + plugins.NotifySimplePush(apikey=" ") + + # Bad event + with pytest.raises(TypeError): + plugins.NotifySimplePush(apikey="abc", event=object) + + with pytest.raises(TypeError): + plugins.NotifySimplePush(apikey="abc", event=" ") def test_notify_zulip_plugin(): @@ -4511,14 +4523,9 @@ def test_notify_zulip_plugin(): token = 'a' * 32 # Invalid organization - try: + with pytest.raises(TypeError): plugins.NotifyZulip( botname='test', organization='#', token=token) - assert False - - except TypeError: - # we'll thrown because an empty list of channels was provided - assert True @mock.patch('requests.get') @@ -4531,33 +4538,19 @@ def test_notify_sendgrid_plugin(mock_post, mock_get): # Disable Throttling to speed testing plugins.NotifyBase.request_rate_per_sec = 0 - try: - # no apikey + # no apikey + with pytest.raises(TypeError): plugins.NotifySendGrid( apikey=None, from_email='user@example.com') - # We shouldn't get here; we should thrown an exception instead - assert False - except TypeError: - assert True - - try: + # invalid from email + with pytest.raises(TypeError): plugins.NotifySendGrid( apikey='abcd', from_email='!invalid') - # We shouldn't get here; we should thrown an exception instead - assert False - except TypeError: - assert True - - try: - # no email + # no email + with pytest.raises(TypeError): plugins.NotifySendGrid(apikey='abcd', from_email=None) - # We shouldn't get here; we should thrown an exception instead - assert False - - except TypeError: - assert True # Invalid To email address plugins.NotifySendGrid( @@ -4600,15 +4593,10 @@ def test_notify_slack_plugin(mock_post, mock_get): mock_get.return_value.status_code = requests.codes.ok # Missing first Token - try: + with pytest.raises(TypeError): plugins.NotifySlack( token_a=None, token_b=token_b, token_c=token_c, targets=channels) - assert False - - except TypeError: - # we'll thrown because an empty list of channels was provided - assert True # Test include_image obj = plugins.NotifySlack( @@ -4642,6 +4630,12 @@ def test_notify_pushbullet_plugin(mock_post, mock_get): mock_post.return_value.status_code = requests.codes.ok mock_get.return_value.status_code = requests.codes.ok + # Invalid Access Token + with pytest.raises(TypeError): + plugins.NotifyPushBullet(accesstoken=None) + with pytest.raises(TypeError): + plugins.NotifyPushBullet(accesstoken=" ") + obj = plugins.NotifyPushBullet( accesstoken=accesstoken, targets=recipients) assert isinstance(obj, plugins.NotifyPushBullet) is True @@ -4683,32 +4677,44 @@ def test_notify_pushed_plugin(mock_post, mock_get): mock_post.return_value.status_code = requests.codes.ok mock_get.return_value.status_code = requests.codes.ok - try: - obj = plugins.NotifyPushed( + # No application Key specified + with pytest.raises(TypeError): + plugins.NotifyPushed( + app_key=None, + app_secret=app_secret, + recipients=None, + ) + + with pytest.raises(TypeError): + plugins.NotifyPushed( + app_key=" ", + app_secret=app_secret, + recipients=None, + ) + # No application Secret specified + with pytest.raises(TypeError): + plugins.NotifyPushed( app_key=app_key, app_secret=None, recipients=None, ) - assert False - except TypeError: - # No application Secret was specified; it's a good thing if - # this exception was thrown - assert True - - try: - obj = plugins.NotifyPushed( + with pytest.raises(TypeError): + plugins.NotifyPushed( app_key=app_key, - app_secret=app_secret, - recipients=None, + app_secret=" ", ) - # recipients list set to (None) is perfectly fine; in this - # case it will notify the App - assert True - except TypeError: - # Exception should never be thrown! - assert False + # recipients list set to (None) is perfectly fine; in this case it will + # notify the App + obj = plugins.NotifyPushed( + app_key=app_key, + app_secret=app_secret, + recipients=None, + ) + assert isinstance(obj, plugins.NotifyPushed) is True + assert len(obj.channels) == 0 + assert len(obj.users) == 0 obj = plugins.NotifyPushed( app_key=app_key, @@ -4724,6 +4730,22 @@ def test_notify_pushed_plugin(mock_post, mock_get): mock_get.return_value.status_code = requests.codes.internal_server_error +def test_notify_pushjet_plugin(): + """ + API: NotifyPushjet() Extra Checks + + """ + # Disable Throttling to speed testing + plugins.NotifyBase.request_rate_per_sec = 0 + + # No application Key specified + with pytest.raises(TypeError): + plugins.NotifyPushjet(secret_key=None) + + with pytest.raises(TypeError): + plugins.NotifyPushjet(secret_key=" ") + + @mock.patch('requests.get') @mock.patch('requests.post') def test_notify_pushover_plugin(mock_post, mock_get): @@ -4736,7 +4758,7 @@ def test_notify_pushover_plugin(mock_post, mock_get): # Initialize some generic (but valid) tokens token = 'a' * 30 - user = 'u' * 30 + user_key = 'u' * 30 invalid_device = 'd' * 35 @@ -4749,24 +4771,21 @@ def test_notify_pushover_plugin(mock_post, mock_get): mock_post.return_value.status_code = requests.codes.ok mock_get.return_value.status_code = requests.codes.ok - try: - obj = plugins.NotifyPushover(user=user, webhook_id=None) - # No token specified - assert False + # No webhook id specified + with pytest.raises(TypeError): + plugins.NotifyPushover(user_key=user_key, webhook_id=None) - except TypeError: - # Exception should be thrown about the fact no token was specified - assert True - - obj = plugins.NotifyPushover(user=user, token=token, targets=devices) + obj = plugins.NotifyPushover( + user_key=user_key, token=token, targets=devices) assert isinstance(obj, plugins.NotifyPushover) is True assert len(obj.targets) == 3 # This call fails because there is 1 invalid device assert obj.notify( - body='body', title='title', notify_type=NotifyType.INFO) is False + body='body', title='title', + notify_type=NotifyType.INFO) is False - obj = plugins.NotifyPushover(user=user, token=token) + obj = plugins.NotifyPushover(user_key=user_key, token=token) assert isinstance(obj, plugins.NotifyPushover) is True # Default is to send to all devices, so there will be a # device defined here @@ -4776,12 +4795,23 @@ def test_notify_pushover_plugin(mock_post, mock_get): assert obj.notify( body='body', title='title', notify_type=NotifyType.INFO) is True - obj = plugins.NotifyPushover(user=user, token=token, targets=set()) + obj = plugins.NotifyPushover(user_key=user_key, token=token, targets=set()) assert isinstance(obj, plugins.NotifyPushover) is True # Default is to send to all devices, so there will be a # device defined here assert len(obj.targets) == 1 + # No User Key specified + with pytest.raises(TypeError): + plugins.NotifyPushover(user_key=None, token="abcd") + + # No Access Token specified + with pytest.raises(TypeError): + plugins.NotifyPushover(user_key="abcd", token=None) + + with pytest.raises(TypeError): + plugins.NotifyPushover(user_key="abcd", token=" ") + @mock.patch('requests.get') @mock.patch('requests.post') @@ -4816,14 +4846,8 @@ def test_notify_rocketchat_plugin(mock_post, mock_get): assert len(obj.rooms) == 1 # No Webhook specified - try: + with pytest.raises(TypeError): obj = plugins.NotifyRocketChat(webhook=None, mode='webhook') - # We should have thrown an exception before we get to the next - # assert line: - assert False - except TypeError: - # We're in good shape if we reach here as we got the expected error - assert True # # Logout @@ -4908,25 +4932,14 @@ def test_notify_telegram_plugin(mock_post, mock_get): mock_get.return_value.content = '{}' mock_post.return_value.content = '{}' - try: - obj = plugins.NotifyTelegram(bot_token=None, targets=chat_ids) - # invalid bot token (None) - assert False + # Exception should be thrown about the fact no bot token was specified + with pytest.raises(TypeError): + plugins.NotifyTelegram(bot_token=None, targets=chat_ids) - except TypeError: - # Exception should be thrown about the fact no bot token was specified - assert True - - try: - obj = plugins.NotifyTelegram( - bot_token=invalid_bot_token, targets=chat_ids) - # invalid bot token - assert False - - except TypeError: - # Exception should be thrown about the fact an invalid bot token was - # specified - assert True + # Exception should be thrown about the fact an invalid bot token was + # specifed + with pytest.raises(TypeError): + plugins.NotifyTelegram(bot_token=invalid_bot_token, targets=chat_ids) obj = plugins.NotifyTelegram( bot_token=bot_token, targets=chat_ids, include_image=True) @@ -5047,14 +5060,10 @@ def test_notify_telegram_plugin(mock_post, mock_get): }}, ], }) - try: - obj = plugins.NotifyTelegram(bot_token=bot_token, targets=None) - # No chat_ids specified - assert False - except TypeError: - # Exception should be thrown about the fact no bot token was specified - assert True + # Exception should be thrown about the fact no bot token was specified + with pytest.raises(TypeError): + plugins.NotifyTelegram(bot_token=bot_token, targets=None) # Detect the bot with a bad response mock_post.return_value.content = dumps({}) @@ -5062,36 +5071,23 @@ def test_notify_telegram_plugin(mock_post, mock_get): # Test our bot detection with a internal server error mock_post.return_value.status_code = requests.codes.internal_server_error - try: - obj = plugins.NotifyTelegram(bot_token=bot_token, targets=None) - # No chat_ids specified - assert False - except TypeError: - # Exception should be thrown over internal server error caused - assert True + # Exception should be thrown over internal server error caused + with pytest.raises(TypeError): + plugins.NotifyTelegram(bot_token=bot_token, targets=None) # Test our bot detection with an unmappable html error mock_post.return_value.status_code = 999 - try: - obj = plugins.NotifyTelegram(bot_token=bot_token, targets=None) - # No chat_ids specified - assert False - - except TypeError: - # Exception should be thrown over invali internal error no - assert True + # Exception should be thrown over invali internal error no + with pytest.raises(TypeError): + plugins.NotifyTelegram(bot_token=bot_token, targets=None) # Do it again but this time provide a failure message mock_post.return_value.content = dumps({'description': 'Failure Message'}) - try: - obj = plugins.NotifyTelegram(bot_token=bot_token, targets=None) - # No chat_ids specified - assert False - except TypeError: - # Exception should be thrown about the fact no bot token was specified - assert True + # Exception should be thrown about the fact no bot token was specified + with pytest.raises(TypeError): + plugins.NotifyTelegram(bot_token=bot_token, targets=None) # Do it again but this time provide a failure message and perform a # notification without a bot detection by providing at least 1 chat id @@ -5102,15 +5098,10 @@ def test_notify_telegram_plugin(mock_post, mock_get): # iterate over our exceptions and test them for _exception in REQUEST_EXCEPTIONS: mock_post.side_effect = _exception - try: - obj = plugins.NotifyTelegram(bot_token=bot_token, targets=None) - # No chat_ids specified - assert False - except TypeError: - # Exception should be thrown about the fact no bot token was - # specified - assert True + # No chat_ids specified + with pytest.raises(TypeError): + obj = plugins.NotifyTelegram(bot_token=bot_token, targets=None) def test_notify_overflow_truncate(): @@ -5158,18 +5149,11 @@ def test_notify_overflow_truncate(): # Pretend everything is okay return True - try: + # We should throw an exception because our specified overflow is wrong. + with pytest.raises(TypeError): # Load our object obj = TestNotification(overflow='invalid') - # We should have thrown an exception because our specified overflow - # is wrong. - assert False - - except TypeError: - # Expected to be here - assert True - # Load our object obj = TestNotification(overflow=OverflowMode.TRUNCATE) assert obj is not None diff --git a/test/test_sns_plugin.py b/test/test_sns_plugin.py index bc5d5e74..36280339 100644 --- a/test/test_sns_plugin.py +++ b/test/test_sns_plugin.py @@ -24,6 +24,7 @@ # THE SOFTWARE. import mock +import pytest import requests from apprise import plugins from apprise import Apprise @@ -45,7 +46,7 @@ def test_object_initialization(): """ # Initializes the plugin with a valid access, but invalid access key - try: + with pytest.raises(TypeError): # No access_key_id specified plugins.NotifySNS( access_key_id=None, @@ -53,14 +54,8 @@ def test_object_initialization(): region_name=TEST_REGION, targets='+1800555999', ) - # The entries above are invalid, our code should never reach here - assert(False) - except TypeError: - # Exception correctly caught - assert(True) - - try: + with pytest.raises(TypeError): # No secret_access_key specified plugins.NotifySNS( access_key_id=TEST_ACCESS_KEY_ID, @@ -68,14 +63,8 @@ def test_object_initialization(): region_name=TEST_REGION, targets='+1800555999', ) - # The entries above are invalid, our code should never reach here - assert(False) - except TypeError: - # Exception correctly caught - assert(True) - - try: + with pytest.raises(TypeError): # No region_name specified plugins.NotifySNS( access_key_id=TEST_ACCESS_KEY_ID, @@ -83,14 +72,8 @@ def test_object_initialization(): region_name=None, targets='+1800555999', ) - # The entries above are invalid, our code should never reach here - assert(False) - except TypeError: - # Exception correctly caught - assert(True) - - try: + with pytest.raises(TypeError): # No recipients plugins.NotifySNS( access_key_id=TEST_ACCESS_KEY_ID, @@ -98,14 +81,8 @@ def test_object_initialization(): region_name=TEST_REGION, targets=None, ) - # Still valid even without recipients - assert(True) - except TypeError: - # Exception correctly caught - assert(False) - - try: + with pytest.raises(TypeError): # No recipients - garbage recipients object plugins.NotifySNS( access_key_id=TEST_ACCESS_KEY_ID, @@ -113,14 +90,8 @@ def test_object_initialization(): region_name=TEST_REGION, targets=object(), ) - # Still valid even without recipients - assert(True) - except TypeError: - # Exception correctly caught - assert(False) - - try: + with pytest.raises(TypeError): # The phone number is invalid, and without it, there is nothing # to notify plugins.NotifySNS( @@ -129,15 +100,8 @@ def test_object_initialization(): region_name=TEST_REGION, targets='+1809', ) - # The recipient is invalid, but it's still okay; this Notification - # still becomes pretty much useless at this point though - assert(True) - except TypeError: - # Exception correctly caught - assert(False) - - try: + with pytest.raises(TypeError): # The phone number is invalid, and without it, there is nothing # to notify; we plugins.NotifySNS( @@ -146,13 +110,6 @@ def test_object_initialization(): region_name=TEST_REGION, targets='#(invalid-topic-because-of-the-brackets)', ) - # The recipient is invalid, but it's still okay; this Notification - # still becomes pretty much useless at this point though - assert(True) - - except TypeError: - # Exception correctly caught - assert(False) def test_url_parsing(): @@ -169,13 +126,13 @@ def test_url_parsing(): ) # Confirm that there were no recipients found - assert(len(results['targets']) == 0) - assert('region_name' in results) - assert(TEST_REGION == results['region_name']) - assert('access_key_id' in results) - assert(TEST_ACCESS_KEY_ID == results['access_key_id']) - assert('secret_access_key' in results) - assert(TEST_ACCESS_KEY_SECRET == results['secret_access_key']) + assert len(results['targets']) == 0 + assert 'region_name' in results + assert TEST_REGION == results['region_name'] + assert 'access_key_id' in results + assert TEST_ACCESS_KEY_ID == results['access_key_id'] + assert 'secret_access_key' in results + assert TEST_ACCESS_KEY_SECRET == results['secret_access_key'] # Detect recipients results = plugins.NotifySNS.parse_url('sns://%s/%s/%s/%s/%s/' % ( @@ -188,15 +145,15 @@ def test_url_parsing(): ) # Confirm that our recipients were found - assert(len(results['targets']) == 2) - assert('+18001234567' in results['targets']) - assert('MyTopic' in results['targets']) - assert('region_name' in results) - assert(TEST_REGION == results['region_name']) - assert('access_key_id' in results) - assert(TEST_ACCESS_KEY_ID == results['access_key_id']) - assert('secret_access_key' in results) - assert(TEST_ACCESS_KEY_SECRET == results['secret_access_key']) + assert len(results['targets']) == 2 + assert '+18001234567' in results['targets'] + assert 'MyTopic' in results['targets'] + assert 'region_name' in results + assert TEST_REGION == results['region_name'] + assert 'access_key_id' in results + assert TEST_ACCESS_KEY_ID == results['access_key_id'] + assert 'secret_access_key' in results + assert TEST_ACCESS_KEY_SECRET == results['secret_access_key'] def test_object_parsing(): @@ -209,20 +166,20 @@ def test_object_parsing(): a = Apprise() # Now test failing variations of our URL - assert(a.add('sns://') is False) - assert(a.add('sns://nosecret') is False) - assert(a.add('sns://nosecret/noregion/') is False) + assert a.add('sns://') is False + assert a.add('sns://nosecret') is False + assert a.add('sns://nosecret/noregion/') is False - # This is valid, but a rather useless URL; there is nothing to notify - assert(a.add('sns://norecipient/norecipient/us-west-2') is True) - assert(len(a) == 1) + # This is valid but without valid recipients, the URL is actually useless + assert a.add('sns://norecipient/norecipient/us-west-2') is False + assert len(a) == 0 # Parse a good one - assert(a.add('sns://oh/yeah/us-west-2/abcdtopic/+12223334444') is True) - assert(len(a) == 2) + assert a.add('sns://oh/yeah/us-west-2/abcdtopic/+12223334444') is True + assert len(a) == 1 - assert(a.add('sns://oh/yeah/us-west-2/12223334444') is True) - assert(len(a) == 3) + assert a.add('sns://oh/yeah/us-west-2/12223334444') is True + assert len(a) == 2 def test_aws_response_handling(): @@ -232,25 +189,25 @@ def test_aws_response_handling(): """ # Not a string response = plugins.NotifySNS.aws_response_to_dict(None) - assert(response['type'] is None) - assert(response['request_id'] is None) + assert response['type'] is None + assert response['request_id'] is None # Invalid XML response = plugins.NotifySNS.aws_response_to_dict( '') - assert(response['type'] is None) - assert(response['request_id'] is None) + assert response['type'] is None + assert response['request_id'] is None # Single Element in XML response = plugins.NotifySNS.aws_response_to_dict( '') - assert(response['type'] == 'SingleElement') - assert(response['request_id'] is None) + assert response['type'] == 'SingleElement' + assert response['request_id'] is None # Empty String response = plugins.NotifySNS.aws_response_to_dict('') - assert(response['type'] is None) - assert(response['request_id'] is None) + assert response['type'] is None + assert response['request_id'] is None response = plugins.NotifySNS.aws_response_to_dict( """ @@ -263,9 +220,9 @@ def test_aws_response_handling(): """) - assert(response['type'] == 'PublishResponse') - assert(response['request_id'] == 'dc258024-d0e6-56bb-af1b-d4fe5f4181a4') - assert(response['message_id'] == '5e16935a-d1fb-5a31-a716-c7805e5c1d2e') + assert response['type'] == 'PublishResponse' + assert response['request_id'] == 'dc258024-d0e6-56bb-af1b-d4fe5f4181a4' + assert response['message_id'] == '5e16935a-d1fb-5a31-a716-c7805e5c1d2e' response = plugins.NotifySNS.aws_response_to_dict( """ @@ -278,9 +235,9 @@ def test_aws_response_handling(): """) - assert(response['type'] == 'CreateTopicResponse') - assert(response['request_id'] == '604bef0f-369c-50c5-a7a4-bbd474c83d6a') - assert(response['topic_arn'] == 'arn:aws:sns:us-east-1:000000000000:abcd') + assert response['type'] == 'CreateTopicResponse' + assert response['request_id'] == '604bef0f-369c-50c5-a7a4-bbd474c83d6a' + assert response['topic_arn'] == 'arn:aws:sns:us-east-1:000000000000:abcd' response = plugins.NotifySNS.aws_response_to_dict( """ @@ -294,12 +251,12 @@ def test_aws_response_handling(): b5614883-babe-56ca-93b2-1c592ba6191e """) - assert(response['type'] == 'ErrorResponse') - assert(response['request_id'] == 'b5614883-babe-56ca-93b2-1c592ba6191e') - assert(response['error_type'] == 'Sender') - assert(response['error_code'] == 'InvalidParameter') - assert(response['error_message'].startswith('Invalid parameter:')) - assert(response['error_message'].endswith('required parameter')) + assert response['type'] == 'ErrorResponse' + assert response['request_id'] == 'b5614883-babe-56ca-93b2-1c592ba6191e' + assert response['error_type'] == 'Sender' + assert response['error_code'] == 'InvalidParameter' + assert response['error_message'].startswith('Invalid parameter:') + assert response['error_message'].endswith('required parameter') @mock.patch('requests.post') @@ -356,7 +313,7 @@ def test_aws_topic_handling(mock_post): '12223334444/TopicA']) # CreateTopic fails - assert(a.notify(title='', body='test') is False) + assert a.notify(title='', body='test') is False def post(url, data, **kwargs): """ @@ -383,7 +340,7 @@ def test_aws_topic_handling(mock_post): mock_post.side_effect = post # Publish fails - assert(a.notify(title='', body='test') is False) + assert a.notify(title='', body='test') is False # Disable our side effect mock_post.side_effect = None @@ -395,14 +352,14 @@ def test_aws_topic_handling(mock_post): # Assign ourselves a new function mock_post.return_value = robj - assert(a.notify(title='', body='test') is False) + assert a.notify(title='', body='test') is False # Handle case where we fails get a bad response robj = mock.Mock() robj.content = '' robj.status_code = requests.codes.bad_request mock_post.return_value = robj - assert(a.notify(title='', body='test') is False) + assert a.notify(title='', body='test') is False # Handle case where we get a valid response and TopicARN robj = mock.Mock() @@ -410,4 +367,4 @@ def test_aws_topic_handling(mock_post): robj.status_code = requests.codes.ok mock_post.return_value = robj # We would have failed to make Post - assert(a.notify(title='', body='test') is True) + assert a.notify(title='', body='test') is True diff --git a/test/test_twitter_plugin.py b/test/test_twitter_plugin.py index f69f84fa..69aa50d9 100644 --- a/test/test_twitter_plugin.py +++ b/test/test_twitter_plugin.py @@ -25,6 +25,7 @@ import six import mock +import pytest import requests from json import dumps from datetime import datetime @@ -41,57 +42,40 @@ def test_twitter_plugin_init(): """ - try: + with pytest.raises(TypeError): plugins.NotifyTwitter( ckey=None, csecret=None, akey=None, asecret=None) - assert False - except TypeError: - # All keys set to none - assert True - try: + with pytest.raises(TypeError): plugins.NotifyTwitter( ckey='value', csecret=None, akey=None, asecret=None) - assert False - except TypeError: - # csecret not set - assert True - try: + with pytest.raises(TypeError): plugins.NotifyTwitter( ckey='value', csecret='value', akey=None, asecret=None) - assert False - except TypeError: - # akey not set - assert True - try: + with pytest.raises(TypeError): plugins.NotifyTwitter( ckey='value', csecret='value', akey='value', asecret=None) - assert False - except TypeError: - # asecret not set - assert True - try: + assert isinstance( plugins.NotifyTwitter( - ckey='value', csecret='value', akey='value', asecret='value') - assert True - except TypeError: - # user not set; but this is okay - # We should not reach here - assert False + ckey='value', csecret='value', akey='value', asecret='value'), + plugins.NotifyTwitter, + ) - try: + assert isinstance( plugins.NotifyTwitter( ckey='value', csecret='value', akey='value', asecret='value', - user='l2gnux') - # We should initialize properly - assert True + user='l2gnux'), + plugins.NotifyTwitter, + ) - except TypeError: - # We should not reach here - assert False + # Invalid Target User + with pytest.raises(TypeError): + plugins.NotifyTwitter( + ckey='value', csecret='value', akey='value', asecret='value', + targets='%G@rB@g3') @mock.patch('requests.get') diff --git a/test/test_utils.py b/test/test_utils.py index bbec4414..a63936dc 100644 --- a/test/test_utils.py +++ b/test/test_utils.py @@ -45,293 +45,290 @@ def test_parse_qsd(): "utils: parse_qsd() testing """ result = utils.parse_qsd('a=1&b=&c&d=abcd') - assert(isinstance(result, dict) is True) - assert(len(result) == 3) + assert isinstance(result, dict) is True + assert len(result) == 3 assert 'qsd' in result assert 'qsd+' in result assert 'qsd-' in result - assert(len(result['qsd']) == 4) + assert len(result['qsd']) == 4 assert 'a' in result['qsd'] assert 'b' in result['qsd'] assert 'c' in result['qsd'] assert 'd' in result['qsd'] - assert(len(result['qsd-']) == 0) - assert(len(result['qsd+']) == 0) + assert len(result['qsd-']) == 0 + assert len(result['qsd+']) == 0 def test_parse_url(): "utils: parse_url() testing """ result = utils.parse_url('http://hostname') - assert(result['schema'] == 'http') - assert(result['host'] == 'hostname') - assert(result['port'] is None) - assert(result['user'] is None) - assert(result['password'] is None) - assert(result['fullpath'] is None) - assert(result['path'] is None) - assert(result['query'] is None) - assert(result['url'] == 'http://hostname') - assert(result['qsd'] == {}) - assert(result['qsd-'] == {}) - assert(result['qsd+'] == {}) + assert result['schema'] == 'http' + assert result['host'] == 'hostname' + assert result['port'] is None + assert result['user'] is None + assert result['password'] is None + assert result['fullpath'] is None + assert result['path'] is None + assert result['query'] is None + assert result['url'] == 'http://hostname' + assert result['qsd'] == {} + assert result['qsd-'] == {} + assert result['qsd+'] == {} result = utils.parse_url('http://hostname/') - assert(result['schema'] == 'http') - assert(result['host'] == 'hostname') - assert(result['port'] is None) - assert(result['user'] is None) - assert(result['password'] is None) - assert(result['fullpath'] == '/') - assert(result['path'] == '/') - assert(result['query'] is None) - assert(result['url'] == 'http://hostname/') - assert(result['qsd'] == {}) - assert(result['qsd-'] == {}) - assert(result['qsd+'] == {}) + assert result['schema'] == 'http' + assert result['host'] == 'hostname' + assert result['port'] is None + assert result['user'] is None + assert result['password'] is None + assert result['fullpath'] == '/' + assert result['path'] == '/' + assert result['query'] is None + assert result['url'] == 'http://hostname/' + assert result['qsd'] == {} + assert result['qsd-'] == {} + assert result['qsd+'] == {} result = utils.parse_url('hostname') - assert(result['schema'] == 'http') - assert(result['host'] == 'hostname') - assert(result['port'] is None) - assert(result['user'] is None) - assert(result['password'] is None) - assert(result['fullpath'] is None) - assert(result['path'] is None) - assert(result['query'] is None) - assert(result['url'] == 'http://hostname') - assert(result['qsd'] == {}) - assert(result['qsd-'] == {}) - assert(result['qsd+'] == {}) + assert result['schema'] == 'http' + assert result['host'] == 'hostname' + assert result['port'] is None + assert result['user'] is None + assert result['password'] is None + assert result['fullpath'] is None + assert result['path'] is None + assert result['query'] is None + assert result['url'] == 'http://hostname' + assert result['qsd'] == {} + assert result['qsd-'] == {} + assert result['qsd+'] == {} result = utils.parse_url('http://hostname/?-KeY=Value') - assert(result['schema'] == 'http') - assert(result['host'] == 'hostname') - assert(result['port'] is None) - assert(result['user'] is None) - assert(result['password'] is None) - assert(result['fullpath'] == '/') - assert(result['path'] == '/') - assert(result['query'] is None) - assert(result['url'] == 'http://hostname/') - assert('-key' in result['qsd']) - assert(unquote(result['qsd']['-key']) == 'Value') - assert('KeY' in result['qsd-']) - assert(unquote(result['qsd-']['KeY']) == 'Value') - assert(result['qsd+'] == {}) + assert result['schema'] == 'http' + assert result['host'] == 'hostname' + assert result['port'] is None + assert result['user'] is None + assert result['password'] is None + assert result['fullpath'] == '/' + assert result['path'] == '/' + assert result['query'] is None + assert result['url'] == 'http://hostname/' + assert '-key' in result['qsd'] + assert unquote(result['qsd']['-key']) == 'Value' + assert 'KeY' in result['qsd-'] + assert unquote(result['qsd-']['KeY']) == 'Value' + assert result['qsd+'] == {} result = utils.parse_url('http://hostname/?+KeY=Value') - assert(result['schema'] == 'http') - assert(result['host'] == 'hostname') - assert(result['port'] is None) - assert(result['user'] is None) - assert(result['password'] is None) - assert(result['fullpath'] == '/') - assert(result['path'] == '/') - assert(result['query'] is None) - assert(result['url'] == 'http://hostname/') - assert('+key' in result['qsd']) - assert('KeY' in result['qsd+']) - assert(result['qsd+']['KeY'] == 'Value') - assert(result['qsd-'] == {}) + assert result['schema'] == 'http' + assert result['host'] == 'hostname' + assert result['port'] is None + assert result['user'] is None + assert result['password'] is None + assert result['fullpath'] == '/' + assert result['path'] == '/' + assert result['query'] is None + assert result['url'] == 'http://hostname/' + assert '+key' in result['qsd'] + assert 'KeY' in result['qsd+'] + assert result['qsd+']['KeY'] == 'Value' + assert result['qsd-'] == {} result = utils.parse_url( 'http://hostname/?+KeY=ValueA&-kEy=ValueB&KEY=Value%20+C') - assert(result['schema'] == 'http') - assert(result['host'] == 'hostname') - assert(result['port'] is None) - assert(result['user'] is None) - assert(result['password'] is None) - assert(result['fullpath'] == '/') - assert(result['path'] == '/') - assert(result['query'] is None) - assert(result['url'] == 'http://hostname/') - assert('+key' in result['qsd']) - assert('-key' in result['qsd']) - assert('key' in result['qsd']) - assert('KeY' in result['qsd+']) - assert(result['qsd+']['KeY'] == 'ValueA') - assert('kEy' in result['qsd-']) - assert(result['qsd-']['kEy'] == 'ValueB') - assert(result['qsd']['key'] == 'Value C') - assert(result['qsd']['+key'] == result['qsd+']['KeY']) - assert(result['qsd']['-key'] == result['qsd-']['kEy']) + assert result['schema'] == 'http' + assert result['host'] == 'hostname' + assert result['port'] is None + assert result['user'] is None + assert result['password'] is None + assert result['fullpath'] == '/' + assert result['path'] == '/' + assert result['query'] is None + assert result['url'] == 'http://hostname/' + assert '+key' in result['qsd'] + assert '-key' in result['qsd'] + assert 'key' in result['qsd'] + assert 'KeY' in result['qsd+'] + assert result['qsd+']['KeY'] == 'ValueA' + assert 'kEy' in result['qsd-'] + assert result['qsd-']['kEy'] == 'ValueB' + assert result['qsd']['key'] == 'Value C' + assert result['qsd']['+key'] == result['qsd+']['KeY'] + assert result['qsd']['-key'] == result['qsd-']['kEy'] result = utils.parse_url('http://hostname////') - assert(result['schema'] == 'http') - assert(result['host'] == 'hostname') - assert(result['port'] is None) - assert(result['user'] is None) - assert(result['password'] is None) - assert(result['fullpath'] == '/') - assert(result['path'] == '/') - assert(result['query'] is None) - assert(result['url'] == 'http://hostname/') - assert(result['qsd'] == {}) - assert(result['qsd-'] == {}) - assert(result['qsd+'] == {}) + assert result['schema'] == 'http' + assert result['host'] == 'hostname' + assert result['port'] is None + assert result['user'] is None + assert result['password'] is None + assert result['fullpath'] == '/' + assert result['path'] == '/' + assert result['query'] is None + assert result['url'] == 'http://hostname/' + assert result['qsd'] == {} + assert result['qsd-'] == {} + assert result['qsd+'] == {} result = utils.parse_url('http://hostname:40////') - assert(result['schema'] == 'http') - assert(result['host'] == 'hostname') - assert(result['port'] == 40) - assert(result['user'] is None) - assert(result['password'] is None) - assert(result['fullpath'] == '/') - assert(result['path'] == '/') - assert(result['query'] is None) - assert(result['url'] == 'http://hostname:40/') - assert(result['qsd'] == {}) - assert(result['qsd-'] == {}) - assert(result['qsd+'] == {}) + assert result['schema'] == 'http' + assert result['host'] == 'hostname' + assert result['port'] == 40 + assert result['user'] is None + assert result['password'] is None + assert result['fullpath'] == '/' + assert result['path'] == '/' + assert result['query'] is None + assert result['url'] == 'http://hostname:40/' + assert result['qsd'] == {} + assert result['qsd-'] == {} + assert result['qsd+'] == {} result = utils.parse_url('HTTP://HoStNaMe:40/test.php') - assert(result['schema'] == 'http') - assert(result['host'] == 'HoStNaMe') - assert(result['port'] == 40) - assert(result['user'] is None) - assert(result['password'] is None) - assert(result['fullpath'] == '/test.php') - assert(result['path'] == '/') - assert(result['query'] == 'test.php') - assert(result['url'] == 'http://HoStNaMe:40/test.php') - assert(result['qsd'] == {}) - assert(result['qsd-'] == {}) - assert(result['qsd+'] == {}) + assert result['schema'] == 'http' + assert result['host'] == 'HoStNaMe' + assert result['port'] == 40 + assert result['user'] is None + assert result['password'] is None + assert result['fullpath'] == '/test.php' + assert result['path'] == '/' + assert result['query'] == 'test.php' + assert result['url'] == 'http://HoStNaMe:40/test.php' + assert result['qsd'] == {} + assert result['qsd-'] == {} + assert result['qsd+'] == {} result = utils.parse_url('HTTPS://user@hostname/test.py') - assert(result['schema'] == 'https') - assert(result['host'] == 'hostname') - assert(result['port'] is None) - assert(result['user'] == 'user') - assert(result['password'] is None) - assert(result['fullpath'] == '/test.py') - assert(result['path'] == '/') - assert(result['query'] == 'test.py') - assert(result['url'] == 'https://user@hostname/test.py') - assert(result['qsd'] == {}) - assert(result['qsd-'] == {}) - assert(result['qsd+'] == {}) + assert result['schema'] == 'https' + assert result['host'] == 'hostname' + assert result['port'] is None + assert result['user'] == 'user' + assert result['password'] is None + assert result['fullpath'] == '/test.py' + assert result['path'] == '/' + assert result['query'] == 'test.py' + assert result['url'] == 'https://user@hostname/test.py' + assert result['qsd'] == {} + assert result['qsd-'] == {} + assert result['qsd+'] == {} result = utils.parse_url(' HTTPS://///user@@@hostname///test.py ') - assert(result['schema'] == 'https') - assert(result['host'] == 'hostname') - assert(result['port'] is None) - assert(result['user'] == 'user') - assert(result['password'] is None) - assert(result['fullpath'] == '/test.py') - assert(result['path'] == '/') - assert(result['query'] == 'test.py') - assert(result['url'] == 'https://user@hostname/test.py') - assert(result['qsd'] == {}) - assert(result['qsd-'] == {}) - assert(result['qsd+'] == {}) + assert result['schema'] == 'https' + assert result['host'] == 'hostname' + assert result['port'] is None + assert result['user'] == 'user' + assert result['password'] is None + assert result['fullpath'] == '/test.py' + assert result['path'] == '/' + assert result['query'] == 'test.py' + assert result['url'] == 'https://user@hostname/test.py' + assert result['qsd'] == {} + assert result['qsd-'] == {} + assert result['qsd+'] == {} result = utils.parse_url( 'HTTPS://user:password@otherHost/full///path/name/', ) - assert(result['schema'] == 'https') - assert(result['host'] == 'otherHost') - assert(result['port'] is None) - assert(result['user'] == 'user') - assert(result['password'] == 'password') - assert(result['fullpath'] == '/full/path/name/') - assert(result['path'] == '/full/path/name/') - assert(result['query'] is None) - assert(result['url'] == 'https://user:password@otherHost/full/path/name/') - assert(result['qsd'] == {}) - assert(result['qsd-'] == {}) - assert(result['qsd+'] == {}) + assert result['schema'] == 'https' + assert result['host'] == 'otherHost' + assert result['port'] is None + assert result['user'] == 'user' + assert result['password'] == 'password' + assert result['fullpath'] == '/full/path/name/' + assert result['path'] == '/full/path/name/' + assert result['query'] is None + assert result['url'] == 'https://user:password@otherHost/full/path/name/' + assert result['qsd'] == {} + assert result['qsd-'] == {} + assert result['qsd+'] == {} # Handle garbage - assert(utils.parse_url(None) is None) + assert utils.parse_url(None) is None result = utils.parse_url( 'mailto://user:password@otherHost/lead2gold@gmail.com' + '?from=test@test.com&name=Chris%20Caron&format=text' ) - assert(result['schema'] == 'mailto') - assert(result['host'] == 'otherHost') - assert(result['port'] is None) - assert(result['user'] == 'user') - assert(result['password'] == 'password') - assert(unquote(result['fullpath']) == '/lead2gold@gmail.com') - assert(result['path'] == '/') - assert(unquote(result['query']) == 'lead2gold@gmail.com') - assert(unquote( - result['url']) == - 'mailto://user:password@otherHost/lead2gold@gmail.com') - assert(len(result['qsd']) == 3) - assert('name' in result['qsd']) - assert(unquote(result['qsd']['name']) == 'Chris Caron') - assert('from' in result['qsd']) - assert(unquote(result['qsd']['from']) == 'test@test.com') - assert('format' in result['qsd']) - assert(unquote(result['qsd']['format']) == 'text') - assert(result['qsd-'] == {}) - assert(result['qsd+'] == {}) + assert result['schema'] == 'mailto' + assert result['host'] == 'otherHost' + assert result['port'] is None + assert result['user'] == 'user' + assert result['password'] == 'password' + assert unquote(result['fullpath']) == '/lead2gold@gmail.com' + assert result['path'] == '/' + assert unquote(result['query']) == 'lead2gold@gmail.com' + assert unquote(result['url']) == \ + 'mailto://user:password@otherHost/lead2gold@gmail.com' + assert len(result['qsd']) == 3 + assert 'name' in result['qsd'] + assert unquote(result['qsd']['name']) == 'Chris Caron' + assert 'from' in result['qsd'] + assert unquote(result['qsd']['from']) == 'test@test.com' + assert 'format' in result['qsd'] + assert unquote(result['qsd']['format']) == 'text' + assert result['qsd-'] == {} + assert result['qsd+'] == {} # Test Passwords with question marks ?; not supported result = utils.parse_url( 'http://user:pass.with.?question@host' ) - assert(result is None) + assert result is None # just hostnames result = utils.parse_url( 'nuxref.com' ) - assert(result['schema'] == 'http') - assert(result['host'] == 'nuxref.com') - assert(result['port'] is None) - assert(result['user'] is None) - assert(result['password'] is None) - assert(result['fullpath'] is None) - assert(result['path'] is None) - assert(result['query'] is None) - assert(result['url'] == 'http://nuxref.com') - assert(result['qsd'] == {}) - assert(result['qsd-'] == {}) - assert(result['qsd+'] == {}) + assert result['schema'] == 'http' + assert result['host'] == 'nuxref.com' + assert result['port'] is None + assert result['user'] is None + assert result['password'] is None + assert result['fullpath'] is None + assert result['path'] is None + assert result['query'] is None + assert result['url'] == 'http://nuxref.com' + assert result['qsd'] == {} + assert result['qsd-'] == {} + assert result['qsd+'] == {} # just host and path - result = utils.parse_url( - 'invalid/host' - ) - assert(result['schema'] == 'http') - assert(result['host'] == 'invalid') - assert(result['port'] is None) - assert(result['user'] is None) - assert(result['password'] is None) - assert(result['fullpath'] == '/host') - assert(result['path'] == '/') - assert(result['query'] == 'host') - assert(result['url'] == 'http://invalid/host') - assert(result['qsd'] == {}) - assert(result['qsd-'] == {}) - assert(result['qsd+'] == {}) + result = utils.parse_url('invalid/host') + assert result['schema'] == 'http' + assert result['host'] == 'invalid' + assert result['port'] is None + assert result['user'] is None + assert result['password'] is None + assert result['fullpath'] == '/host' + assert result['path'] == '/' + assert result['query'] == 'host' + assert result['url'] == 'http://invalid/host' + assert result['qsd'] == {} + assert result['qsd-'] == {} + assert result['qsd+'] == {} # just all out invalid - assert(utils.parse_url('?') is None) - assert(utils.parse_url('/') is None) + assert utils.parse_url('?') is None + assert utils.parse_url('/') is None # A default port of zero is still considered valid, but # is removed in the response. result = utils.parse_url('http://nuxref.com:0') - assert(result['schema'] == 'http') - assert(result['host'] == 'nuxref.com') - assert(result['port'] is None) - assert(result['user'] is None) - assert(result['password'] is None) - assert(result['fullpath'] is None) - assert(result['path'] is None) - assert(result['query'] is None) - assert(result['url'] == 'http://nuxref.com') - assert(result['qsd'] == {}) - assert(result['qsd-'] == {}) - assert(result['qsd+'] == {}) + assert result['schema'] == 'http' + assert result['host'] == 'nuxref.com' + assert result['port'] is None + assert result['user'] is None + assert result['password'] is None + assert result['fullpath'] is None + assert result['path'] is None + assert result['query'] is None + assert result['url'] == 'http://nuxref.com' + assert result['qsd'] == {} + assert result['qsd-'] == {} + assert result['qsd+'] == {} # Test some illegal strings result = utils.parse_url(object, verify_host=False) @@ -345,7 +342,7 @@ def test_parse_url(): # Do it again without host validation result = utils.parse_url('test://', verify_host=False) - assert(result['schema'] == 'test') + assert result['schema'] == 'test' # It's worth noting that the hostname is an empty string and is NEVER set # to None if it wasn't specified. assert result['host'] == '' @@ -423,10 +420,10 @@ def test_parse_url(): assert result['port'] is None assert result['user'] == '' assert result['password'] == '' - assert(unquote(result['fullpath']) == '/_/@^&/jack.json') - assert(unquote(result['path']) == '/_/@^&/') + assert unquote(result['fullpath']) == '/_/@^&/jack.json' + assert unquote(result['path']) == '/_/@^&/' assert result['query'] == 'jack.json' - assert(unquote(result['url']) == 'crazy://:@/_/@^&/jack.json') + assert unquote(result['url']) == 'crazy://:@/_/@^&/jack.json' assert result['qsd'] == {} assert result['qsd-'] == {} @@ -434,42 +431,42 @@ def test_parse_url(): def test_parse_bool(): "utils: parse_bool() testing """ - assert(utils.parse_bool('Enabled', None) is True) - assert(utils.parse_bool('Disabled', None) is False) - assert(utils.parse_bool('Allow', None) is True) - assert(utils.parse_bool('Deny', None) is False) - assert(utils.parse_bool('Yes', None) is True) - assert(utils.parse_bool('YES', None) is True) - assert(utils.parse_bool('Always', None) is True) - assert(utils.parse_bool('No', None) is False) - assert(utils.parse_bool('NO', None) is False) - assert(utils.parse_bool('NEVER', None) is False) - assert(utils.parse_bool('TrUE', None) is True) - assert(utils.parse_bool('tRUe', None) is True) - assert(utils.parse_bool('FAlse', None) is False) - assert(utils.parse_bool('F', None) is False) - assert(utils.parse_bool('T', None) is True) - assert(utils.parse_bool('0', None) is False) - assert(utils.parse_bool('1', None) is True) - assert(utils.parse_bool('True', None) is True) - assert(utils.parse_bool('Yes', None) is True) - assert(utils.parse_bool(1, None) is True) - assert(utils.parse_bool(0, None) is False) - assert(utils.parse_bool(True, None) is True) - assert(utils.parse_bool(False, None) is False) + assert utils.parse_bool('Enabled', None) is True + assert utils.parse_bool('Disabled', None) is False + assert utils.parse_bool('Allow', None) is True + assert utils.parse_bool('Deny', None) is False + assert utils.parse_bool('Yes', None) is True + assert utils.parse_bool('YES', None) is True + assert utils.parse_bool('Always', None) is True + assert utils.parse_bool('No', None) is False + assert utils.parse_bool('NO', None) is False + assert utils.parse_bool('NEVER', None) is False + assert utils.parse_bool('TrUE', None) is True + assert utils.parse_bool('tRUe', None) is True + assert utils.parse_bool('FAlse', None) is False + assert utils.parse_bool('F', None) is False + assert utils.parse_bool('T', None) is True + assert utils.parse_bool('0', None) is False + assert utils.parse_bool('1', None) is True + assert utils.parse_bool('True', None) is True + assert utils.parse_bool('Yes', None) is True + assert utils.parse_bool(1, None) is True + assert utils.parse_bool(0, None) is False + assert utils.parse_bool(True, None) is True + assert utils.parse_bool(False, None) is False # only the int of 0 will return False since the function # casts this to a boolean - assert(utils.parse_bool(2, None) is True) + assert utils.parse_bool(2, None) is True # An empty list is still false - assert(utils.parse_bool([], None) is False) + assert utils.parse_bool([], None) is False # But a list that contains something is True - assert(utils.parse_bool(['value', ], None) is True) + assert utils.parse_bool(['value', ], None) is True # Use Default (which is False) - assert(utils.parse_bool('OhYeah') is False) + assert utils.parse_bool('OhYeah') is False # Adjust Default and get a different result - assert(utils.parse_bool('OhYeah', True) is True) + assert utils.parse_bool('OhYeah', True) is True def test_is_hostname(): @@ -608,14 +605,15 @@ def test_parse_list(): results = utils.parse_list( '.mkv,.avi,.divx,.xvid,.mov,.wmv,.mp4,.mpg,.mpeg,.vob,.iso') - assert(results == sorted([ + assert results == sorted([ '.divx', '.iso', '.mkv', '.mov', '.mpg', '.avi', '.mpeg', '.vob', '.xvid', '.wmv', '.mp4', - ])) + ]) class StrangeObject(object): def __str__(self): return '.avi' + # Now 2 lists with lots of duplicates and other delimiters results = utils.parse_list( '.mkv,.avi,.divx,.xvid,.mov,.wmv,.mp4,.mpg .mpeg,.vob,,; ;', @@ -623,10 +621,13 @@ def test_parse_list(): '.vob,.iso', ['.vob', ['.vob', '.mkv', StrangeObject(), ], ], StrangeObject()) - assert(results == sorted([ + assert results == sorted([ '.divx', '.iso', '.mkv', '.mov', '.mpg', '.avi', '.mpeg', '.vob', '.xvid', '.wmv', '.mp4', - ])) + ]) + + # Garbage in is removed + assert utils.parse_list(object(), 42, None) == [] # Now a list with extras we want to add as strings # empty entries are removed @@ -634,10 +635,10 @@ def test_parse_list(): '.divx', '.iso', '.mkv', '.mov', '', ' ', '.avi', '.mpeg', '.vob', '.xvid', '.mp4'], '.mov,.wmv,.mp4,.mpg') - assert(results == sorted([ + assert results == sorted([ '.divx', '.wmv', '.iso', '.mkv', '.mov', '.mpg', '.avi', '.vob', '.xvid', '.mpeg', '.mp4', - ])) + ]) def test_exclusive_match(): @@ -735,6 +736,46 @@ def test_exclusive_match(): logic='match_me', data=data, match_all='match_me') is True +def test_apprise_validate_regex(tmpdir): + """ + API: Apprise() Validate Regex tests + + """ + assert utils.validate_regex(None) is None + assert utils.validate_regex(object) is None + assert utils.validate_regex(42) is None + assert utils.validate_regex("") is None + assert utils.validate_regex(" ") is None + assert utils.validate_regex("abc") == "abc" + + # value is a keyword that is extracted (if found) + assert utils.validate_regex( + "- abcd -", r'-(?P[^-]+)-', fmt="{value}") == "abcd" + assert utils.validate_regex( + "- abcd -", r'-(?P[^-]+)-', strip=False, + fmt="{value}") == " abcd " + + # String flags supported in addition to numeric + assert utils.validate_regex( + "- abcd -", r'-(?P[^-]+)-', 'i', fmt="{value}") == "abcd" + assert utils.validate_regex( + "- abcd -", r'-(?P[^-]+)-', re.I, fmt="{value}") == "abcd" + + # Test multiple flag settings + assert utils.validate_regex( + "- abcd -", r'-(?P[^-]+)-', 'isax', fmt="{value}") == "abcd" + + # Invalid flags are just ignored. The below fails to match + # because the default value of 'i' is over-ridden by what is + # identfied below, and no flag is set at the end of the day + assert utils.validate_regex( + "- abcd -", r'-(?P[ABCD]+)-', '-%2gb', fmt="{value}") is None + assert utils.validate_regex( + "- abcd -", r'-(?P[ABCD]+)-', '', fmt="{value}") is None + assert utils.validate_regex( + "- abcd -", r'-(?P[ABCD]+)-', None, fmt="{value}") is None + + def test_environ_temporary_change(): """utils: environ() testing """ diff --git a/test/test_windows_plugin.py b/test/test_windows_plugin.py index 9a08ead2..344a1f2a 100644 --- a/test/test_windows_plugin.py +++ b/test/test_windows_plugin.py @@ -113,37 +113,41 @@ def test_windows_plugin(): obj.duration = 0 # Test URL functionality - assert(isinstance(obj.url(), six.string_types) is True) + assert isinstance(obj.url(), six.string_types) is True # Check that it found our mocked environments - assert(obj._enabled is True) + assert obj._enabled is True # _on_destroy check obj._on_destroy(0, '', 0, 0) # test notifications - assert(obj.notify(title='title', body='body', - notify_type=apprise.NotifyType.INFO) is True) + assert obj.notify( + title='title', body='body', + notify_type=apprise.NotifyType.INFO) is True obj = apprise.Apprise.instantiate( 'windows://_/?image=True', suppress_exceptions=False) obj.duration = 0 - assert(isinstance(obj.url(), six.string_types) is True) - assert(obj.notify(title='title', body='body', - notify_type=apprise.NotifyType.INFO) is True) + assert isinstance(obj.url(), six.string_types) is True + assert obj.notify( + title='title', body='body', + notify_type=apprise.NotifyType.INFO) is True obj = apprise.Apprise.instantiate( 'windows://_/?image=False', suppress_exceptions=False) obj.duration = 0 - assert(isinstance(obj.url(), six.string_types) is True) - assert(obj.notify(title='title', body='body', - notify_type=apprise.NotifyType.INFO) is True) + assert isinstance(obj.url(), six.string_types) is True + assert obj.notify( + title='title', body='body', + notify_type=apprise.NotifyType.INFO) is True obj = apprise.Apprise.instantiate( 'windows://_/?duration=1', suppress_exceptions=False) - assert(isinstance(obj.url(), six.string_types) is True) - assert(obj.notify(title='title', body='body', - notify_type=apprise.NotifyType.INFO) is True) + assert isinstance(obj.url(), six.string_types) is True + assert obj.notify( + title='title', body='body', + notify_type=apprise.NotifyType.INFO) is True # loads okay assert obj.duration == 1 @@ -165,20 +169,23 @@ def test_windows_plugin(): # Test our loading of our icon exception; it will still allow the # notification to be sent win32gui.LoadImage.side_effect = AttributeError - assert(obj.notify(title='title', body='body', - notify_type=apprise.NotifyType.INFO) is True) + assert obj.notify( + title='title', body='body', + notify_type=apprise.NotifyType.INFO) is True # Undo our change win32gui.LoadImage.side_effect = None # Test our global exception handling win32gui.UpdateWindow.side_effect = AttributeError - assert(obj.notify(title='title', body='body', - notify_type=apprise.NotifyType.INFO) is False) + assert obj.notify( + title='title', body='body', + notify_type=apprise.NotifyType.INFO) is False # Undo our change win32gui.UpdateWindow.side_effect = None # Toggle our testing for when we can't send notifications because the # package has been made unavailable to us obj._enabled = False - assert(obj.notify(title='title', body='body', - notify_type=apprise.NotifyType.INFO) is False) + assert obj.notify( + title='title', body='body', + notify_type=apprise.NotifyType.INFO) is False