Refactored qa, unit testing, and service init; refs #157 (#160)

This commit is contained in:
Chris Caron 2019-10-09 12:39:31 -04:00 committed by GitHub
parent 44a21651b3
commit c6922d8f3a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
52 changed files with 2066 additions and 2001 deletions

View File

@ -40,6 +40,7 @@ except ImportError:
from .NotifyBase import NotifyBase from .NotifyBase import NotifyBase
from ..URLBase import PrivacyMode from ..URLBase import PrivacyMode
from ..utils import parse_bool from ..utils import parse_bool
from ..utils import validate_regex
from ..common import NotifyType from ..common import NotifyType
from ..common import NotifyImageSize from ..common import NotifyImageSize
from ..AppriseLocale import gettext_lazy as _ from ..AppriseLocale import gettext_lazy as _
@ -58,11 +59,6 @@ IS_TAG = re.compile(r'^[@](?P<name>[A-Z0-9]{1,63})$', re.I)
# this plugin supports it. # this plugin supports it.
IS_DEVICETOKEN = re.compile(r'^[A-Z0-9]{64}$', re.I) 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 # Used to break apart list of potential tags by their delimiter into a useable
# list. # list.
TAGS_LIST_DELIM = re.compile(r'[ \t\r\n,\\/]+') TAGS_LIST_DELIM = re.compile(r'[ \t\r\n,\\/]+')
@ -105,30 +101,30 @@ class NotifyBoxcar(NotifyBase):
'access_key': { 'access_key': {
'name': _('Access Key'), 'name': _('Access Key'),
'type': 'string', 'type': 'string',
'regex': (r'[A-Z0-9_-]{64}', 'i'),
'private': True, 'private': True,
'required': True, 'required': True,
'regex': (r'^[A-Z0-9_-]{64}$', 'i'),
'map_to': 'access', 'map_to': 'access',
}, },
'secret_key': { 'secret_key': {
'name': _('Secret Key'), 'name': _('Secret Key'),
'type': 'string', 'type': 'string',
'regex': (r'[A-Z0-9_-]{64}', 'i'),
'private': True, 'private': True,
'required': True, 'required': True,
'regex': (r'^[A-Z0-9_-]{64}$', 'i'),
'map_to': 'secret', 'map_to': 'secret',
}, },
'target_tag': { 'target_tag': {
'name': _('Target Tag ID'), 'name': _('Target Tag ID'),
'type': 'string', 'type': 'string',
'prefix': '@', 'prefix': '@',
'regex': (r'[A-Z0-9]{1,63}', 'i'), 'regex': (r'^[A-Z0-9]{1,63}$', 'i'),
'map_to': 'targets', 'map_to': 'targets',
}, },
'target_device': { 'target_device': {
'name': _('Target Device ID'), 'name': _('Target Device ID'),
'type': 'string', 'type': 'string',
'regex': (r'[A-Z0-9]{64}', 'i'), 'regex': (r'^[A-Z0-9]{64}$', 'i'),
'map_to': 'targets', 'map_to': 'targets',
}, },
'targets': { 'targets': {
@ -163,33 +159,21 @@ class NotifyBoxcar(NotifyBase):
# Initialize device_token list # Initialize device_token list
self.device_tokens = list() self.device_tokens = list()
try: # Access Key (associated with project)
# Access Key (associated with project) self.access = validate_regex(
self.access = access.strip() access, *self.template_tokens['access_key']['regex'])
if not self.access:
except AttributeError: msg = 'An invalid Boxcar Access Key ' \
msg = 'The specified access key is invalid.' '({}) was specified.'.format(access)
self.logger.warning(msg) self.logger.warning(msg)
raise TypeError(msg) raise TypeError(msg)
try: # Secret Key (associated with project)
# Secret Key (associated with project) self.secret = validate_regex(
self.secret = secret.strip() secret, *self.template_tokens['secret_key']['regex'])
if not self.secret:
except AttributeError: msg = 'An invalid Boxcar Secret Key ' \
msg = 'The specified secret key is invalid.' '({}) was specified.'.format(secret)
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)
self.logger.warning(msg) self.logger.warning(msg)
raise TypeError(msg) raise TypeError(msg)
@ -228,7 +212,6 @@ class NotifyBoxcar(NotifyBase):
""" """
Perform Boxcar Notification Perform Boxcar Notification
""" """
headers = { headers = {
'User-Agent': self.app_id, 'User-Agent': self.app_id,
'Content-Type': 'application/json' 'Content-Type': 'application/json'

View File

@ -112,7 +112,7 @@ class NotifyClickSend(NotifyBase):
'name': _('Target Phone No'), 'name': _('Target Phone No'),
'type': 'string', 'type': 'string',
'prefix': '+', 'prefix': '+',
'regex': (r'[0-9\s)(+-]+', 'i'), 'regex': (r'^[0-9\s)(+-]+$', 'i'),
'map_to': 'targets', 'map_to': 'targets',
}, },
'targets': { 'targets': {

View File

@ -131,7 +131,7 @@ class NotifyD7Networks(NotifyBase):
'name': _('Target Phone No'), 'name': _('Target Phone No'),
'type': 'string', 'type': 'string',
'prefix': '+', 'prefix': '+',
'regex': (r'[0-9\s)(+-]+', 'i'), 'regex': (r'^[0-9\s)(+-]+$', 'i'),
'map_to': 'targets', 'map_to': 'targets',
}, },
'targets': { 'targets': {
@ -227,6 +227,8 @@ class NotifyD7Networks(NotifyBase):
self.logger.warning(msg) self.logger.warning(msg)
raise TypeError(msg) raise TypeError(msg)
return
def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs): def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
""" """
Depending on whether we are set to batch mode or single mode this Depending on whether we are set to batch mode or single mode this

View File

@ -240,6 +240,8 @@ class NotifyDBus(NotifyBase):
# or not. # or not.
self.include_image = include_image self.include_image = include_image
return
def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs): def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
""" """
Perform DBus Notification Perform DBus Notification

View File

@ -49,6 +49,7 @@ from ..common import NotifyImageSize
from ..common import NotifyFormat from ..common import NotifyFormat
from ..common import NotifyType from ..common import NotifyType
from ..utils import parse_bool from ..utils import parse_bool
from ..utils import validate_regex
from ..AppriseLocale import gettext_lazy as _ from ..AppriseLocale import gettext_lazy as _
@ -144,20 +145,22 @@ class NotifyDiscord(NotifyBase):
""" """
super(NotifyDiscord, self).__init__(**kwargs) super(NotifyDiscord, self).__init__(**kwargs)
if not webhook_id: # Webhook ID (associated with project)
msg = 'An invalid Client ID was specified.' 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) self.logger.warning(msg)
raise TypeError(msg) raise TypeError(msg)
if not webhook_token: # Webhook Token (associated with project)
msg = 'An invalid Webhook Token was specified.' 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) self.logger.warning(msg)
raise TypeError(msg) raise TypeError(msg)
# Store our data
self.webhook_id = webhook_id
self.webhook_token = webhook_token
# Text To Speech # Text To Speech
self.tts = tts self.tts = tts

View File

@ -139,7 +139,7 @@ class NotifyEmby(NotifyBase):
if not self.user: if not self.user:
# User was not specified # User was not specified
msg = 'No Username was specified.' msg = 'No Emby username was specified.'
self.logger.warning(msg) self.logger.warning(msg)
raise TypeError(msg) raise TypeError(msg)

View File

@ -91,6 +91,8 @@ class NotifyFaast(NotifyBase):
# Associate an image with our post # Associate an image with our post
self.include_image = include_image self.include_image = include_image
return
def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs): def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
""" """
Perform Faast Notification Perform Faast Notification

View File

@ -47,6 +47,7 @@ from ..common import NotifyFormat
from ..common import NotifyImageSize from ..common import NotifyImageSize
from ..utils import parse_list from ..utils import parse_list
from ..utils import parse_bool from ..utils import parse_bool
from ..utils import validate_regex
from ..AppriseLocale import gettext_lazy as _ from ..AppriseLocale import gettext_lazy as _
@ -56,12 +57,8 @@ FLOCK_HTTP_ERROR_MAP = {
} }
# Used to detect a channel/user # Used to detect a channel/user
IS_CHANNEL_RE = re.compile(r'^(#|g:)(?P<id>[A-Z0-9_]{12})$', re.I) IS_CHANNEL_RE = re.compile(r'^(#|g:)(?P<id>[A-Z0-9_]+)$', re.I)
IS_USER_RE = re.compile(r'^(@|u:)?(?P<id>[A-Z0-9_]{12})$', re.I) IS_USER_RE = re.compile(r'^(@|u:)?(?P<id>[A-Z0-9_]+)$', 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)
class NotifyFlock(NotifyBase): class NotifyFlock(NotifyBase):
@ -103,7 +100,7 @@ class NotifyFlock(NotifyBase):
'token': { 'token': {
'name': _('Access Key'), 'name': _('Access Key'),
'type': 'string', 'type': 'string',
'regex': (r'[a-z0-9-]{24}', 'i'), 'regex': (r'^[a-z0-9-]{24}$', 'i'),
'private': True, 'private': True,
'required': True, 'required': True,
}, },
@ -115,14 +112,14 @@ class NotifyFlock(NotifyBase):
'name': _('To User ID'), 'name': _('To User ID'),
'type': 'string', 'type': 'string',
'prefix': '@', 'prefix': '@',
'regex': (r'[A-Z0-9_]{12}', 'i'), 'regex': (r'^[A-Z0-9_]{12}$', 'i'),
'map_to': 'targets', 'map_to': 'targets',
}, },
'to_channel': { 'to_channel': {
'name': _('To Channel ID'), 'name': _('To Channel ID'),
'type': 'string', 'type': 'string',
'prefix': '#', 'prefix': '#',
'regex': (r'[A-Z0-9_]{12}', 'i'), 'regex': (r'^[A-Z0-9_]{12}$', 'i'),
'map_to': 'targets', 'map_to': 'targets',
}, },
'targets': { 'targets': {
@ -153,15 +150,18 @@ class NotifyFlock(NotifyBase):
# Build ourselves a target list # Build ourselves a target list
self.targets = list() self.targets = list()
# Initialize our token object self.token = validate_regex(
self.token = token.strip() token, *self.template_tokens['token']['regex'])
if not self.token:
if not IS_API_TOKEN.match(self.token): msg = 'An invalid Flock Access Key ' \
msg = 'The Flock API Token specified ({}) is invalid.'.format( '({}) was specified.'.format(token)
self.token)
self.logger.warning(msg) self.logger.warning(msg)
raise TypeError(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 # Track any issues
has_error = False has_error = False
@ -183,15 +183,13 @@ class NotifyFlock(NotifyBase):
self.logger.warning( self.logger.warning(
'Ignoring invalid target ({}) specified.'.format(target)) '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 # 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) self.logger.warning(msg)
raise TypeError(msg) raise TypeError(msg)
# Track whether or not we want to send an image with our notification return
# or not.
self.include_image = include_image
def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs): def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
""" """

View File

@ -50,14 +50,12 @@ from ..common import NotifyFormat
from ..common import NotifyType from ..common import NotifyType
from ..utils import parse_list from ..utils import parse_list
from ..utils import parse_bool from ..utils import parse_bool
from ..utils import validate_regex
from ..AppriseLocale import gettext_lazy as _ from ..AppriseLocale import gettext_lazy as _
# API Gitter URL # API Gitter URL
GITTER_API_URL = 'https://api.gitter.im/v1' 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 # Used to break path apart into list of targets
TARGET_LIST_DELIM = re.compile(r'[ \t\r\n,\\/]+') TARGET_LIST_DELIM = re.compile(r'[ \t\r\n,\\/]+')
@ -112,9 +110,9 @@ class NotifyGitter(NotifyBase):
'token': { 'token': {
'name': _('Token'), 'name': _('Token'),
'type': 'string', 'type': 'string',
'regex': (r'[a-z0-9]{40}', 'i'),
'private': True, 'private': True,
'required': True, 'required': True,
'regex': (r'^[a-z0-9]{40}$', 'i'),
}, },
'targets': { 'targets': {
'name': _('Rooms'), 'name': _('Rooms'),
@ -141,24 +139,21 @@ class NotifyGitter(NotifyBase):
""" """
super(NotifyGitter, self).__init__(**kwargs) super(NotifyGitter, self).__init__(**kwargs)
try: # Secret Key (associated with project)
# The personal access token associated with the account self.token = validate_regex(
self.token = token.strip() token, *self.template_tokens['token']['regex'])
if not self.token:
except AttributeError: msg = 'An invalid Gitter API Token ' \
# Token was None '({}) was specified.'.format(token)
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)
self.logger.warning(msg) self.logger.warning(msg)
raise TypeError(msg) raise TypeError(msg)
# Parse our targets # Parse our targets
self.targets = parse_list(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 # Used to track maping of rooms to their numeric id lookup for
# messaging # messaging
@ -168,6 +163,8 @@ class NotifyGitter(NotifyBase):
# or not. # or not.
self.include_image = include_image self.include_image = include_image
return
def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs): def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
""" """
Perform Gitter Notification Perform Gitter Notification
@ -183,8 +180,6 @@ class NotifyGitter(NotifyBase):
if image_url: if image_url:
body = '![alt]({})\n{}'.format(image_url, body) body = '![alt]({})\n{}'.format(image_url, body)
# Create a copy of the targets list
targets = list(self.targets)
if self._room_mapping is None: if self._room_mapping is None:
# Populate our room mapping # Populate our room mapping
self._room_mapping = {} self._room_mapping = {}
@ -225,10 +220,8 @@ class NotifyGitter(NotifyBase):
'uri': entry['uri'], 'uri': entry['uri'],
} }
if len(targets) == 0: # Create a copy of the targets list
# No targets specified targets = list(self.targets)
return False
while len(targets): while len(targets):
target = targets.pop(0).lower() target = targets.pop(0).lower()

View File

@ -150,6 +150,8 @@ class NotifyGnome(NotifyBase):
# or not. # or not.
self.include_image = include_image self.include_image = include_image
return
def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs): def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
""" """
Perform Gnome Notification Perform Gnome Notification

View File

@ -31,12 +31,12 @@
# f2c2688f0b5e6a816bbcec768ca1c0de5af76b88/ADD_MESSAGE_EXAMPLES.md#python # f2c2688f0b5e6a816bbcec768ca1c0de5af76b88/ADD_MESSAGE_EXAMPLES.md#python
# API: https://gotify.net/docs/swagger-docs # API: https://gotify.net/docs/swagger-docs
import six
import requests import requests
from json import dumps from json import dumps
from .NotifyBase import NotifyBase from .NotifyBase import NotifyBase
from ..common import NotifyType from ..common import NotifyType
from ..utils import validate_regex
from ..AppriseLocale import gettext_lazy as _ from ..AppriseLocale import gettext_lazy as _
@ -121,9 +121,12 @@ class NotifyGotify(NotifyBase):
""" """
super(NotifyGotify, self).__init__(**kwargs) super(NotifyGotify, self).__init__(**kwargs)
if not isinstance(token, six.string_types): # Token (associated with project)
msg = 'An invalid Gotify token was specified.' self.token = validate_regex(token)
self.logger.warning('msg') if not self.token:
msg = 'An invalid Gotify Token ' \
'({}) was specified.'.format(token)
self.logger.warning(msg)
raise TypeError(msg) raise TypeError(msg)
if priority not in GOTIFY_PRIORITIES: if priority not in GOTIFY_PRIORITIES:
@ -138,11 +141,6 @@ class NotifyGotify(NotifyBase):
else: else:
self.schema = 'http' 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 return
def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs): def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):

View File

@ -324,7 +324,6 @@ class NotifyGrowl(NotifyBase):
# We're done early as we couldn't load the results # We're done early as we couldn't load the results
return results return results
# Apply our settings now
version = None version = None
if 'version' in results['qsd'] and len(results['qsd']['version']): if 'version' in results['qsd'] and len(results['qsd']['version']):
# Allow the user to specify the version of the protocol to use. # Allow the user to specify the version of the protocol to use.

View File

@ -46,6 +46,7 @@ from json import dumps
from .NotifyBase import NotifyBase from .NotifyBase import NotifyBase
from ..common import NotifyType from ..common import NotifyType
from ..utils import parse_list from ..utils import parse_list
from ..utils import validate_regex
from ..AppriseLocale import gettext_lazy as _ from ..AppriseLocale import gettext_lazy as _
@ -148,22 +149,21 @@ class NotifyIFTTT(NotifyBase):
""" """
super(NotifyIFTTT, self).__init__(**kwargs) super(NotifyIFTTT, self).__init__(**kwargs)
if not webhook_id: # Webhook ID (associated with project)
msg = 'You must specify the Webhooks webhook_id.' 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) self.logger.warning(msg)
raise TypeError(msg) raise TypeError(msg)
# Store our Events we wish to trigger # Store our Events we wish to trigger
self.events = parse_list(events) self.events = parse_list(events)
if not self.events: if not self.events:
msg = 'You must specify at least one event you wish to trigger on.' msg = 'You must specify at least one event you wish to trigger on.'
self.logger.warning(msg) self.logger.warning(msg)
raise TypeError(msg) raise TypeError(msg)
# Store our APIKey
self.webhook_id = webhook_id
# Tokens to include in post # Tokens to include in post
self.add_tokens = {} self.add_tokens = {}
if add_tokens: if add_tokens:

View File

@ -41,18 +41,16 @@ from ..common import NotifyImageSize
from ..common import NotifyType from ..common import NotifyType
from ..utils import parse_list from ..utils import parse_list
from ..utils import parse_bool from ..utils import parse_bool
from ..utils import validate_regex
from ..AppriseLocale import gettext_lazy as _ 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 # Extend HTTP Error Messages
JOIN_HTTP_ERROR_MAP = { JOIN_HTTP_ERROR_MAP = {
401: 'Unauthorized - Invalid Token.', 401: 'Unauthorized - Invalid Token.',
} }
# Used to detect a device # 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 # Used to detect a device
IS_GROUP_RE = re.compile( IS_GROUP_RE = re.compile(
@ -64,6 +62,24 @@ IS_GROUP_RE = re.compile(
JOIN_IMAGE_XY = NotifyImageSize.XY_72 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): class NotifyJoin(NotifyBase):
""" """
A wrapper for Join Notifications A wrapper for Join Notifications
@ -104,14 +120,14 @@ class NotifyJoin(NotifyBase):
'apikey': { 'apikey': {
'name': _('API Key'), 'name': _('API Key'),
'type': 'string', 'type': 'string',
'regex': (r'[a-z0-9]{32}', 'i'), 'regex': (r'^[a-z0-9]{32}$', 'i'),
'private': True, 'private': True,
'required': True, 'required': True,
}, },
'device': { 'device': {
'name': _('Device ID'), 'name': _('Device ID'),
'type': 'string', 'type': 'string',
'regex': (r'[a-z0-9]{32}', 'i'), 'regex': (r'^[a-z0-9]{32}$', 'i'),
'map_to': 'targets', 'map_to': 'targets',
}, },
'group': { 'group': {
@ -136,37 +152,79 @@ class NotifyJoin(NotifyBase):
'default': False, 'default': False,
'map_to': 'include_image', 'map_to': 'include_image',
}, },
'priority': {
'name': _('Priority'),
'type': 'choice:int',
'values': JOIN_PRIORITIES,
'default': JoinPriority.NORMAL,
},
'to': { 'to': {
'alias_of': 'targets', '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 Initialize Join Object
""" """
super(NotifyJoin, self).__init__(**kwargs) 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 # Track whether or not we want to send an image with our notification
# or not. # or not.
self.include_image = include_image 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): def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
""" """
Perform Join Notification Perform Join Notification
@ -180,26 +238,17 @@ class NotifyJoin(NotifyBase):
# error tracking (used for function return) # error tracking (used for function return)
has_error = False has_error = False
# Create a copy of the devices list # Capture a list of our targets to notify
devices = list(self.devices) targets = list(self.targets)
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())
elif not IS_DEVICE_RE.match(device): while len(targets):
self.logger.warning( # Pop the first element off of our list
'Skipping specified invalid device/group "{}"' target = targets.pop(0)
.format(device)
)
# Mark our failure
has_error = True
continue
url_args = { url_args = {
'apikey': self.apikey, 'apikey': self.apikey,
'deviceId': device, 'deviceId': target,
'priority': str(self.priority),
'title': title, 'title': title,
'text': body, 'text': body,
} }
@ -242,7 +291,7 @@ class NotifyJoin(NotifyBase):
self.logger.warning( self.logger.warning(
'Failed to send Join notification to {}: ' 'Failed to send Join notification to {}: '
'{}{}error={}.'.format( '{}{}error={}.'.format(
device, target,
status_str, status_str,
', ' if status_str else '', ', ' if status_str else '',
r.status_code)) r.status_code))
@ -255,12 +304,12 @@ class NotifyJoin(NotifyBase):
continue continue
else: else:
self.logger.info('Sent Join notification to %s.' % device) self.logger.info('Sent Join notification to %s.' % target)
except requests.RequestException as e: except requests.RequestException as e:
self.logger.warning( self.logger.warning(
'A Connection error occured sending Join:%s ' 'A Connection error occured sending Join:%s '
'notification.' % device 'notification.' % target
) )
self.logger.debug('Socket Exception: %s' % str(e)) self.logger.debug('Socket Exception: %s' % str(e))
@ -274,20 +323,30 @@ class NotifyJoin(NotifyBase):
""" """
Returns the URL built dynamically based on specified arguments. 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 # Define any arguments set
args = { args = {
'format': self.notify_format, 'format': self.notify_format,
'overflow': self.overflow_mode, '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', 'image': 'yes' if self.include_image else 'no',
'verify': 'yes' if self.verify_certificate 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, schema=self.secure_protocol,
apikey=self.pprint(self.apikey, privacy, safe=''), apikey=self.pprint(self.apikey, privacy, safe=''),
devices='/'.join([NotifyJoin.quote(x, safe='') targets='/'.join([NotifyJoin.quote(x, safe='')
for x in self.devices]), for x in self.targets]),
args=NotifyJoin.urlencode(args)) args=NotifyJoin.urlencode(args))
@staticmethod @staticmethod
@ -310,6 +369,23 @@ class NotifyJoin(NotifyBase):
# Unquote our API Key # Unquote our API Key
results['apikey'] = NotifyJoin.unquote(results['apikey']) 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 # Our Devices
results['targets'] = list() results['targets'] = list()
if results['user']: if results['user']:

View File

@ -33,27 +33,14 @@
# The API reference used to build this plugin was documented here: # The API reference used to build this plugin was documented here:
# https://docs.kumulos.com/messaging/api/#sending-in-app-messages # https://docs.kumulos.com/messaging/api/#sending-in-app-messages
# #
import re
import requests import requests
from json import dumps from json import dumps
from .NotifyBase import NotifyBase from .NotifyBase import NotifyBase
from ..common import NotifyType from ..common import NotifyType
from ..utils import validate_regex
from ..AppriseLocale import gettext_lazy as _ 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 # Extend HTTP Error Messages
KUMULOS_HTTP_ERROR_MAP = { KUMULOS_HTTP_ERROR_MAP = {
401: 'Unauthorized - Invalid API and/or Server Key.', 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.', 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): class NotifyKumulos(NotifyBase):
""" """
@ -103,14 +87,16 @@ class NotifyKumulos(NotifyBase):
'type': 'string', 'type': 'string',
'private': True, 'private': True,
'required': 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': { 'serverkey': {
'name': _('Server Key'), 'name': _('Server Key'),
'type': 'string', 'type': 'string',
'private': True, 'private': True,
'required': 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) super(NotifyKumulos, self).__init__(**kwargs)
if not apikey: # API Key (associated with project)
msg = 'The Kumulos API Key is not specified.' 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) self.logger.warning(msg)
raise TypeError(msg) raise TypeError(msg)
self.apikey = apikey.strip() # Server Key (associated with project)
if not VALIDATE_APIKEY.match(self.apikey): self.serverkey = validate_regex(
msg = 'The Kumulos API Key specified ({}) is invalid.'\ serverkey, *self.template_tokens['serverkey']['regex'])
.format(apikey) if not self.serverkey:
self.logger.warning(msg) msg = 'An invalid Kumulos Server Key ' \
raise TypeError(msg) '({}) was specified.'.format(serverkey)
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)
self.logger.warning(msg) self.logger.warning(msg)
raise TypeError(msg) raise TypeError(msg)

View File

@ -37,11 +37,9 @@ import requests
from .NotifyBase import NotifyBase from .NotifyBase import NotifyBase
from ..common import NotifyType from ..common import NotifyType
from ..utils import parse_list from ..utils import parse_list
from ..utils import validate_regex
from ..AppriseLocale import gettext_lazy as _ 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 # Some Phone Number Detection
IS_PHONE_NO = re.compile(r'^\+?(?P<phone>[0-9\s)(+-]+)\s*$') IS_PHONE_NO = re.compile(r'^\+?(?P<phone>[0-9\s)(+-]+)\s*$')
@ -118,13 +116,14 @@ class NotifyMSG91(NotifyBase):
'name': _('Authentication Key'), 'name': _('Authentication Key'),
'type': 'string', 'type': 'string',
'required': True, 'required': True,
'regex': (r'[a-z0-9]+', 'i'), 'private': True,
'regex': (r'^[a-z0-9]+$', 'i'),
}, },
'target_phone': { 'target_phone': {
'name': _('Target Phone No'), 'name': _('Target Phone No'),
'type': 'string', 'type': 'string',
'prefix': '+', 'prefix': '+',
'regex': (r'[0-9\s)(+-]+', 'i'), 'regex': (r'^[0-9\s)(+-]+$', 'i'),
'map_to': 'targets', 'map_to': 'targets',
}, },
'targets': { 'targets': {
@ -162,19 +161,12 @@ class NotifyMSG91(NotifyBase):
""" """
super(NotifyMSG91, self).__init__(**kwargs) super(NotifyMSG91, self).__init__(**kwargs)
try: # Authentication Key (associated with project)
# The authentication key associated with the account self.authkey = validate_regex(
self.authkey = authkey.strip() authkey, *self.template_tokens['authkey']['regex'])
if not self.authkey:
except AttributeError: msg = 'An invalid MSG91 Authentication Key ' \
# Token was None '({}) was specified.'.format(authkey)
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)
self.logger.warning(msg) self.logger.warning(msg)
raise TypeError(msg) raise TypeError(msg)
@ -237,16 +229,19 @@ class NotifyMSG91(NotifyBase):
'({}) specified.'.format(target), '({}) 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): def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
""" """
Perform MSG91 Notification 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 # Prepare our headers
headers = { headers = {
'User-Agent': self.app_id, 'User-Agent': self.app_id,

View File

@ -69,24 +69,13 @@ from ..common import NotifyImageSize
from ..common import NotifyType from ..common import NotifyType
from ..common import NotifyFormat from ..common import NotifyFormat
from ..utils import parse_bool from ..utils import parse_bool
from ..utils import validate_regex
from ..AppriseLocale import gettext_lazy as _ from ..AppriseLocale import gettext_lazy as _
# Used to prepare our UUID regex matching # Used to prepare our UUID regex matching
UUID4_RE = \ 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}' 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): class NotifyMSTeams(NotifyBase):
""" """
@ -124,26 +113,32 @@ class NotifyMSTeams(NotifyBase):
# Define our template tokens # Define our template tokens
template_tokens = dict(NotifyBase.template_tokens, **{ template_tokens = dict(NotifyBase.template_tokens, **{
# Token required as part of the API request
# /AAAAAAAAA@AAAAAAAAA/........./.........
'token_a': { 'token_a': {
'name': _('Token A'), 'name': _('Token A'),
'type': 'string', 'type': 'string',
'private': True, 'private': True,
'required': 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': { 'token_b': {
'name': _('Token B'), 'name': _('Token B'),
'type': 'string', 'type': 'string',
'private': True, 'private': True,
'required': 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': { 'token_c': {
'name': _('Token C'), 'name': _('Token C'),
'type': 'string', 'type': 'string',
'private': True, 'private': True,
'required': 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) super(NotifyMSTeams, self).__init__(**kwargs)
if not token_a: self.token_a = validate_regex(
msg = 'The first MSTeams API token is not specified.' 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) self.logger.warning(msg)
raise TypeError(msg) raise TypeError(msg)
if not token_b: self.token_b = validate_regex(
msg = 'The second MSTeams API token is not specified.' 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) self.logger.warning(msg)
raise TypeError(msg) raise TypeError(msg)
if not token_c: self.token_c = validate_regex(
msg = 'The third MSTeams API token is not specified.' 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) self.logger.warning(msg)
raise TypeError(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 # Place a thumbnail image inline with the message body
self.include_image = include_image self.include_image = include_image
return
def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs): def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
""" """
Perform Microsoft Teams Notification Perform Microsoft Teams Notification

View File

@ -57,6 +57,7 @@ from .NotifyBase import NotifyBase
from ..common import NotifyType from ..common import NotifyType
from ..utils import parse_list from ..utils import parse_list
from ..utils import is_email from ..utils import is_email
from ..utils import validate_regex
from ..AppriseLocale import gettext_lazy as _ from ..AppriseLocale import gettext_lazy as _
# Provide some known codes Mailgun uses and what they translate to: # Provide some known codes Mailgun uses and what they translate to:
@ -169,19 +170,17 @@ class NotifyMailgun(NotifyBase):
""" """
super(NotifyMailgun, self).__init__(**kwargs) super(NotifyMailgun, self).__init__(**kwargs)
try: # API Key (associated with project)
# The personal access apikey associated with the account self.apikey = validate_regex(apikey)
self.apikey = apikey.strip() if not self.apikey:
msg = 'An invalid Mailgun API Key ' \
except AttributeError: '({}) was specified.'.format(apikey)
# Token was None
msg = 'No API Key was specified.'
self.logger.warning(msg) self.logger.warning(msg)
raise TypeError(msg) raise TypeError(msg)
# Validate our username # Validate our username
if not self.user: if not self.user:
msg = 'No username was specified.' msg = 'No Mailgun username was specified.'
self.logger.warning(msg) self.logger.warning(msg)
raise TypeError(msg) raise TypeError(msg)
@ -198,7 +197,7 @@ class NotifyMailgun(NotifyBase):
raise raise
except: except:
# Invalid region specified # Invalid region specified
msg = 'The region specified ({}) is invalid.' \ msg = 'The Mailgun region specified ({}) is invalid.' \
.format(region_name) .format(region_name)
self.logger.warning(msg) self.logger.warning(msg)
raise TypeError(msg) raise TypeError(msg)

View File

@ -23,7 +23,6 @@
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE. # THE SOFTWARE.
import re
import six import six
import requests import requests
from json import dumps from json import dumps
@ -33,15 +32,13 @@ from ..common import NotifyImageSize
from ..common import NotifyType from ..common import NotifyType
from ..utils import parse_bool from ..utils import parse_bool
from ..utils import parse_list from ..utils import parse_list
from ..utils import validate_regex
from ..AppriseLocale import gettext_lazy as _ from ..AppriseLocale import gettext_lazy as _
# Some Reference Locations: # Some Reference Locations:
# - https://docs.mattermost.com/developer/webhooks-incoming.html # - https://docs.mattermost.com/developer/webhooks-incoming.html
# - https://docs.mattermost.com/administration/config-settings.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): class NotifyMatterMost(NotifyBase):
""" """
@ -97,7 +94,7 @@ class NotifyMatterMost(NotifyBase):
'authtoken': { 'authtoken': {
'name': _('Access Key'), 'name': _('Access Key'),
'type': 'string', 'type': 'string',
'regex': (r'[a-z0-9]{24,32}', 'i'), 'regex': (r'^[a-z0-9]{24,32}$', 'i'),
'private': True, 'private': True,
'required': True, 'required': True,
}, },
@ -152,17 +149,12 @@ class NotifyMatterMost(NotifyBase):
self.fullpath = '' if not isinstance( self.fullpath = '' if not isinstance(
fullpath, six.string_types) else fullpath.strip() fullpath, six.string_types) else fullpath.strip()
# Our Authorization Token # Authorization Token (associated with project)
self.authtoken = authtoken self.authtoken = validate_regex(
authtoken, *self.template_tokens['authtoken']['regex'])
# Validate authtoken if not self.authtoken:
if not authtoken: msg = 'An invalid MatterMost Authorization Token ' \
msg = 'Missing MatterMost Authorization Token.' '({}) was specified.'.format(authtoken)
self.logger.warning(msg)
raise TypeError(msg)
if not VALIDATE_AUTHTOKEN.match(authtoken):
msg = 'Invalid MatterMost Authorization Token Specified.'
self.logger.warning(msg) self.logger.warning(msg)
raise TypeError(msg) raise TypeError(msg)
@ -340,7 +332,6 @@ class NotifyMatterMost(NotifyBase):
# all entries before it will be our path # all entries before it will be our path
tokens = NotifyMatterMost.split_path(results['fullpath']) tokens = NotifyMatterMost.split_path(results['fullpath'])
# Apply our settings now
results['authtoken'] = None if not tokens else tokens.pop() results['authtoken'] = None if not tokens else tokens.pop()
# Store our path # Store our path

View File

@ -35,11 +35,9 @@ import requests
from .NotifyBase import NotifyBase from .NotifyBase import NotifyBase
from ..common import NotifyType from ..common import NotifyType
from ..utils import parse_list from ..utils import parse_list
from ..utils import validate_regex
from ..AppriseLocale import gettext_lazy as _ 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 # Some Phone Number Detection
IS_PHONE_NO = re.compile(r'^\+?(?P<phone>[0-9\s)(+-]+)\s*$') IS_PHONE_NO = re.compile(r'^\+?(?P<phone>[0-9\s)(+-]+)\s*$')
@ -83,20 +81,21 @@ class NotifyMessageBird(NotifyBase):
'name': _('API Key'), 'name': _('API Key'),
'type': 'string', 'type': 'string',
'required': True, 'required': True,
'regex': (r'[a-z0-9]{25}', 'i'), 'private': True,
'regex': (r'^[a-z0-9]{25}$', 'i'),
}, },
'source': { 'source': {
'name': _('Source Phone No'), 'name': _('Source Phone No'),
'type': 'string', 'type': 'string',
'prefix': '+', 'prefix': '+',
'required': True, 'required': True,
'regex': (r'[0-9\s)(+-]+', 'i'), 'regex': (r'^[0-9\s)(+-]+$', 'i'),
}, },
'target_phone': { 'target_phone': {
'name': _('Target Phone No'), 'name': _('Target Phone No'),
'type': 'string', 'type': 'string',
'prefix': '+', 'prefix': '+',
'regex': (r'[0-9\s)(+-]+', 'i'), 'regex': (r'^[0-9\s)(+-]+$', 'i'),
'map_to': 'targets', 'map_to': 'targets',
}, },
'targets': { 'targets': {
@ -121,19 +120,12 @@ class NotifyMessageBird(NotifyBase):
""" """
super(NotifyMessageBird, self).__init__(**kwargs) super(NotifyMessageBird, self).__init__(**kwargs)
try: # API Key (associated with project)
# The authentication key associated with the account self.apikey = validate_regex(
self.apikey = apikey.strip() apikey, *self.template_tokens['apikey']['regex'])
if not self.apikey:
except AttributeError: msg = 'An invalid MessageBird API Key ' \
# Token was None '({}) was specified.'.format(apikey)
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)
self.logger.warning(msg) self.logger.warning(msg)
raise TypeError(msg) raise TypeError(msg)
@ -158,7 +150,14 @@ class NotifyMessageBird(NotifyBase):
# Parse our targets # Parse our targets
self.targets = list() 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: # Validate targets and drop bad ones:
result = IS_PHONE_NO.match(target) result = IS_PHONE_NO.match(target)
if result: if result:
@ -180,6 +179,14 @@ class NotifyMessageBird(NotifyBase):
'({}) specified.'.format(target), '({}) 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): def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
""" """
Perform MessageBird Notification Perform MessageBird Notification
@ -202,13 +209,10 @@ class NotifyMessageBird(NotifyBase):
'body': body, 'body': body,
} }
# Create a copy of the targets list # Create a copy of the targets list
targets = list(self.targets) targets = list(self.targets)
if len(targets) == 0:
# No sources specified, use our own phone no
targets.append(self.source)
while len(targets): while len(targets):
# Get our target to notify # Get our target to notify
target = targets.pop(0) target = targets.pop(0)

View File

@ -36,12 +36,9 @@ from .NotifyBase import NotifyBase
from ..URLBase import PrivacyMode from ..URLBase import PrivacyMode
from ..common import NotifyType from ..common import NotifyType
from ..utils import parse_list from ..utils import parse_list
from ..utils import validate_regex
from ..AppriseLocale import gettext_lazy as _ 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 # Some Phone Number Detection
IS_PHONE_NO = re.compile(r'^\+?(?P<phone>[0-9\s)(+-]+)\s*$') IS_PHONE_NO = re.compile(r'^\+?(?P<phone>[0-9\s)(+-]+)\s*$')
@ -94,27 +91,28 @@ class NotifyNexmo(NotifyBase):
'name': _('API Key'), 'name': _('API Key'),
'type': 'string', 'type': 'string',
'required': True, 'required': True,
'regex': (r'AC[a-z0-9]{8}', 'i'), 'regex': (r'^AC[a-z0-9]{8}$', 'i'),
'private': True,
}, },
'secret': { 'secret': {
'name': _('API Secret'), 'name': _('API Secret'),
'type': 'string', 'type': 'string',
'private': True, 'private': True,
'required': True, 'required': True,
'regex': (r'[a-z0-9]{16}', 'i'), 'regex': (r'^[a-z0-9]{16}$', 'i'),
}, },
'from_phone': { 'from_phone': {
'name': _('From Phone No'), 'name': _('From Phone No'),
'type': 'string', 'type': 'string',
'required': True, 'required': True,
'regex': (r'\+?[0-9\s)(+-]+', 'i'), 'regex': (r'^\+?[0-9\s)(+-]+$', 'i'),
'map_to': 'source', 'map_to': 'source',
}, },
'target_phone': { 'target_phone': {
'name': _('Target Phone No'), 'name': _('Target Phone No'),
'type': 'string', 'type': 'string',
'prefix': '+', 'prefix': '+',
'regex': (r'[0-9\s)(+-]+', 'i'), 'regex': (r'^[0-9\s)(+-]+$', 'i'),
'map_to': 'targets', 'map_to': 'targets',
}, },
'targets': { 'targets': {
@ -153,35 +151,21 @@ class NotifyNexmo(NotifyBase):
""" """
super(NotifyNexmo, self).__init__(**kwargs) super(NotifyNexmo, self).__init__(**kwargs)
try: # API Key (associated with project)
# The Account SID associated with the account self.apikey = validate_regex(
self.apikey = apikey.strip() apikey, *self.template_tokens['apikey']['regex'])
if not self.apikey:
except AttributeError: msg = 'An invalid Nexmo API Key ' \
# Token was None '({}) was specified.'.format(apikey)
msg = 'No Nexmo APIKey was specified.'
self.logger.warning(msg) self.logger.warning(msg)
raise TypeError(msg) raise TypeError(msg)
if not VALIDATE_APIKEY.match(self.apikey): # API Secret (associated with project)
msg = 'The Nexmo API Key specified ({}) is invalid.'\ self.secret = validate_regex(
.format(self.apikey) secret, *self.template_tokens['secret']['regex'])
self.logger.warning(msg) if not self.secret:
raise TypeError(msg) msg = 'An invalid Nexmo API Secret ' \
'({}) was specified.'.format(secret)
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)
self.logger.warning(msg) self.logger.warning(msg)
raise TypeError(msg) raise TypeError(msg)
@ -242,6 +226,8 @@ class NotifyNexmo(NotifyBase):
'({}) specified.'.format(target), '({}) specified.'.format(target),
) )
return
def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs): def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
""" """
Perform Nexmo Notification Perform Nexmo Notification

View File

@ -23,19 +23,13 @@
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE. # THE SOFTWARE.
import re
import requests import requests
from .NotifyBase import NotifyBase from .NotifyBase import NotifyBase
from ..common import NotifyType from ..common import NotifyType
from ..utils import validate_regex
from ..AppriseLocale import gettext_lazy as _ 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 # Priorities
class ProwlPriority(object): class ProwlPriority(object):
@ -104,11 +98,13 @@ class NotifyProwl(NotifyBase):
'type': 'string', 'type': 'string',
'private': True, 'private': True,
'required': True, 'required': True,
'regex': (r'^[A-Za-z0-9]{40}$', 'i'),
}, },
'providerkey': { 'providerkey': {
'name': _('Provider Key'), 'name': _('Provider Key'),
'type': 'string', 'type': 'string',
'private': True, 'private': True,
'regex': (r'^[A-Za-z0-9]{40}$', 'i'),
}, },
}) })
@ -129,31 +125,35 @@ class NotifyProwl(NotifyBase):
super(NotifyProwl, self).__init__(**kwargs) super(NotifyProwl, self).__init__(**kwargs)
if priority not in PROWL_PRIORITIES: if priority not in PROWL_PRIORITIES:
self.priority = ProwlPriority.NORMAL self.priority = self.template_args['priority']['default']
else: else:
self.priority = priority self.priority = priority
if not VALIDATE_APIKEY.match(apikey): # API Key (associated with project)
msg = 'The API key specified ({}) is invalid.'.format(apikey) 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) self.logger.warning(msg)
raise TypeError(msg) raise TypeError(msg)
# Store the API key
self.apikey = apikey
# Store the provider key (if specified) # Store the provider key (if specified)
if providerkey: if providerkey:
if not VALIDATE_PROVIDERKEY.match(providerkey): self.providerkey = validate_regex(
msg = \ providerkey, *self.template_tokens['providerkey']['regex'])
'The Provider key specified ({}) is invalid.' \ if not self.providerkey:
.format(providerkey) msg = 'An invalid Prowl Provider Key ' \
'({}) was specified.'.format(providerkey)
self.logger.warning(msg) self.logger.warning(msg)
raise TypeError(msg) raise TypeError(msg)
# Store the Provider Key else:
self.providerkey = providerkey # No provider key was set
self.providerkey = None
return
def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs): def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
""" """

View File

@ -30,6 +30,7 @@ from .NotifyBase import NotifyBase
from ..utils import GET_EMAIL_RE from ..utils import GET_EMAIL_RE
from ..common import NotifyType from ..common import NotifyType
from ..utils import parse_list from ..utils import parse_list
from ..utils import validate_regex
from ..AppriseLocale import gettext_lazy as _ from ..AppriseLocale import gettext_lazy as _
# Flag used as a placeholder to sending to all devices # Flag used as a placeholder to sending to all devices
@ -110,12 +111,20 @@ class NotifyPushBullet(NotifyBase):
""" """
super(NotifyPushBullet, self).__init__(**kwargs) 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) self.targets = parse_list(targets)
if len(self.targets) == 0: if len(self.targets) == 0:
self.targets = (PUSHBULLET_SEND_TO_ALL, ) self.targets = (PUSHBULLET_SEND_TO_ALL, )
return
def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs): def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
""" """
Perform PushBullet Notification Perform PushBullet Notification

View File

@ -32,10 +32,11 @@ from .NotifyBase import NotifyBase
from ..URLBase import PrivacyMode from ..URLBase import PrivacyMode
from ..common import NotifyType from ..common import NotifyType
from ..utils import parse_list from ..utils import parse_list
from ..utils import validate_regex
from ..AppriseLocale import gettext_lazy as _ from ..AppriseLocale import gettext_lazy as _
# Used to detect and parse channels # Used to detect and parse channels
IS_CHANNEL = re.compile(r'^#(?P<name>[A-Za-z0-9]+)$') IS_CHANNEL = re.compile(r'^#?(?P<name>[A-Za-z0-9]+)$')
# Used to detect and parse a users push id # Used to detect and parse a users push id
IS_USER_PUSHED_ID = re.compile(r'^@(?P<name>[A-Za-z0-9]+)$') IS_USER_PUSHED_ID = re.compile(r'^@(?P<name>[A-Za-z0-9]+)$')
@ -121,13 +122,19 @@ class NotifyPushed(NotifyBase):
""" """
super(NotifyPushed, self).__init__(**kwargs) super(NotifyPushed, self).__init__(**kwargs)
if not app_key: # Application Key (associated with project)
msg = 'An invalid Application Key was specified.' 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) self.logger.warning(msg)
raise TypeError(msg) raise TypeError(msg)
if not app_secret: # Access Secret (associated with project)
msg = 'An invalid Application Secret was specified.' 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) self.logger.warning(msg)
raise TypeError(msg) raise TypeError(msg)
@ -137,28 +144,34 @@ class NotifyPushed(NotifyBase):
# Initialize user list # Initialize user list
self.users = list() self.users = list()
# Validate recipients and drop bad ones: # Get our targets
for target in parse_list(targets): targets = parse_list(targets)
result = IS_CHANNEL.match(target) if targets:
if result: # Validate recipients and drop bad ones:
# store valid device for target in targets:
self.channels.append(result.group('name')) result = IS_CHANNEL.match(target)
continue if result:
# store valid device
self.channels.append(result.group('name'))
continue
result = IS_USER_PUSHED_ID.match(target) result = IS_USER_PUSHED_ID.match(target)
if result: if result:
# store valid room # store valid room
self.users.append(result.group('name')) self.users.append(result.group('name'))
continue continue
self.logger.warning( self.logger.warning(
'Dropped invalid channel/userid ' 'Dropped invalid channel/userid '
'(%s) specified.' % target, '(%s) specified.' % target,
) )
# Store our data if len(self.channels) + len(self.users) == 0:
self.app_key = app_key # We have no valid channels or users to notify after
self.app_secret = app_secret # explicitly identifying at least one.
msg = 'No Pushed targets to notify.'
self.logger.warning(msg)
raise TypeError(msg)
return return
@ -325,8 +338,6 @@ class NotifyPushed(NotifyBase):
# We're done early as we couldn't load the results # We're done early as we couldn't load the results
return results return results
# Apply our settings now
# The first token is stored in the hostname # The first token is stored in the hostname
app_key = NotifyPushed.unquote(results['host']) app_key = NotifyPushed.unquote(results['host'])

View File

@ -29,6 +29,7 @@ from json import dumps
from .NotifyBase import NotifyBase from .NotifyBase import NotifyBase
from ..URLBase import PrivacyMode from ..URLBase import PrivacyMode
from ..common import NotifyType from ..common import NotifyType
from ..utils import validate_regex
from ..AppriseLocale import gettext_lazy as _ from ..AppriseLocale import gettext_lazy as _
@ -107,14 +108,15 @@ class NotifyPushjet(NotifyBase):
""" """
super(NotifyPushjet, self).__init__(**kwargs) super(NotifyPushjet, self).__init__(**kwargs)
if not secret_key: # Secret Key (associated with project)
# You must provide a Pushjet key to work with self.secret_key = validate_regex(secret_key)
msg = 'You must specify a Pushjet Secret Key.' if not self.secret_key:
msg = 'An invalid Pushjet Secret Key ' \
'({}) was specified.'.format(secret_key)
self.logger.warning(msg) self.logger.warning(msg)
raise TypeError(msg) raise TypeError(msg)
# store our key return
self.secret_key = secret_key
def url(self, privacy=False, *args, **kwargs): def url(self, privacy=False, *args, **kwargs):
""" """
@ -125,9 +127,6 @@ class NotifyPushjet(NotifyBase):
args = { args = {
'format': self.notify_format, 'format': self.notify_format,
'overflow': self.overflow_mode, 'overflow': self.overflow_mode,
'secret': self.pprint(
self.secret_key, privacy,
mode=PrivacyMode.Secret, quote=False),
'verify': 'yes' if self.verify_certificate else 'no', 'verify': 'yes' if self.verify_certificate else 'no',
} }
@ -142,12 +141,14 @@ class NotifyPushjet(NotifyBase):
self.password, privacy, mode=PrivacyMode.Secret, safe=''), 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, schema=self.secure_protocol if self.secure else self.protocol,
auth=auth, auth=auth,
hostname=NotifyPushjet.quote(self.host, safe=''), hostname=NotifyPushjet.quote(self.host, safe=''),
port='' if self.port is None or self.port == default_port port='' if self.port is None or self.port == default_port
else ':{}'.format(self.port), else ':{}'.format(self.port),
secret=self.pprint(
self.secret_key, privacy, mode=PrivacyMode.Secret, safe=''),
args=NotifyPushjet.urlencode(args), args=NotifyPushjet.urlencode(args),
) )
@ -273,7 +274,7 @@ class NotifyPushjet(NotifyBase):
# through it in addition to supporting the secret key # through it in addition to supporting the secret key
if 'secret' in results['qsd'] and len(results['qsd']['secret']): if 'secret' in results['qsd'] and len(results['qsd']['secret']):
results['secret_key'] = \ results['secret_key'] = \
NotifyPushjet.parse_list(results['qsd']['secret']) NotifyPushjet.unquote(results['qsd']['secret'])
if results.get('secret_key') is None: if results.get('secret_key') is None:
# Deprication Notice issued for v0.7.9 # Deprication Notice issued for v0.7.9

View File

@ -30,18 +30,13 @@ import requests
from .NotifyBase import NotifyBase from .NotifyBase import NotifyBase
from ..common import NotifyType from ..common import NotifyType
from ..utils import parse_list from ..utils import parse_list
from ..utils import validate_regex
from ..AppriseLocale import gettext_lazy as _ from ..AppriseLocale import gettext_lazy as _
# Flag used as a placeholder to sending to all devices # Flag used as a placeholder to sending to all devices
PUSHOVER_SEND_TO_ALL = 'ALL_DEVICES' PUSHOVER_SEND_TO_ALL = 'ALL_DEVICES'
# Used to validate API Key # Used to detect a Device
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
VALIDATE_DEVICE = re.compile(r'^[a-z0-9_]{1,25}$', re.I) VALIDATE_DEVICE = re.compile(r'^[a-z0-9_]{1,25}$', re.I)
@ -158,20 +153,19 @@ class NotifyPushover(NotifyBase):
'type': 'string', 'type': 'string',
'private': True, 'private': True,
'required': True, 'required': True,
'regex': (r'[a-z0-9]{30}', 'i'), 'regex': (r'^[a-z0-9]{30}$', 'i'),
'map_to': 'user',
}, },
'token': { 'token': {
'name': _('Access Token'), 'name': _('Access Token'),
'type': 'string', 'type': 'string',
'private': True, 'private': True,
'required': True, 'required': True,
'regex': (r'[a-z0-9]{30}', 'i'), 'regex': (r'^[a-z0-9]{30}$', 'i'),
}, },
'target_device': { 'target_device': {
'name': _('Target Device'), 'name': _('Target Device'),
'type': 'string', 'type': 'string',
'regex': (r'[a-z0-9_]{1,25}', 'i'), 'regex': (r'^[a-z0-9_]{1,25}$', 'i'),
'map_to': 'targets', 'map_to': 'targets',
}, },
'targets': { 'targets': {
@ -191,7 +185,7 @@ class NotifyPushover(NotifyBase):
'sound': { 'sound': {
'name': _('Sound'), 'name': _('Sound'),
'type': 'string', 'type': 'string',
'regex': (r'[a-z]{1,12}', 'i'), 'regex': (r'^[a-z]{1,12}$', 'i'),
'default': PushoverSound.PUSHOVER, 'default': PushoverSound.PUSHOVER,
}, },
'retry': { 'retry': {
@ -212,26 +206,28 @@ class NotifyPushover(NotifyBase):
}, },
}) })
def __init__(self, token, targets=None, priority=None, sound=None, def __init__(self, user_key, token, targets=None, priority=None,
retry=None, expire=None, sound=None, retry=None, expire=None, **kwargs):
**kwargs):
""" """
Initialize Pushover Object Initialize Pushover Object
""" """
super(NotifyPushover, self).__init__(**kwargs) super(NotifyPushover, self).__init__(**kwargs)
try: # Access Token (associated with project)
# The token associated with the account self.token = validate_regex(
self.token = token.strip() token, *self.template_tokens['token']['regex'])
if not self.token:
except AttributeError: msg = 'An invalid Pushover Access Token ' \
# Token was None '({}) was specified.'.format(token)
msg = 'No API Token was specified.'
self.logger.warning(msg) self.logger.warning(msg)
raise TypeError(msg) raise TypeError(msg)
if not VALIDATE_TOKEN.match(self.token): # User Key (associated with project)
msg = 'The API Token specified (%s) is invalid.'.format(token) 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) self.logger.warning(msg)
raise TypeError(msg) raise TypeError(msg)
@ -249,7 +245,7 @@ class NotifyPushover(NotifyBase):
# The Priority of the message # The Priority of the message
if priority not in PUSHOVER_PRIORITIES: if priority not in PUSHOVER_PRIORITIES:
self.priority = PushoverPriority.NORMAL self.priority = self.template_args['priority']['default']
else: else:
self.priority = priority self.priority = priority
@ -258,7 +254,7 @@ class NotifyPushover(NotifyBase):
if self.priority == PushoverPriority.EMERGENCY: if self.priority == PushoverPriority.EMERGENCY:
# How often to resend notification, in seconds # How often to resend notification, in seconds
self.retry = NotifyPushover.template_args['retry']['default'] self.retry = self.template_args['retry']['default']
try: try:
self.retry = int(retry) self.retry = int(retry)
except (ValueError, TypeError): except (ValueError, TypeError):
@ -266,7 +262,7 @@ class NotifyPushover(NotifyBase):
pass pass
# How often to resend notification, in seconds # How often to resend notification, in seconds
self.expire = NotifyPushover.template_args['expire']['default'] self.expire = self.template_args['expire']['default']
try: try:
self.expire = int(expire) self.expire = int(expire)
except (ValueError, TypeError): except (ValueError, TypeError):
@ -274,23 +270,16 @@ class NotifyPushover(NotifyBase):
pass pass
if self.retry < 30: if self.retry < 30:
msg = 'Retry must be at least 30.' msg = 'Pushover retry must be at least 30 seconds.'
self.logger.warning(msg) self.logger.warning(msg)
raise TypeError(msg) raise TypeError(msg)
if self.expire < 0 or self.expire > 10800: 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) self.logger.warning(msg)
raise TypeError(msg) raise TypeError(msg)
return
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)
def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs): def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
""" """
@ -323,7 +312,7 @@ class NotifyPushover(NotifyBase):
# prepare JSON Object # prepare JSON Object
payload = { payload = {
'token': self.token, 'token': self.token,
'user': self.user, 'user': self.user_key,
'priority': str(self.priority), 'priority': str(self.priority),
'title': title, 'title': title,
'message': body, 'message': body,
@ -406,8 +395,8 @@ class NotifyPushover(NotifyBase):
'format': self.notify_format, 'format': self.notify_format,
'overflow': self.overflow_mode, 'overflow': self.overflow_mode,
'priority': 'priority':
_map[PushoverPriority.NORMAL] if self.priority not in _map _map[self.template_args['priority']['default']]
else _map[self.priority], if self.priority not in _map else _map[self.priority],
'verify': 'yes' if self.verify_certificate else 'no', 'verify': 'yes' if self.verify_certificate else 'no',
} }
# Only add expire and retry for emergency messages, # Only add expire and retry for emergency messages,
@ -426,7 +415,7 @@ class NotifyPushover(NotifyBase):
return '{schema}://{user_key}@{token}/{devices}/?{args}'.format( return '{schema}://{user_key}@{token}/{devices}/?{args}'.format(
schema=self.secure_protocol, 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=''), token=self.pprint(self.token, privacy, safe=''),
devices=devices, devices=devices,
args=NotifyPushover.urlencode(args)) args=NotifyPushover.urlencode(args))
@ -464,6 +453,9 @@ class NotifyPushover(NotifyBase):
# Retrieve all of our targets # Retrieve all of our targets
results['targets'] = NotifyPushover.split_path(results['fullpath']) results['targets'] = NotifyPushover.split_path(results['fullpath'])
# User Key is retrieved from the user
results['user_key'] = NotifyPushover.unquote(results['user'])
# Get the sound # Get the sound
if 'sound' in results['qsd'] and len(results['qsd']['sound']): if 'sound' in results['qsd'] and len(results['qsd']['sound']):
results['sound'] = \ results['sound'] = \

View File

@ -40,14 +40,9 @@ from .NotifyBase import NotifyBase
from ..common import NotifyImageSize from ..common import NotifyImageSize
from ..common import NotifyType from ..common import NotifyType
from ..utils import parse_bool from ..utils import parse_bool
from ..utils import validate_regex
from ..AppriseLocale import gettext_lazy as _ 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): class RyverWebhookMode(object):
""" """
@ -99,12 +94,14 @@ class NotifyRyver(NotifyBase):
'name': _('Organization'), 'name': _('Organization'),
'type': 'string', 'type': 'string',
'required': True, 'required': True,
'regex': (r'^[A-Z0-9_-]{3,32}$', 'i'),
}, },
'token': { 'token': {
'name': _('Token'), 'name': _('Token'),
'type': 'string', 'type': 'string',
'required': True, 'required': True,
'private': True, 'private': True,
'regex': (r'^[A-Z0-9]{15}$', 'i'),
}, },
'user': { 'user': {
'name': _('Bot Name'), 'name': _('Bot Name'),
@ -135,25 +132,21 @@ class NotifyRyver(NotifyBase):
""" """
super(NotifyRyver, self).__init__(**kwargs) super(NotifyRyver, self).__init__(**kwargs)
if not token: # API Token (associated with project)
msg = 'No Ryver token was specified.' 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) self.logger.warning(msg)
raise TypeError(msg) raise TypeError(msg)
if not organization: # Organization (associated with project)
msg = 'No Ryver organization was specified.' self.organization = validate_regex(
self.logger.warning(msg) organization, *self.template_tokens['organization']['regex'])
raise TypeError(msg) if not self.organization:
msg = 'An invalid Ryver Organization ' \
if not VALIDATE_TOKEN.match(token.strip()): '({}) was specified.'.format(organization)
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)
self.logger.warning(msg) self.logger.warning(msg)
raise TypeError(msg) raise TypeError(msg)
@ -167,12 +160,6 @@ class NotifyRyver(NotifyBase):
self.logger.warning(msg) self.logger.warning(msg)
raise TypeError(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 # Place an image inline with the message body
self.include_image = include_image self.include_image = include_image
@ -193,6 +180,8 @@ class NotifyRyver(NotifyBase):
re.IGNORECASE, re.IGNORECASE,
) )
return
def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs): def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
""" """
Perform Ryver Notification Perform Ryver Notification

View File

@ -36,6 +36,7 @@ from .NotifyBase import NotifyBase
from ..URLBase import PrivacyMode from ..URLBase import PrivacyMode
from ..common import NotifyType from ..common import NotifyType
from ..utils import parse_list from ..utils import parse_list
from ..utils import validate_regex
from ..AppriseLocale import gettext_lazy as _ from ..AppriseLocale import gettext_lazy as _
# Some Phone Number Detection # Some Phone Number Detection
@ -117,21 +118,21 @@ class NotifySNS(NotifyBase):
'name': _('Region'), 'name': _('Region'),
'type': 'string', 'type': 'string',
'required': True, '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', 'map_to': 'region_name',
}, },
'target_phone_no': { 'target_phone_no': {
'name': _('Target Phone No'), 'name': _('Target Phone No'),
'type': 'string', 'type': 'string',
'map_to': 'targets', 'map_to': 'targets',
'regex': (r'[0-9\s)(+-]+', 'i') 'regex': (r'^[0-9\s)(+-]+$', 'i')
}, },
'target_topic': { 'target_topic': {
'name': _('Target Topic'), 'name': _('Target Topic'),
'type': 'string', 'type': 'string',
'map_to': 'targets', 'map_to': 'targets',
'prefix': '#', 'prefix': '#',
'regex': (r'[A-Za-z0-9_-]+', 'i'), 'regex': (r'^[A-Za-z0-9_-]+$', 'i'),
}, },
'targets': { 'targets': {
'name': _('Targets'), 'name': _('Targets'),
@ -153,18 +154,28 @@ class NotifySNS(NotifyBase):
""" """
super(NotifySNS, self).__init__(**kwargs) 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.' msg = 'An invalid AWS Access Key ID was specified.'
self.logger.warning(msg) self.logger.warning(msg)
raise TypeError(msg) raise TypeError(msg)
if not secret_access_key: # Store our AWS API Secret Access key
msg = 'An invalid AWS Secret Access Key was specified.' 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) self.logger.warning(msg)
raise TypeError(msg) raise TypeError(msg)
if not (region_name and IS_REGION.match(region_name)): # Acquire our AWS Region Name:
msg = 'An invalid AWS Region was specified.' # 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) self.logger.warning(msg)
raise TypeError(msg) raise TypeError(msg)
@ -174,16 +185,6 @@ class NotifySNS(NotifyBase):
# Initialize numbers list # Initialize numbers list
self.phone = 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 # Set our notify_url based on our region
self.notify_url = 'https://sns.{}.amazonaws.com/'\ self.notify_url = 'https://sns.{}.amazonaws.com/'\
.format(self.aws_region_name) .format(self.aws_region_name)
@ -231,8 +232,12 @@ class NotifySNS(NotifyBase):
) )
if len(self.phone) == 0 and len(self.topics) == 0: if len(self.phone) == 0 and len(self.topics) == 0:
self.logger.warning( # We have a bot token and no target(s) to message
'There are no valid target(s) identified to notify.') msg = 'No AWS targets to notify.'
self.logger.warning(msg)
raise TypeError(msg)
return
def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs): def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
""" """

View File

@ -43,7 +43,6 @@
# - https://sendgrid.com/docs/ui/sending-email/\ # - https://sendgrid.com/docs/ui/sending-email/\
# how-to-send-an-email-with-dynamic-transactional-templates/ # how-to-send-an-email-with-dynamic-transactional-templates/
import re
import requests import requests
from json import dumps from json import dumps
@ -52,10 +51,9 @@ from ..common import NotifyFormat
from ..common import NotifyType from ..common import NotifyType
from ..utils import parse_list from ..utils import parse_list
from ..utils import GET_EMAIL_RE from ..utils import GET_EMAIL_RE
from ..utils import validate_regex
from ..AppriseLocale import gettext_lazy as _ from ..AppriseLocale import gettext_lazy as _
IS_APIKEY_RE = re.compile(r'^([A-Z0-9._-]+)$', re.I)
# Extend HTTP Error Messages # Extend HTTP Error Messages
SENDGRID_HTTP_ERROR_MAP = { SENDGRID_HTTP_ERROR_MAP = {
401: 'Unauthorized - You do not have authorization to make the request.', 401: 'Unauthorized - You do not have authorization to make the request.',
@ -109,6 +107,7 @@ class NotifySendGrid(NotifyBase):
'type': 'string', 'type': 'string',
'private': True, 'private': True,
'required': True, 'required': True,
'regex': (r'^[A-Z0-9._-]+$', 'i'),
}, },
'from_email': { 'from_email': {
'name': _('Source Email'), 'name': _('Source Email'),
@ -162,16 +161,12 @@ class NotifySendGrid(NotifyBase):
""" """
super(NotifySendGrid, self).__init__(**kwargs) super(NotifySendGrid, self).__init__(**kwargs)
# The API Key needed to perform all SendMail API i/o # API Key (associated with project)
self.apikey = apikey self.apikey = validate_regex(
try: apikey, *self.template_tokens['apikey']['regex'])
result = IS_APIKEY_RE.match(self.apikey) if not self.apikey:
if not result: msg = 'An invalid SendGrid API Key ' \
# let outer exception handle this '({}) was specified.'.format(apikey)
raise TypeError
except (TypeError, AttributeError):
msg = 'Invalid API Key specified: {}'.format(self.apikey)
self.logger.warning(msg) self.logger.warning(msg)
raise TypeError(msg) raise TypeError(msg)

View File

@ -29,6 +29,7 @@ import requests
from .NotifyBase import NotifyBase from .NotifyBase import NotifyBase
from ..URLBase import PrivacyMode from ..URLBase import PrivacyMode
from ..common import NotifyType from ..common import NotifyType
from ..utils import validate_regex
from ..AppriseLocale import gettext_lazy as _ from ..AppriseLocale import gettext_lazy as _
# Default our global support flag # Default our global support flag
@ -120,11 +121,26 @@ class NotifySimplePush(NotifyBase):
""" """
super(NotifySimplePush, self).__init__(**kwargs) super(NotifySimplePush, self).__init__(**kwargs)
# Store the API key # API Key (associated with project)
self.apikey = apikey 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 if event:
self.event = 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) # Encrypt Message (providing support is available)
if self.password and self.user and not CRYPTOGRAPHY_AVAILABLE: if self.password and self.user and not CRYPTOGRAPHY_AVAILABLE:
@ -182,7 +198,6 @@ class NotifySimplePush(NotifyBase):
payload = { payload = {
'key': self.apikey, 'key': self.apikey,
} }
event = self.event
if self.password and self.user and CRYPTOGRAPHY_AVAILABLE: if self.password and self.user and CRYPTOGRAPHY_AVAILABLE:
body = self._encrypt(body) body = self._encrypt(body)
@ -198,8 +213,9 @@ class NotifySimplePush(NotifyBase):
'title': title, 'title': title,
}) })
if event: if self.event:
payload['event'] = event # Store Event
payload['event'] = self.event
self.logger.debug('SimplePush POST URL: %s (cert_verify=%r)' % ( self.logger.debug('SimplePush POST URL: %s (cert_verify=%r)' % (
self.notify_url, self.verify_certificate, self.notify_url, self.verify_certificate,

View File

@ -46,20 +46,9 @@ from ..common import NotifyType
from ..common import NotifyFormat from ..common import NotifyFormat
from ..utils import parse_bool from ..utils import parse_bool
from ..utils import parse_list from ..utils import parse_list
from ..utils import validate_regex
from ..AppriseLocale import gettext_lazy as _ 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 # Extend HTTP Error Messages
SLACK_HTTP_ERROR_MAP = { SLACK_HTTP_ERROR_MAP = {
401: 'Unauthorized - Invalid Token.', 401: 'Unauthorized - Invalid Token.',
@ -68,9 +57,6 @@ SLACK_HTTP_ERROR_MAP = {
# Used to break path apart into list of channels # Used to break path apart into list of channels
CHANNEL_LIST_DELIM = re.compile(r'[ \t\r\n,#\\/]+') 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): class NotifySlack(NotifyBase):
""" """
@ -116,26 +102,32 @@ class NotifySlack(NotifyBase):
'type': 'string', 'type': 'string',
'map_to': 'user', 'map_to': 'user',
}, },
# Token required as part of the API request
# /AAAAAAAAA/........./........................
'token_a': { 'token_a': {
'name': _('Token A'), 'name': _('Token A'),
'type': 'string', 'type': 'string',
'private': True, 'private': True,
'required': 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': { 'token_b': {
'name': _('Token B'), 'name': _('Token B'),
'type': 'string', 'type': 'string',
'private': True, 'private': True,
'required': 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': { 'token_c': {
'name': _('Token C'), 'name': _('Token C'),
'type': 'string', 'type': 'string',
'private': True, 'private': True,
'required': True, 'required': True,
'regex': (r'[A-Za-z0-9]{24}', 'i'), 'regex': (r'^[A-Za-z0-9]{24}$', 'i'),
}, },
'target_encoded_id': { 'target_encoded_id': {
'name': _('Target Encoded ID'), 'name': _('Target Encoded ID'),
@ -181,48 +173,30 @@ class NotifySlack(NotifyBase):
""" """
super(NotifySlack, self).__init__(**kwargs) super(NotifySlack, self).__init__(**kwargs)
if not token_a: self.token_a = validate_regex(
msg = 'The first API token is not specified.' 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) self.logger.warning(msg)
raise TypeError(msg) raise TypeError(msg)
if not token_b: self.token_b = validate_regex(
msg = 'The second API token is not specified.' 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) self.logger.warning(msg)
raise TypeError(msg) raise TypeError(msg)
if not token_c: self.token_c = validate_regex(
msg = 'The third API token is not specified.' 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) self.logger.warning(msg)
raise TypeError(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: if not self.user:
self.logger.warning( self.logger.warning(
'No user was specified; using "%s".' % self.app_id) '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 # Place a thumbnail image inline with the message body
self.include_image = include_image self.include_image = include_image
return
def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs): def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
""" """
Perform Slack Notification Perform Slack Notification
@ -303,27 +279,30 @@ class NotifySlack(NotifyBase):
channel = channels.pop(0) channel = channels.pop(0)
if channel is not None: if channel is not None:
# Channel over-ride was specified _channel = validate_regex(
if not IS_VALID_TARGET_RE.match(channel): channel, r'[+#@]?([A-Z0-9_]{1,32})')
if not _channel:
# Channel over-ride was specified
self.logger.warning( self.logger.warning(
"The specified target {} is invalid;" "The specified target {} is invalid;"
"skipping.".format(channel)) "skipping.".format(_channel))
# Mark our failure # Mark our failure
has_error = True has_error = True
continue continue
if len(channel) > 1 and channel[0] == '+': if len(_channel) > 1 and _channel[0] == '+':
# Treat as encoded id if prefixed with a + # 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' # Treat @ value 'as is'
payload['channel'] = channel payload['channel'] = _channel
else: else:
# Prefix with channel hash tag # Prefix with channel hash tag
payload['channel'] = '#%s' % channel payload['channel'] = '#{}'.format(_channel)
# Acquire our to-be footer icon if configured to do so # Acquire our to-be footer icon if configured to do so
image_url = None if not self.include_image \ image_url = None if not self.include_image \
@ -478,9 +457,9 @@ class NotifySlack(NotifyBase):
result = re.match( result = re.match(
r'^https?://hooks\.slack\.com/services/' r'^https?://hooks\.slack\.com/services/'
r'(?P<token_a>[A-Z0-9]{9})/' r'(?P<token_a>[A-Z0-9]+)/'
r'(?P<token_b>[A-Z0-9]{9})/' r'(?P<token_b>[A-Z0-9]+)/'
r'(?P<token_c>[A-Z0-9]{24})/?' r'(?P<token_c>[A-Z0-9]+)/?'
r'(?P<args>\?[.+])?$', url, re.I) r'(?P<args>\?[.+])?$', url, re.I)
if result: if result:

View File

@ -47,12 +47,12 @@
# - https://push.techulus.com/ - Main Website # - https://push.techulus.com/ - Main Website
# - https://pushtechulus.docs.apiary.io - API Documentation # - https://pushtechulus.docs.apiary.io - API Documentation
import re
import requests import requests
from json import dumps from json import dumps
from .NotifyBase import NotifyBase from .NotifyBase import NotifyBase
from ..common import NotifyType from ..common import NotifyType
from ..utils import validate_regex
from ..AppriseLocale import gettext_lazy as _ from ..AppriseLocale import gettext_lazy as _
# Token required as part of the API request # Token required as part of the API request
@ -60,9 +60,6 @@ from ..AppriseLocale import gettext_lazy as _
UUID4_RE = \ 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}' 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): class NotifyTechulusPush(NotifyBase):
""" """
@ -99,7 +96,7 @@ class NotifyTechulusPush(NotifyBase):
'type': 'string', 'type': 'string',
'private': True, 'private': True,
'required': 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) 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 # 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): def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
""" """

View File

@ -61,17 +61,11 @@ from ..common import NotifyImageSize
from ..common import NotifyFormat from ..common import NotifyFormat
from ..utils import parse_bool from ..utils import parse_bool
from ..utils import parse_list from ..utils import parse_list
from ..utils import validate_regex
from ..AppriseLocale import gettext_lazy as _ from ..AppriseLocale import gettext_lazy as _
TELEGRAM_IMAGE_XY = NotifyImageSize.XY_256 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<key>[0-9]+:[a-z0-9_-]+)/*$',
re.IGNORECASE,
)
# Chat ID is required # Chat ID is required
# If the Chat ID is positive, then it's addressed to a single person # If the Chat ID is positive, then it's addressed to a single person
# If the Chat ID is negative, then it's targeting a group # If the Chat ID is negative, then it's targeting a group
@ -119,14 +113,16 @@ class NotifyTelegram(NotifyBase):
'type': 'string', 'type': 'string',
'private': True, 'private': True,
'required': 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<key>[0-9]+:[a-z0-9_-]+)$', 'i'),
}, },
'target_user': { 'target_user': {
'name': _('Target Chat ID'), 'name': _('Target Chat ID'),
'type': 'string', 'type': 'string',
'map_to': 'targets', 'map_to': 'targets',
'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': { 'targets': {
'name': _('Targets'), 'name': _('Targets'),
@ -160,24 +156,15 @@ class NotifyTelegram(NotifyBase):
""" """
super(NotifyTelegram, self).__init__(**kwargs) super(NotifyTelegram, self).__init__(**kwargs)
try: self.bot_token = validate_regex(
self.bot_token = bot_token.strip() bot_token, *self.template_tokens['bot_token']['regex'],
fmt='{key}')
except AttributeError: if not self.bot_token:
# Token was None err = 'The Telegram Bot Token specified ({}) is invalid.'.format(
err = 'No Bot Token was specified.' bot_token)
self.logger.warning(err) self.logger.warning(err)
raise TypeError(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 # Parse our list
self.targets = parse_list(targets) self.targets = parse_list(targets)

View File

@ -48,13 +48,10 @@ from .NotifyBase import NotifyBase
from ..URLBase import PrivacyMode from ..URLBase import PrivacyMode
from ..common import NotifyType from ..common import NotifyType
from ..utils import parse_list from ..utils import parse_list
from ..utils import validate_regex
from ..AppriseLocale import gettext_lazy as _ 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 # Some Phone Number Detection
IS_PHONE_NO = re.compile(r'^\+?(?P<phone>[0-9\s)(+-]+)\s*$') IS_PHONE_NO = re.compile(r'^\+?(?P<phone>[0-9\s)(+-]+)\s*$')
@ -108,33 +105,33 @@ class NotifyTwilio(NotifyBase):
'type': 'string', 'type': 'string',
'private': True, 'private': True,
'required': True, 'required': True,
'regex': (r'AC[a-f0-9]{32}', 'i'), 'regex': (r'^AC[a-f0-9]+$', 'i'),
}, },
'auth_token': { 'auth_token': {
'name': _('Auth Token'), 'name': _('Auth Token'),
'type': 'string', 'type': 'string',
'private': True, 'private': True,
'required': True, 'required': True,
'regex': (r'[a-f0-9]{32}', 'i'), 'regex': (r'^[a-f0-9]+$', 'i'),
}, },
'from_phone': { 'from_phone': {
'name': _('From Phone No'), 'name': _('From Phone No'),
'type': 'string', 'type': 'string',
'required': True, 'required': True,
'regex': (r'\+?[0-9\s)(+-]+', 'i'), 'regex': (r'^\+?[0-9\s)(+-]+$', 'i'),
'map_to': 'source', 'map_to': 'source',
}, },
'target_phone': { 'target_phone': {
'name': _('Target Phone No'), 'name': _('Target Phone No'),
'type': 'string', 'type': 'string',
'prefix': '+', 'prefix': '+',
'regex': (r'[0-9\s)(+-]+', 'i'), 'regex': (r'^[0-9\s)(+-]+$', 'i'),
'map_to': 'targets', 'map_to': 'targets',
}, },
'short_code': { 'short_code': {
'name': _('Target Short Code'), 'name': _('Target Short Code'),
'type': 'string', 'type': 'string',
'regex': (r'[0-9]{5,6}', 'i'), 'regex': (r'^[0-9]{5,6}$', 'i'),
'map_to': 'targets', 'map_to': 'targets',
}, },
'targets': { 'targets': {
@ -166,35 +163,21 @@ class NotifyTwilio(NotifyBase):
""" """
super(NotifyTwilio, self).__init__(**kwargs) super(NotifyTwilio, self).__init__(**kwargs)
try: # The Account SID associated with the account
# The Account SID associated with the account self.account_sid = validate_regex(
self.account_sid = account_sid.strip() account_sid, *self.template_tokens['account_sid']['regex'])
if not self.account_sid:
except AttributeError: msg = 'An invalid Twilio Account SID ' \
# Token was None '({}) was specified.'.format(account_sid)
msg = 'No Account SID was specified.'
self.logger.warning(msg) self.logger.warning(msg)
raise TypeError(msg) raise TypeError(msg)
if not VALIDATE_ACCOUNT_SID.match(self.account_sid): # The Authentication Token associated with the account
msg = 'The Account SID specified ({}) is invalid.' \ self.auth_token = validate_regex(
.format(account_sid) auth_token, *self.template_tokens['auth_token']['regex'])
self.logger.warning(msg) if not self.auth_token:
raise TypeError(msg) msg = 'An invalid Twilio Authentication Token ' \
'({}) was specified.'.format(auth_token)
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)
self.logger.warning(msg) self.logger.warning(msg)
raise TypeError(msg) raise TypeError(msg)
@ -254,14 +237,16 @@ class NotifyTwilio(NotifyBase):
'({}) specified.'.format(target), '({}) specified.'.format(target),
) )
if len(self.targets) == 0: if not self.targets:
msg = 'There are no valid targets identified to notify.'
if len(self.source) in (5, 6): if len(self.source) in (5, 6):
# raise a warning since we're a short-code. We need # raise a warning since we're a short-code. We need
# a number to message # a number to message
msg = 'There are no valid Twilio targets to notify.'
self.logger.warning(msg) self.logger.warning(msg)
raise TypeError(msg) raise TypeError(msg)
return
def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs): def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
""" """
Perform Twilio Notification Perform Twilio Notification

View File

@ -37,6 +37,7 @@ from ..URLBase import PrivacyMode
from ..common import NotifyType from ..common import NotifyType
from ..utils import parse_list from ..utils import parse_list
from ..utils import parse_bool from ..utils import parse_bool
from ..utils import validate_regex
from ..AppriseLocale import gettext_lazy as _ from ..AppriseLocale import gettext_lazy as _
IS_USER = re.compile(r'^\s*@?(?P<user>[A-Z0-9_]+)$', re.I) IS_USER = re.compile(r'^\s*@?(?P<user>[A-Z0-9_]+)$', re.I)
@ -186,23 +187,27 @@ class NotifyTwitter(NotifyBase):
""" """
super(NotifyTwitter, self).__init__(**kwargs) super(NotifyTwitter, self).__init__(**kwargs)
if not ckey: self.ckey = validate_regex(ckey)
msg = 'An invalid Consumer API Key was specified.' if not self.ckey:
msg = 'An invalid Twitter Consumer Key was specified.'
self.logger.warning(msg) self.logger.warning(msg)
raise TypeError(msg) raise TypeError(msg)
if not csecret: self.csecret = validate_regex(csecret)
msg = 'An invalid Consumer Secret API Key was specified.' if not self.csecret:
msg = 'An invalid Twitter Consumer Secret was specified.'
self.logger.warning(msg) self.logger.warning(msg)
raise TypeError(msg) raise TypeError(msg)
if not akey: self.akey = validate_regex(akey)
msg = 'An invalid Access Token API Key was specified.' if not self.akey:
msg = 'An invalid Twitter Access Key was specified.'
self.logger.warning(msg) self.logger.warning(msg)
raise TypeError(msg) raise TypeError(msg)
if not asecret: self.asecret = validate_regex(asecret)
msg = 'An invalid Access Token Secret API Key was specified.' if not self.asecret:
msg = 'An invalid Access Secret was specified.'
self.logger.warning(msg) self.logger.warning(msg)
raise TypeError(msg) raise TypeError(msg)
@ -219,6 +224,9 @@ class NotifyTwitter(NotifyBase):
self.logger.warning(msg) self.logger.warning(msg)
raise TypeError(msg) raise TypeError(msg)
# Track any errors
has_error = False
# Identify our targets # Identify our targets
self.targets = [] self.targets = []
for target in parse_list(targets): for target in parse_list(targets):
@ -227,15 +235,19 @@ class NotifyTwitter(NotifyBase):
self.targets.append(match.group('user')) self.targets.append(match.group('user'))
continue continue
has_error = True
self.logger.warning( self.logger.warning(
'Dropped invalid user ({}) specified.'.format(target), 'Dropped invalid user ({}) specified.'.format(target),
) )
# Store our data if has_error and not self.targets:
self.ckey = ckey # We have specified that we want to notify one or more individual
self.csecret = csecret # and we failed to load any of them. Since it's also valid to
self.akey = akey # notify no one at all (which means we notify ourselves), it's
self.asecret = asecret # important we don't switch from the users original intentions
msg = 'No Twitter targets to notify.'
self.logger.warning(msg)
raise TypeError(msg)
return 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) \ targets = self._whoami(lazy=self.cache) if not len(self.targets) \
else self._user_lookup(self.targets, lazy=self.cache) else self._user_lookup(self.targets, lazy=self.cache)

View File

@ -63,11 +63,9 @@ from json import dumps
from .NotifyBase import NotifyBase from .NotifyBase import NotifyBase
from ..common import NotifyType from ..common import NotifyType
from ..common import NotifyFormat from ..common import NotifyFormat
from ..utils import validate_regex
from ..AppriseLocale import gettext_lazy as _ 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 # Extend HTTP Error Messages
# Based on: https://developer.webex.com/docs/api/basics/rate-limiting # Based on: https://developer.webex.com/docs/api/basics/rate-limiting
WEBEX_HTTP_ERROR_MAP = { WEBEX_HTTP_ERROR_MAP = {
@ -119,7 +117,7 @@ class NotifyWebexTeams(NotifyBase):
'type': 'string', 'type': 'string',
'private': True, 'private': True,
'required': 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) super(NotifyWebexTeams, self).__init__(**kwargs)
if not token: # The token associated with the account
msg = 'The Webex Teams token is not specified.' self.token = validate_regex(
self.logger.warning(msg) token, *self.template_tokens['token']['regex'])
raise TypeError(msg) if not self.token:
if not VALIDATE_TOKEN.match(token.strip()):
msg = 'The Webex Teams token specified ({}) is invalid.'\ msg = 'The Webex Teams token specified ({}) is invalid.'\
.format(token) .format(token)
self.logger.warning(msg) self.logger.warning(msg)
raise TypeError(msg) raise TypeError(msg)
# The token associated with the account
self.token = token.strip()
def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs): def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
""" """
Perform Webex Teams Notification Perform Webex Teams Notification

View File

@ -157,7 +157,7 @@ class NotifyXMPP(NotifyBase):
'name': _('XEP'), 'name': _('XEP'),
'type': 'list:string', 'type': 'list:string',
'prefix': 'xep-', 'prefix': 'xep-',
'regex': (r'[1-9][0-9]{0,3}', 'i'), 'regex': (r'^[1-9][0-9]{0,3}$', 'i'),
}, },
'jid': { 'jid': {
'name': _('Source JID'), 'name': _('Source JID'),

View File

@ -61,15 +61,13 @@ import requests
from .NotifyBase import NotifyBase from .NotifyBase import NotifyBase
from ..common import NotifyType from ..common import NotifyType
from ..utils import parse_list from ..utils import parse_list
from ..utils import validate_regex
from ..utils import GET_EMAIL_RE from ..utils import GET_EMAIL_RE
from ..AppriseLocale import gettext_lazy as _ from ..AppriseLocale import gettext_lazy as _
# A Valid Bot Name # A Valid Bot Name
VALIDATE_BOTNAME = re.compile(r'(?P<name>[A-Z0-9_]{1,32})(-bot)?', re.I) VALIDATE_BOTNAME = re.compile(r'(?P<name>[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 # Organization required as part of the API request
VALIDATE_ORG = re.compile( VALIDATE_ORG = re.compile(
r'(?P<org>[A-Z0-9_-]{1,32})(\.(?P<hostname>[^\s]+))?', re.I) r'(?P<org>[A-Z0-9_-]{1,32})(\.(?P<hostname>[^\s]+))?', re.I)
@ -124,18 +122,20 @@ class NotifyZulip(NotifyBase):
'botname': { 'botname': {
'name': _('Bot Name'), 'name': _('Bot Name'),
'type': 'string', 'type': 'string',
'regex': (r'^[A-Z0-9_]{1,32}(-bot)?$', 'i'),
}, },
'organization': { 'organization': {
'name': _('Organization'), 'name': _('Organization'),
'type': 'string', 'type': 'string',
'required': True, 'required': True,
'regex': (r'^[A-Z0-9_-]{1,32})$', 'i')
}, },
'token': { 'token': {
'name': _('Token'), 'name': _('Token'),
'type': 'string', 'type': 'string',
'required': True, 'required': True,
'private': True, 'private': True,
'regex': (r'[A-Z0-9]{32}', 'i'), 'regex': (r'^[A-Z0-9]{32}$', 'i'),
}, },
'target_user': { 'target_user': {
'name': _('Target User'), 'name': _('Target User'),
@ -208,20 +208,14 @@ class NotifyZulip(NotifyBase):
self.logger.warning(msg) self.logger.warning(msg)
raise TypeError(msg) raise TypeError(msg)
try: self.token = validate_regex(
if not VALIDATE_TOKEN.match(token.strip()): token, *self.template_tokens['token']['regex'])
# let outer exception handle this if not self.token:
raise TypeError
except (TypeError, AttributeError):
msg = 'The Zulip token specified ({}) is invalid.'\ msg = 'The Zulip token specified ({}) is invalid.'\
.format(token) .format(token)
self.logger.warning(msg) self.logger.warning(msg)
raise TypeError(msg) raise TypeError(msg)
# The token associated with the account
self.token = token.strip()
self.targets = parse_list(targets) self.targets = parse_list(targets)
if len(self.targets) == 0: if len(self.targets) == 0:
# No channels identified, use default # No channels identified, use default

View File

@ -378,6 +378,16 @@ def details(plugin):
# Argument/Option Handling # Argument/Option Handling
for key in list(template_args.keys()): 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 # _lookup_default looks up what the default value
if '_lookup_default' in template_args[key]: if '_lookup_default' in template_args[key]:
template_args[key]['default'] = getattr( template_args[key]['default'] = getattr(

View File

@ -28,6 +28,7 @@ import six
import contextlib import contextlib
import os import os
from os.path import expanduser from os.path import expanduser
from functools import reduce
try: try:
# Python 2.7 # Python 2.7
@ -113,10 +114,17 @@ GET_EMAIL_RE = re.compile(
re.IGNORECASE, re.IGNORECASE,
) )
# Regular expression used to extract a phone number
GET_PHONE_NO_RE = re.compile(r'^\+?(?P<phone>[0-9\s)(+-]+)\s*$')
# Regular expression used to destinguish between multiple URLs # Regular expression used to destinguish between multiple URLs
URL_DETECTION_RE = re.compile( URL_DETECTION_RE = re.compile(
r'([a-z0-9]+?:\/\/.*?)[\s,]*(?=$|[a-z0-9]+?:\/\/)', re.I) 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): def is_hostname(hostname):
""" """
@ -512,14 +520,6 @@ def parse_list(*args):
elif isinstance(arg, (set, list, tuple)): elif isinstance(arg, (set, list, tuple)):
result += parse_list(*arg) 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 # 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 # treat these entries as though all elements found
# must exist in the notification service # must exist in the notification service
entries = set(parse_list(entry)) 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): if len(entries.intersection(data.union({match_all}))) == len(entries):
# our set contains all of the entries found # our set contains all of the entries found
@ -587,6 +592,82 @@ def is_exclusive_match(logic, data, match_all='all'):
return matched 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<year>[0-9]{2})[0-9]+(?P<value>[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 @contextlib.contextmanager
def environ(*remove, **update): def environ(*remove, **update):
""" """

View File

@ -68,11 +68,11 @@ def test_apprise():
a = Apprise() a = Apprise()
# no items # no items
assert(len(a) == 0) assert len(a) == 0
# Apprise object can also be directly tested with 'if' keyword # Apprise object can also be directly tested with 'if' keyword
# No entries results in a False response # No entries results in a False response
assert(not a) assert not a
# Create an Asset object # Create an Asset object
asset = AppriseAsset(theme='default') asset = AppriseAsset(theme='default')
@ -89,66 +89,62 @@ def test_apprise():
a = Apprise(servers=servers) a = Apprise(servers=servers)
# 2 servers loaded # 2 servers loaded
assert(len(a) == 2) assert len(a) == 2
# Apprise object can also be directly tested with 'if' keyword # Apprise object can also be directly tested with 'if' keyword
# At least one entry results in a True response # At least one entry results in a True response
assert(a) assert a
# We can retrieve our URLs this way: # We can retrieve our URLs this way:
assert(len(a.urls()) == 2) assert len(a.urls()) == 2
# We can add another server # We can add another server
assert( assert a.add('mmosts://mattermost.server.local/'
a.add('mmosts://mattermost.server.local/' '3ccdd113474722377935511fc85d3dd4') is True
'3ccdd113474722377935511fc85d3dd4') is True) assert len(a) == 3
assert(len(a) == 3)
# We can pop an object off of our stack by it's indexed value: # We can pop an object off of our stack by it's indexed value:
obj = a.pop(0) obj = a.pop(0)
assert(isinstance(obj, NotifyBase) is True) assert isinstance(obj, NotifyBase) is True
assert(len(a) == 2) assert len(a) == 2
# We can retrieve elements from our list too by reference: # 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: # We can iterate over our list too:
count = 0 count = 0
for o in a: for o in a:
assert(isinstance(o.url(), six.string_types) is True) assert isinstance(o.url(), six.string_types) is True
count += 1 count += 1
# verify that we did indeed iterate over each element # verify that we did indeed iterate over each element
assert(len(a) == count) assert len(a) == count
# We can empty our set # We can empty our set
a.clear() a.clear()
assert(len(a) == 0) assert len(a) == 0
# An invalid schema # An invalid schema
assert( assert a.add('this is not a parseable url at all') is False
a.add('this is not a parseable url at all') is False) assert len(a) == 0
assert(len(a) == 0)
# An unsupported schema # An unsupported schema
assert( assert a.add(
a.add('invalid://we.just.do.not.support.this.plugin.type') is False) 'invalid://we.just.do.not.support.this.plugin.type') is False
assert(len(a) == 0) assert len(a) == 0
# A poorly formatted URL # A poorly formatted URL
assert( assert a.add('json://user:@@@:bad?no.good') is False
a.add('json://user:@@@:bad?no.good') is False) assert len(a) == 0
assert(len(a) == 0)
# Add a server with our asset we created earlier # Add a server with our asset we created earlier
assert( assert a.add('mmosts://mattermost.server.local/'
a.add('mmosts://mattermost.server.local/' '3ccdd113474722377935511fc85d3dd4', asset=asset) is True
'3ccdd113474722377935511fc85d3dd4', asset=asset) is True)
# Clear our server listings again # Clear our server listings again
a.clear() a.clear()
# No servers to notify # 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): class BadNotification(NotifyBase):
def __init__(self, **kwargs): def __init__(self, **kwargs):
@ -183,26 +179,26 @@ def test_apprise():
# Just to explain what is happening here, we would have parsed the # 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 # url properly but failed when we went to go and create an instance
# of it. # of it.
assert(a.add('bad://localhost') is False) assert a.add('bad://localhost') is False
assert(len(a) == 0) assert len(a) == 0
assert(a.add('good://localhost') is True) assert a.add('good://localhost') is True
assert(len(a) == 1) assert len(a) == 1
# Bad Notification Type is still allowed as it is presumed the user # Bad Notification Type is still allowed as it is presumed the user
# know's what their doing # know's what their doing
assert(a.notify( assert a.notify(
title="my title", body="my body", notify_type='bad') is True) title="my title", body="my body", notify_type='bad') is True
# No Title/Body combo's # No Title/Body combo's
assert(a.notify(title=None, body=None) is False) assert a.notify(title=None, body=None) is False
assert(a.notify(title='', 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='') is False
# As long as one is present, we're good # As long as one is present, we're good
assert(a.notify(title=None, 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=None) is True
assert(a.notify(title="present", body="present") is True) assert a.notify(title="present", body="present") is True
# Clear our server listings again # Clear our server listings again
a.clear() a.clear()
@ -244,14 +240,14 @@ def test_apprise():
# Store our good notification in our schema map # Store our good notification in our schema map
SCHEMA_MAP['runtime'] = RuntimeNotification SCHEMA_MAP['runtime'] = RuntimeNotification
assert(a.add('runtime://localhost') is True) assert a.add('runtime://localhost') is True
assert(a.add('throw://localhost') is True) assert a.add('throw://localhost') is True
assert(a.add('fail://localhost') is True) assert a.add('fail://localhost') is True
assert(len(a) == 3) assert len(a) == 3
# Test when our notify both throws an exception and or just # Test when our notify both throws an exception and or just
# simply returns False # 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 # Create a Notification that throws an unexected exception
class ThrowInstantiateNotification(NotifyBase): class ThrowInstantiateNotification(NotifyBase):
@ -267,7 +263,7 @@ def test_apprise():
# Reset our object # Reset our object
a.clear() a.clear()
assert(len(a) == 0) assert len(a) == 0
# Instantiate a bad object # Instantiate a bad object
plugin = a.instantiate(object, tag="bad_object") plugin = a.instantiate(object, tag="bad_object")
@ -275,40 +271,37 @@ def test_apprise():
# Instantiate a good object # Instantiate a good object
plugin = a.instantiate('good://localhost', tag="good") plugin = a.instantiate('good://localhost', tag="good")
assert(isinstance(plugin, NotifyBase)) assert isinstance(plugin, NotifyBase)
# Test simple tagging inside of the object # Test simple tagging inside of the object
assert("good" in plugin) assert "good" in plugin
assert("bad" not in plugin) assert "bad" not in plugin
# the in (__contains__ override) is based on or'ed content; so although # 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 # 'bad' isn't tagged as being in the plugin, 'good' is, so the return
# value of this is True # value of this is True
assert(["bad", "good"] in plugin) assert ["bad", "good"] in plugin
assert(set(["bad", "good"]) in plugin) assert set(["bad", "good"]) in plugin
assert(("bad", "good") in plugin) assert ("bad", "good") in plugin
# We an add already substatiated instances into our Apprise object # We an add already substatiated instances into our Apprise object
a.add(plugin) a.add(plugin)
assert(len(a) == 1) assert len(a) == 1
# We can add entries as a list too (to add more then one) # We can add entries as a list too (to add more then one)
a.add([plugin, plugin, plugin]) a.add([plugin, plugin, plugin])
assert(len(a) == 4) assert len(a) == 4
# Reset our object again # Reset our object again
a.clear() a.clear()
try: with pytest.raises(TypeError):
a.instantiate('throw://localhost', suppress_exceptions=False) a.instantiate('throw://localhost', suppress_exceptions=False)
assert(False)
except TypeError: assert len(a) == 0
assert(True)
assert(len(a) == 0)
assert(a.instantiate( assert a.instantiate(
'throw://localhost', suppress_exceptions=True) is None) 'throw://localhost', suppress_exceptions=True) is None
assert(len(a) == 0) assert len(a) == 0
# #
# We rince and repeat the same tests as above, however we do them # We rince and repeat the same tests as above, however we do them
@ -317,50 +310,53 @@ def test_apprise():
# Reset our object # Reset our object
a.clear() a.clear()
assert(len(a) == 0) assert len(a) == 0
# Instantiate a good object # Instantiate a good object
plugin = a.instantiate({ plugin = a.instantiate({
'schema': 'good', 'schema': 'good',
'host': 'localhost'}, tag="good") 'host': 'localhost'}, tag="good")
assert(isinstance(plugin, NotifyBase)) assert isinstance(plugin, NotifyBase)
# Test simple tagging inside of the object # Test simple tagging inside of the object
assert("good" in plugin) assert "good" in plugin
assert("bad" not in plugin) assert "bad" not in plugin
# the in (__contains__ override) is based on or'ed content; so although # 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 # 'bad' isn't tagged as being in the plugin, 'good' is, so the return
# value of this is True # value of this is True
assert(["bad", "good"] in plugin) assert ["bad", "good"] in plugin
assert(set(["bad", "good"]) in plugin) assert set(["bad", "good"]) in plugin
assert(("bad", "good") in plugin) assert ("bad", "good") in plugin
# We an add already substatiated instances into our Apprise object # We an add already substatiated instances into our Apprise object
a.add(plugin) a.add(plugin)
assert(len(a) == 1) assert len(a) == 1
# We can add entries as a list too (to add more then one) # We can add entries as a list too (to add more then one)
a.add([plugin, plugin, plugin]) a.add([plugin, plugin, plugin])
assert(len(a) == 4) assert len(a) == 4
# Reset our object again # Reset our object again
a.clear() a.clear()
try: with pytest.raises(TypeError):
a.instantiate({ a.instantiate({
'schema': 'throw', 'schema': 'throw',
'host': 'localhost'}, suppress_exceptions=False) 'host': 'localhost'}, suppress_exceptions=False)
assert(False)
except TypeError: assert len(a) == 0
assert(True)
assert(len(a) == 0)
assert(a.instantiate({ assert a.instantiate({
'schema': 'throw', 'schema': 'throw',
'host': 'localhost'}, suppress_exceptions=True) is None) 'host': 'localhost'}, suppress_exceptions=True) is None
assert(len(a) == 0) assert len(a) == 0
def test_apprise_pretty_print(tmpdir):
"""
API: Apprise() Pretty Print tests
"""
# Privacy Print # Privacy Print
# PrivacyMode.Secret always returns the same thing to avoid guessing # PrivacyMode.Secret always returns the same thing to avoid guessing
assert URLBase.pprint( assert URLBase.pprint(
@ -410,6 +406,10 @@ def test_apprise():
assert URLBase.pprint( assert URLBase.pprint(
"abcdefghijk", privacy=True, mode=PrivacyMode.Tail) == '...hijk' "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.get')
@mock.patch('requests.post') @mock.patch('requests.post')
@ -437,90 +437,94 @@ def test_apprise_tagging(mock_post, mock_get):
a = Apprise() a = Apprise()
# An invalid addition can't add the tag # An invalid addition can't add the tag
assert(a.add('averyinvalidschema://localhost', tag='uhoh') is False) assert a.add('averyinvalidschema://localhost', tag='uhoh') is False
assert(a.add({ assert a.add({
'schema': 'averyinvalidschema', 'schema': 'averyinvalidschema',
'host': 'localhost'}, tag='uhoh') is False) 'host': 'localhost'}, tag='uhoh') is False
# Add entry and assign it to a tag called 'awesome' # Add entry and assign it to a tag called 'awesome'
assert(a.add('json://localhost/path1/', tag='awesome') is True) assert a.add('json://localhost/path1/', tag='awesome') is True
assert(a.add({ assert a.add({
'schema': 'json', 'schema': 'json',
'host': 'localhost', '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' # Add another notification and assign it to a tag called 'awesome'
# and another tag called 'local' # 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 # notify the awesome tag; this would notify both services behind the
# scenes # 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 # notify all of the tags
assert(a.notify( assert a.notify(
title="my title", body="my body", tag=['awesome', 'mmost']) is True) title="my title", body="my body", tag=['awesome', 'mmost']) is True
# When we query against our loaded notifications for a tag that simply # When we query against our loaded notifications for a tag that simply
# isn't assigned to anything, we return None. None (different then False) # 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... # tells us that we litterally had nothing to query. We didn't fail...
# but we also didn't do anything... # but we also didn't do anything...
assert(a.notify( assert a.notify(
title="my title", body="my body", tag='missing') is None) title="my title", body="my body", tag='missing') is None
# Now to test the ability to and and/or notifications # Now to test the ability to and and/or notifications
a = Apprise() a = Apprise()
# Add a tag by tuple # 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 # 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 # 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) # 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 # 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) # 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) # add a tag set by set (again)
assert(a.add('json://localhost/tagCDE/', assert a.add('json://localhost/tagCDE/',
tag=set(["TagC", "TagD", "TagE"])) is True) tag=set(["TagC", "TagD", "TagE"])) is True
# Expression: TagC and TagD # Expression: TagC and TagD
# Matches the following only: # Matches the following only:
# - json://localhost/tagCD/ # - json://localhost/tagCD/
# - json://localhost/tagCDE/ # - json://localhost/tagCDE/
assert(a.notify( assert a.notify(
title="my title", body="my body", tag=[('TagC', 'TagD')]) is True) title="my title", body="my body", tag=[('TagC', 'TagD')]) is True
# Expression: (TagY and TagZ) or TagX # Expression: (TagY and TagZ) or TagX
# Matches nothing, None is returned in this case # Matches nothing, None is returned in this case
assert(a.notify( assert a.notify(
title="my title", body="my body", title="my title", body="my body",
tag=[('TagY', 'TagZ'), 'TagX']) is None) tag=[('TagY', 'TagZ'), 'TagX']) is None
# Expression: (TagY and TagZ) or TagA # Expression: (TagY and TagZ) or TagA
# Matches the following only: # Matches the following only:
# - json://localhost/tagAB/ # - json://localhost/tagAB/
assert(a.notify( assert a.notify(
title="my title", body="my body", title="my title", body="my body",
tag=[('TagY', 'TagZ'), 'TagA']) is True) tag=[('TagY', 'TagZ'), 'TagA']) is True
# Expression: (TagE and TagD) or TagB # Expression: (TagE and TagD) or TagB
# Matches the following only: # Matches the following only:
# - json://localhost/tagCDE/ # - json://localhost/tagCDE/
# - json://localhost/tagAB/ # - json://localhost/tagAB/
# - json://localhost/tagB/ # - json://localhost/tagB/
assert(a.notify( assert a.notify(
title="my title", body="my body", 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 # Garbage Entries in tag field just get stripped out. the below
# notify as a result. So we simply return None # is the same as notifying no tags at all. Since we have not added
assert(a.notify( # 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", title="my title", body="my body",
tag=[(object, ), ]) is None) tag=[(object, ), ]) is None
def test_apprise_notify_formats(tmpdir): def test_apprise_notify_formats(tmpdir):
@ -536,7 +540,7 @@ def test_apprise_notify_formats(tmpdir):
a = Apprise() a = Apprise()
# no items # no items
assert(len(a) == 0) assert len(a) == 0
class TextNotification(NotifyBase): class TextNotification(NotifyBase):
# set our default notification format # 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 # defined plugin above was defined to default to HTML which triggers
# a markdown to take place if the body_format specified on the notify # a markdown to take place if the body_format specified on the notify
# call # call
assert(a.add('html://localhost') is True) assert a.add('html://localhost') is True
assert(a.add('html://another.server') is True) assert a.add('html://another.server') is True
assert(a.add('html://and.another') is True) assert a.add('html://and.another') is True
assert(a.add('text://localhost') is True) assert a.add('text://localhost') is True
assert(a.add('text://another.server') is True) assert a.add('text://another.server') is True
assert(a.add('text://and.another') is True) assert a.add('text://and.another') is True
assert(a.add('markdown://localhost') is True) assert a.add('markdown://localhost') is True
assert(a.add('markdown://another.server') is True) assert a.add('markdown://another.server') is True
assert(a.add('markdown://and.another') 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", assert a.notify(
body_format=NotifyFormat.MARKDOWN) is True) title="markdown", body="## Testing Markdown",
body_format=NotifyFormat.MARKDOWN) is True
assert(a.notify(title="text", body="Testing Text", assert a.notify(
body_format=NotifyFormat.TEXT) is True) title="text", body="Testing Text",
body_format=NotifyFormat.TEXT) is True
assert(a.notify(title="html", body="<b>HTML</b>", assert a.notify(
body_format=NotifyFormat.HTML) is True) title="html", body="<b>HTML</b>",
body_format=NotifyFormat.HTML) is True
def test_apprise_asset(tmpdir): def test_apprise_asset(tmpdir):
@ -623,7 +630,7 @@ def test_apprise_asset(tmpdir):
""" """
a = AppriseAsset(theme=None) a = AppriseAsset(theme=None)
# Default theme # Default theme
assert(a.theme == 'default') assert a.theme == 'default'
a = AppriseAsset( a = AppriseAsset(
theme='dark', theme='dark',
@ -634,58 +641,49 @@ def test_apprise_asset(tmpdir):
a.default_html_color = '#abcabc' a.default_html_color = '#abcabc'
a.html_notify_map[NotifyType.INFO] = '#aaaaaa' a.html_notify_map[NotifyType.INFO] = '#aaaaaa'
assert(a.color('invalid', tuple) == (171, 202, 188)) assert a.color('invalid', tuple) == (171, 202, 188)
assert(a.color(NotifyType.INFO, tuple) == (170, 170, 170)) assert a.color(NotifyType.INFO, tuple) == (170, 170, 170)
assert(a.color('invalid', int) == 11258556) assert a.color('invalid', int) == 11258556
assert(a.color(NotifyType.INFO, int) == 11184810) assert a.color(NotifyType.INFO, int) == 11184810
assert(a.color('invalid', None) == '#abcabc') assert a.color('invalid', None) == '#abcabc'
assert(a.color(NotifyType.INFO, None) == '#aaaaaa') assert a.color(NotifyType.INFO, None) == '#aaaaaa'
# None is the default # None is the default
assert(a.color(NotifyType.INFO) == '#aaaaaa') assert a.color(NotifyType.INFO) == '#aaaaaa'
# Invalid Type # Invalid Type
try: with pytest.raises(ValueError):
a.color(NotifyType.INFO, dict)
# We should not get here (exception should be thrown)
assert(False)
except ValueError:
# The exception we expect since dict is not supported # The exception we expect since dict is not supported
assert(True) a.color(NotifyType.INFO, dict)
except Exception: assert a.image_url(NotifyType.INFO, NotifyImageSize.XY_256) == \
# Any other exception is not good 'http://localhost/dark/info-256x256.png'
assert(False)
assert(a.image_url(NotifyType.INFO, NotifyImageSize.XY_256) == assert a.image_path(
'http://localhost/dark/info-256x256.png')
assert(a.image_path(
NotifyType.INFO, NotifyType.INFO,
NotifyImageSize.XY_256, 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 # This path doesn't exist so image_raw will fail (since we just
# randompyl picked it for testing) # 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, NotifyType.INFO,
NotifyImageSize.XY_256, NotifyImageSize.XY_256,
must_exist=True) is None) must_exist=True) is None
# Create a new object (with our default settings) # Create a new object (with our default settings)
a = AppriseAsset() a = AppriseAsset()
# Our default configuration can access our file # Our default configuration can access our file
assert(a.image_path( assert a.image_path(
NotifyType.INFO, NotifyType.INFO,
NotifyImageSize.XY_256, 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 # Create a temporary directory
sub = tmpdir.mkdir("great.theme") sub = tmpdir.mkdir("great.theme")
@ -703,14 +701,14 @@ def test_apprise_asset(tmpdir):
) )
# We'll be able to read file we just created # 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 # We can retrieve the filename at this point even with must_exist set
# to True # to True
assert(a.image_path( assert a.image_path(
NotifyType.INFO, NotifyType.INFO,
NotifyImageSize.XY_256, 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 # 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 # 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.') pytest.skip('The Root user can not run file permission tests.')
chmod(dirname(sub.strpath), 0o000) 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 # Our path doesn't exist anymore using this logic
assert(a.image_path( assert a.image_path(
NotifyType.INFO, NotifyType.INFO,
NotifyImageSize.XY_256, 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 # Return our permission so we don't have any problems with our cleanup
chmod(dirname(sub.strpath), 0o700) chmod(dirname(sub.strpath), 0o700)
# Our content is retrivable again # 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 # our file path is accessible again too
assert(a.image_path( assert a.image_path(
NotifyType.INFO, NotifyType.INFO,
NotifyImageSize.XY_256, 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 # We do the same test, but set the permission on the file
chmod(a.image_path(NotifyType.INFO, NotifyImageSize.XY_256), 0o000) chmod(a.image_path(NotifyType.INFO, NotifyImageSize.XY_256), 0o000)
# our path will still exist in this case # our path will still exist in this case
assert(a.image_path( assert a.image_path(
NotifyType.INFO, NotifyType.INFO,
NotifyImageSize.XY_256, NotifyImageSize.XY_256,
must_exist=True) is not None) must_exist=True) is not None
# but we will not be able to open it # 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 # Restore our permissions
chmod(a.image_path(NotifyType.INFO, NotifyImageSize.XY_256), 0o640) 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) a = AppriseAsset(image_path_mask=False, image_url_mask=False)
# We always return none in these calls now # We always return none in these calls now
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_url(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, assert a.image_path(NotifyType.INFO, NotifyImageSize.XY_256,
must_exist=False) is None) must_exist=False) is None
assert(a.image_path(NotifyType.INFO, NotifyImageSize.XY_256, assert a.image_path(NotifyType.INFO, NotifyImageSize.XY_256,
must_exist=True) is None) must_exist=True) is None
# Test our default extension out # Test our default extension out
a = AppriseAsset( a = AppriseAsset(
@ -772,28 +770,28 @@ def test_apprise_asset(tmpdir):
image_url_mask='http://localhost/{THEME}/{TYPE}-{XY}{EXTENSION}', image_url_mask='http://localhost/{THEME}/{TYPE}-{XY}{EXTENSION}',
default_extension='.jpeg', default_extension='.jpeg',
) )
assert(a.image_path( assert a.image_path(
NotifyType.INFO, NotifyType.INFO,
NotifyImageSize.XY_256, 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, NotifyType.INFO,
NotifyImageSize.XY_256) == 'http://localhost/' NotifyImageSize.XY_256) == \
'default/info-256x256.jpeg') 'http://localhost/default/info-256x256.jpeg'
# extension support # extension support
assert(a.image_path( assert a.image_path(
NotifyType.INFO, NotifyType.INFO,
NotifyImageSize.XY_128, NotifyImageSize.XY_128,
must_exist=False, 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, NotifyType.INFO,
NotifyImageSize.XY_256, NotifyImageSize.XY_256,
extension='.test') == 'http://localhost/' extension='.test') == \
'default/info-256x256.test') 'http://localhost/default/info-256x256.test'
def test_apprise_details(): def test_apprise_details():
@ -894,6 +892,11 @@ def test_apprise_details():
# #
'_exists_if': 'always_true', '_exists_if': 'always_true',
}, },
# alias_of testing
'test_alias_of': {
'alias_of': 'mylistB',
'delim': ('-', ' ')
}
}) })
def url(self): def url(self):
@ -1116,10 +1119,11 @@ def test_apprise_details_plugin_verification():
assert '{} is an invalid regex'\ assert '{} is an invalid regex'\
.format(arg['regex'][0]) .format(arg['regex'][0])
# Regex should never start and/or end with ^/$; leave # Regex should always start and/or end with ^/$
# that up to the user making use of the regex instead assert re.match(
assert re.match(r'^[()\s]*\^', arg['regex'][0]) is None r'^\^.+?$', arg['regex'][0]) is not None
assert re.match(r'[()\s$]*\$', arg['regex'][0]) is None assert re.match(
r'^.+?\$$', arg['regex'][0]) is not None
if arg['type'].startswith('list'): if arg['type'].startswith('list'):
# Delimiters MUST be defined # Delimiters MUST be defined
@ -1132,8 +1136,55 @@ def test_apprise_details_plugin_verification():
assert isinstance(arg['alias_of'], six.string_types) assert isinstance(arg['alias_of'], six.string_types)
# Track our alias_of object # Track our alias_of object
map_to_aliases.add(arg['alias_of']) 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: if six.PY2:
# inspect our object # inspect our object

View File

@ -25,6 +25,7 @@
import sys import sys
import six import six
import pytest
from apprise.AppriseAsset import AppriseAsset from apprise.AppriseAsset import AppriseAsset
from apprise.config.ConfigBase import ConfigBase from apprise.config.ConfigBase import ConfigBase
from apprise.config import __load_matrix from apprise.config import __load_matrix
@ -41,22 +42,12 @@ def test_config_base():
""" """
# invalid types throw exceptions # invalid types throw exceptions
try: with pytest.raises(TypeError):
ConfigBase(**{'format': 'invalid'}) 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 # Config format types are not the same as ConfigBase ones
try: with pytest.raises(TypeError):
ConfigBase(**{'format': 'markdown'}) ConfigBase(**{'format': 'markdown'})
# We should never reach here as an exception should be thrown
assert(False)
except TypeError:
assert(True)
cb = ConfigBase(**{'format': 'yaml'}) cb = ConfigBase(**{'format': 'yaml'})
assert isinstance(cb, ConfigBase) assert isinstance(cb, ConfigBase)

View File

@ -274,17 +274,17 @@ def test_email_plugin(mock_smtp, mock_smtpssl):
# Expected None but didn't get it # Expected None but didn't get it
print('%s instantiated %s (but expected None)' % ( print('%s instantiated %s (but expected None)' % (
url, str(obj))) url, str(obj)))
assert(False) assert False
assert(isinstance(obj, instance)) assert isinstance(obj, instance)
if isinstance(obj, plugins.NotifyBase): if isinstance(obj, plugins.NotifyBase):
# We loaded okay; now lets make sure we can reverse this url # 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 # Test url() with privacy=True
assert(isinstance( assert isinstance(
obj.url(privacy=True), six.string_types) is True) obj.url(privacy=True), six.string_types) is True
# Some Simple Invalid Instance Testing # Some Simple Invalid Instance Testing
assert instance.parse_url(None) is None 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 # assertion failure makes things easier to debug later on
print('TEST FAIL: {} regenerated as {}'.format( print('TEST FAIL: {} regenerated as {}'.format(
url, obj.url())) url, obj.url()))
assert(False) assert False
if self: if self:
# Iterate over our expected entries inside of our object # Iterate over our expected entries inside of our object
for key, val in self.items(): for key, val in self.items():
# Test that our object has the desired key # Test that our object has the desired key
assert(hasattr(key, obj)) assert hasattr(key, obj)
assert(getattr(key, obj) == val) assert getattr(key, obj) == val
try: try:
if test_smtplib_exceptions is False: if test_smtplib_exceptions is False:
@ -389,7 +389,7 @@ def test_webbase_lookup(mock_smtp, mock_smtpssl):
obj = Apprise.instantiate( obj = Apprise.instantiate(
'mailto://user:pass@l2g.com', suppress_exceptions=True) 'mailto://user:pass@l2g.com', suppress_exceptions=True)
assert(isinstance(obj, plugins.NotifyEmail)) assert isinstance(obj, plugins.NotifyEmail)
assert len(obj.targets) == 1 assert len(obj.targets) == 1
assert 'user@l2g.com' in obj.targets assert 'user@l2g.com' in obj.targets
assert obj.from_addr == 'user@l2g.com' assert obj.from_addr == 'user@l2g.com'
@ -417,7 +417,7 @@ def test_smtplib_init_fail(mock_smtplib):
obj = Apprise.instantiate( obj = Apprise.instantiate(
'mailto://user:pass@gmail.com', suppress_exceptions=False) 'mailto://user:pass@gmail.com', suppress_exceptions=False)
assert(isinstance(obj, plugins.NotifyEmail)) assert isinstance(obj, plugins.NotifyEmail)
# Support Exception handling of smtplib.SMTP # Support Exception handling of smtplib.SMTP
mock_smtplib.side_effect = RuntimeError('Test') mock_smtplib.side_effect = RuntimeError('Test')
@ -443,7 +443,7 @@ def test_smtplib_send_okay(mock_smtplib):
# Defaults to HTML # Defaults to HTML
obj = Apprise.instantiate( obj = Apprise.instantiate(
'mailto://user:pass@gmail.com', suppress_exceptions=False) '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 # Support an email simulation where we can correctly quit
mock_smtplib.starttls.return_value = True 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.sendmail.return_value = True
mock_smtplib.quit.return_value = True mock_smtplib.quit.return_value = True
assert(obj.notify( assert obj.notify(
body='body', title='test', notify_type=NotifyType.INFO) is True) body='body', title='test', notify_type=NotifyType.INFO) is True
# Set Text # Set Text
obj = Apprise.instantiate( obj = Apprise.instantiate(
'mailto://user:pass@gmail.com?format=text', suppress_exceptions=False) 'mailto://user:pass@gmail.com?format=text', suppress_exceptions=False)
assert(isinstance(obj, plugins.NotifyEmail)) assert isinstance(obj, plugins.NotifyEmail)
assert(obj.notify( assert obj.notify(
body='body', title='test', notify_type=NotifyType.INFO) is True) body='body', title='test', notify_type=NotifyType.INFO) is True
def test_email_url_escaping(): def test_email_url_escaping():

View File

@ -26,7 +26,7 @@ import six
import mock import mock
import requests import requests
from apprise import plugins from apprise import plugins
# from apprise import AppriseAsset
from json import dumps from json import dumps
from datetime import datetime from datetime import datetime
@ -87,16 +87,6 @@ def test_notify_gitter_plugin_general(mock_post, mock_get):
mock_get.return_value = request mock_get.return_value = request
mock_post.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 # Variation Initializations
obj = plugins.NotifyGitter(token=token, targets='apprise') obj = plugins.NotifyGitter(token=token, targets='apprise')
assert isinstance(obj, plugins.NotifyGitter) is True 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 assert obj.send(body="test") is True
# Variation Initializations # 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, plugins.NotifyGitter) is True
assert isinstance(obj.url(), six.string_types) 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 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

View File

@ -136,141 +136,149 @@ def test_dbus_plugin(mock_mainloop, mock_byte, mock_bytearray,
# Create our instance (identify all supported types) # Create our instance (identify all supported types)
obj = apprise.Apprise.instantiate('dbus://', suppress_exceptions=False) 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) 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) 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) 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 obj.duration = 0
# Check that it found our mocked environments # 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 # Test our class loading using a series of arguments
try: with pytest.raises(TypeError):
apprise.plugins.NotifyDBus(**{'schema': 'invalid'}) 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 # Invalid URLs
assert apprise.plugins.NotifyDBus.parse_url('') is None assert apprise.plugins.NotifyDBus.parse_url('') is None
# Set our X and Y coordinate and try the notification # Set our X and Y coordinate and try the notification
assert( assert apprise.plugins.NotifyDBus(
apprise.plugins.NotifyDBus( x_axis=0, y_axis=0, **{'schema': 'dbus'})\
x_axis=0, y_axis=0, **{'schema': 'dbus'})
.notify(title='', body='body', .notify(title='', body='body',
notify_type=apprise.NotifyType.INFO) is True) notify_type=apprise.NotifyType.INFO) is True
# test notifications # test notifications
assert(obj.notify(title='title', body='body', assert obj.notify(
notify_type=apprise.NotifyType.INFO) is True) title='title', body='body',
notify_type=apprise.NotifyType.INFO) is True
# test notification without a title # test notification without a title
assert(obj.notify(title='', body='body', assert obj.notify(
notify_type=apprise.NotifyType.INFO) is True) title='', body='body',
notify_type=apprise.NotifyType.INFO) is True
# Test our arguments through the instantiate call # Test our arguments through the instantiate call
obj = apprise.Apprise.instantiate( obj = apprise.Apprise.instantiate(
'dbus://_/?image=True', suppress_exceptions=False) 'dbus://_/?image=True', suppress_exceptions=False)
assert(isinstance(obj, apprise.plugins.NotifyDBus) is True) assert isinstance(obj, apprise.plugins.NotifyDBus) is True
assert(isinstance(obj.url(), six.string_types) is True) assert isinstance(obj.url(), six.string_types) is True
assert(obj.notify(title='title', body='body', assert obj.notify(
notify_type=apprise.NotifyType.INFO) is True) title='title', body='body',
notify_type=apprise.NotifyType.INFO) is True
obj = apprise.Apprise.instantiate( obj = apprise.Apprise.instantiate(
'dbus://_/?image=False', suppress_exceptions=False) 'dbus://_/?image=False', suppress_exceptions=False)
assert(isinstance(obj, apprise.plugins.NotifyDBus) is True) assert isinstance(obj, apprise.plugins.NotifyDBus) is True
assert(isinstance(obj.url(), six.string_types) is True) assert isinstance(obj.url(), six.string_types) is True
assert(obj.notify(title='title', body='body', assert obj.notify(
notify_type=apprise.NotifyType.INFO) is True) title='title', body='body',
notify_type=apprise.NotifyType.INFO) is True
# Test priority (alias to urgency) handling # Test priority (alias to urgency) handling
obj = apprise.Apprise.instantiate( obj = apprise.Apprise.instantiate(
'dbus://_/?priority=invalid', suppress_exceptions=False) 'dbus://_/?priority=invalid', suppress_exceptions=False)
assert(isinstance(obj, apprise.plugins.NotifyDBus) is True) assert isinstance(obj, apprise.plugins.NotifyDBus) is True
assert(isinstance(obj.url(), six.string_types) is True) assert isinstance(obj.url(), six.string_types) is True
assert(obj.notify(title='title', body='body', assert obj.notify(
notify_type=apprise.NotifyType.INFO) is True) title='title', body='body',
notify_type=apprise.NotifyType.INFO) is True
obj = apprise.Apprise.instantiate( obj = apprise.Apprise.instantiate(
'dbus://_/?priority=high', suppress_exceptions=False) 'dbus://_/?priority=high', suppress_exceptions=False)
assert(isinstance(obj, apprise.plugins.NotifyDBus) is True) assert isinstance(obj, apprise.plugins.NotifyDBus) is True
assert(isinstance(obj.url(), six.string_types) is True) assert isinstance(obj.url(), six.string_types) is True
assert(obj.notify(title='title', body='body', assert obj.notify(
notify_type=apprise.NotifyType.INFO) is True) title='title', body='body',
notify_type=apprise.NotifyType.INFO) is True
obj = apprise.Apprise.instantiate( obj = apprise.Apprise.instantiate(
'dbus://_/?priority=2', suppress_exceptions=False) 'dbus://_/?priority=2', suppress_exceptions=False)
assert(isinstance(obj, apprise.plugins.NotifyDBus) is True) assert isinstance(obj, apprise.plugins.NotifyDBus) is True
assert(isinstance(obj.url(), six.string_types) is True) assert isinstance(obj.url(), six.string_types) is True
assert(obj.notify(title='title', body='body', assert obj.notify(
notify_type=apprise.NotifyType.INFO) is True) title='title', body='body',
notify_type=apprise.NotifyType.INFO) is True
# Test urgency handling # Test urgency handling
obj = apprise.Apprise.instantiate( obj = apprise.Apprise.instantiate(
'dbus://_/?urgency=invalid', suppress_exceptions=False) 'dbus://_/?urgency=invalid', suppress_exceptions=False)
assert(isinstance(obj, apprise.plugins.NotifyDBus) is True) assert isinstance(obj, apprise.plugins.NotifyDBus) is True
assert(isinstance(obj.url(), six.string_types) is True) assert isinstance(obj.url(), six.string_types) is True
assert(obj.notify(title='title', body='body', assert obj.notify(
notify_type=apprise.NotifyType.INFO) is True) title='title', body='body',
notify_type=apprise.NotifyType.INFO) is True
obj = apprise.Apprise.instantiate( obj = apprise.Apprise.instantiate(
'dbus://_/?urgency=high', suppress_exceptions=False) 'dbus://_/?urgency=high', suppress_exceptions=False)
assert(isinstance(obj, apprise.plugins.NotifyDBus) is True) assert isinstance(obj, apprise.plugins.NotifyDBus) is True
assert(isinstance(obj.url(), six.string_types) is True) assert isinstance(obj.url(), six.string_types) is True
assert(obj.notify(title='title', body='body', assert obj.notify(
notify_type=apprise.NotifyType.INFO) is True) title='title', body='body',
notify_type=apprise.NotifyType.INFO) is True
obj = apprise.Apprise.instantiate( obj = apprise.Apprise.instantiate(
'dbus://_/?urgency=2', suppress_exceptions=False) 'dbus://_/?urgency=2', suppress_exceptions=False)
assert(isinstance(obj, apprise.plugins.NotifyDBus) is True) assert isinstance(obj, apprise.plugins.NotifyDBus) is True
assert(isinstance(obj.url(), six.string_types) is True) assert isinstance(obj.url(), six.string_types) is True
assert(obj.notify(title='title', body='body', assert obj.notify(
notify_type=apprise.NotifyType.INFO) is True) title='title', body='body',
notify_type=apprise.NotifyType.INFO) is True
obj = apprise.Apprise.instantiate( obj = apprise.Apprise.instantiate(
'dbus://_/?urgency=', suppress_exceptions=False) 'dbus://_/?urgency=', suppress_exceptions=False)
assert(isinstance(obj, apprise.plugins.NotifyDBus) is True) assert isinstance(obj, apprise.plugins.NotifyDBus) is True
assert(isinstance(obj.url(), six.string_types) is True) assert isinstance(obj.url(), six.string_types) is True
assert(obj.notify(title='title', body='body', assert obj.notify(
notify_type=apprise.NotifyType.INFO) is True) title='title', body='body',
notify_type=apprise.NotifyType.INFO) is True
# Test x/y # Test x/y
obj = apprise.Apprise.instantiate( obj = apprise.Apprise.instantiate(
'dbus://_/?x=5&y=5', suppress_exceptions=False) 'dbus://_/?x=5&y=5', suppress_exceptions=False)
assert(isinstance(obj, apprise.plugins.NotifyDBus) is True) assert isinstance(obj, apprise.plugins.NotifyDBus) is True
assert(isinstance(obj.url(), six.string_types) is True) assert isinstance(obj.url(), six.string_types) is True
assert(obj.notify(title='title', body='body', assert obj.notify(
notify_type=apprise.NotifyType.INFO) is True) title='title', body='body',
notify_type=apprise.NotifyType.INFO) is True
obj = apprise.Apprise.instantiate( obj = apprise.Apprise.instantiate(
'dbus://_/?x=invalid&y=invalid', suppress_exceptions=False) 'dbus://_/?x=invalid&y=invalid', suppress_exceptions=False)
assert(isinstance(obj, apprise.plugins.NotifyDBus) is True) assert isinstance(obj, apprise.plugins.NotifyDBus) is True
assert(isinstance(obj.url(), six.string_types) is True) assert isinstance(obj.url(), six.string_types) is True
assert(obj.notify(title='title', body='body', assert obj.notify(
notify_type=apprise.NotifyType.INFO) is True) 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 # gracefully fail
mock_notify = mock.Mock() mock_notify = mock.Mock()
mock_interface.return_value = mock_notify mock_interface.return_value = mock_notify
mock_notify.Notify.side_effect = AttributeError() mock_notify.Notify.side_effect = AttributeError()
assert(obj.notify(title='', body='body', assert obj.notify(
notify_type=apprise.NotifyType.INFO) is False) title='', body='body',
notify_type=apprise.NotifyType.INFO) is False
mock_notify.Notify.side_effect = None mock_notify.Notify.side_effect = None
# Test our loading of our icon exception; it will still allow the # Test our loading of our icon exception; it will still allow the
# notification to be sent # notification to be sent
mock_pixbuf.new_from_file.side_effect = AttributeError() mock_pixbuf.new_from_file.side_effect = AttributeError()
assert(obj.notify(title='title', body='body', assert obj.notify(
notify_type=apprise.NotifyType.INFO) is True) title='title', body='body',
notify_type=apprise.NotifyType.INFO) is True
# Undo our change # Undo our change
mock_pixbuf.new_from_file.side_effect = None 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 # Toggle our testing for when we can't send notifications because the
# package has been made unavailable to us # package has been made unavailable to us
obj._enabled = False obj._enabled = False
assert(obj.notify(title='title', body='body', assert obj.notify(
notify_type=apprise.NotifyType.INFO) is False) title='title', body='body',
notify_type=apprise.NotifyType.INFO) is False
# Test the setting of a the urgency # Test the setting of a the urgency
apprise.plugins.NotifyDBus(urgency=0) apprise.plugins.NotifyDBus(urgency=0)
@ -306,15 +315,16 @@ def test_dbus_plugin(mock_mainloop, mock_byte, mock_bytearray,
# Create our instance # Create our instance
obj = apprise.Apprise.instantiate('glib://', suppress_exceptions=False) 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 obj.duration = 0
# Test url() call # 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 # Our notification succeeds even though the gi library was not loaded
assert(obj.notify(title='title', body='body', assert obj.notify(
notify_type=apprise.NotifyType.INFO) is True) title='title', body='body',
notify_type=apprise.NotifyType.INFO) is True
# Verify this all works in the event a ValueError is also thronw # Verify this all works in the event a ValueError is also thronw
# out of the call to gi.require_version() # 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 # Create our instance
obj = apprise.Apprise.instantiate('glib://', suppress_exceptions=False) 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 obj.duration = 0
# Test url() call # 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 # Our notification succeeds even though the gi library was not loaded
assert(obj.notify(title='title', body='body', assert obj.notify(
notify_type=apprise.NotifyType.INFO) is True) title='title', body='body',
notify_type=apprise.NotifyType.INFO) is True
# Force a global import error # Force a global import error
_session_bus = sys.modules['dbus'] _session_bus = sys.modules['dbus']
@ -358,15 +369,16 @@ def test_dbus_plugin(mock_mainloop, mock_byte, mock_bytearray,
# Create our instance # Create our instance
obj = apprise.Apprise.instantiate('glib://', suppress_exceptions=False) 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 obj.duration = 0
# Test url() call # 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 # Our notification fail because the dbus library wasn't present
assert(obj.notify(title='title', body='body', assert obj.notify(
notify_type=apprise.NotifyType.INFO) is False) title='title', body='body',
notify_type=apprise.NotifyType.INFO) is False
# Since playing with the sys.modules is not such a good idea, # Since playing with the sys.modules is not such a good idea,
# let's just put it back now :) # let's just put it back now :)

View File

@ -211,7 +211,7 @@ def test_growl_plugin(mock_gntp):
try: try:
obj = Apprise.instantiate(url, suppress_exceptions=False) obj = Apprise.instantiate(url, suppress_exceptions=False)
assert(exception is None) assert exception is None
if obj is None: if obj is None:
# We're done # We're done
@ -219,17 +219,17 @@ def test_growl_plugin(mock_gntp):
if instance is None: if instance is None:
# Expected None but didn't get it # 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): if isinstance(obj, plugins.NotifyBase):
# We loaded okay; now lets make sure we can reverse this url # 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 # Test our privacy=True flag
assert(isinstance( assert isinstance(
obj.url(privacy=True), six.string_types) is True) obj.url(privacy=True), six.string_types) is True
# Instantiate the exact same object again using the URL from # Instantiate the exact same object again using the URL from
# the one that was already created properly # 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 # assertion failure makes things easier to debug later on
print('TEST FAIL: {} regenerated as {}'.format( print('TEST FAIL: {} regenerated as {}'.format(
url, obj.url())) url, obj.url()))
assert(False) assert False
if self: if self:
# Iterate over our expected entries inside of our object # Iterate over our expected entries inside of our object
for key, val in self.items(): for key, val in self.items():
# Test that our object has the desired key # Test that our object has the desired key
assert(hasattr(key, obj)) assert hasattr(key, obj)
assert(getattr(key, obj) == val) assert getattr(key, obj) == val
try: try:
if test_growl_notify_exceptions is False: if test_growl_notify_exceptions is False:
@ -292,5 +292,5 @@ def test_growl_plugin(mock_gntp):
except Exception as e: except Exception as e:
# Handle our exception # Handle our exception
print('%s / %s' % (url, str(e))) print('%s / %s' % (url, str(e)))
assert(exception is not None) assert exception is not None
assert(isinstance(e, exception)) assert isinstance(e, exception)

View File

@ -23,6 +23,7 @@
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE. # THE SOFTWARE.
import six import six
import pytest
from datetime import datetime from datetime import datetime
from datetime import timedelta from datetime import timedelta
@ -43,22 +44,12 @@ def test_notify_base():
""" """
# invalid types throw exceptions # invalid types throw exceptions
try: with pytest.raises(TypeError):
NotifyBase(**{'format': 'invalid'}) NotifyBase(**{'format': 'invalid'})
# We should never reach here as an exception should be thrown
assert(False)
except TypeError:
assert(True)
# invalid types throw exceptions # invalid types throw exceptions
try: with pytest.raises(TypeError):
NotifyBase(**{'overflow': 'invalid'}) NotifyBase(**{'overflow': 'invalid'})
# We should never reach here as an exception should be thrown
assert(False)
except TypeError:
assert(True)
# Bad port information # Bad port information
nb = NotifyBase(port='invalid') nb = NotifyBase(port='invalid')
@ -216,7 +207,7 @@ def test_notify_base():
# Test invalid data # Test invalid data
assert NotifyBase.parse_list(None) == [] assert NotifyBase.parse_list(None) == []
assert NotifyBase.parse_list(42) == ['42', ] assert NotifyBase.parse_list(42) == []
result = NotifyBase.parse_list( result = NotifyBase.parse_list(
',path,?name=Dr%20Disrespect', unquote=False) ',path,?name=Dr%20Disrespect', unquote=False)

File diff suppressed because it is too large Load Diff

View File

@ -24,6 +24,7 @@
# THE SOFTWARE. # THE SOFTWARE.
import mock import mock
import pytest
import requests import requests
from apprise import plugins from apprise import plugins
from apprise import Apprise from apprise import Apprise
@ -45,7 +46,7 @@ def test_object_initialization():
""" """
# Initializes the plugin with a valid access, but invalid access key # Initializes the plugin with a valid access, but invalid access key
try: with pytest.raises(TypeError):
# No access_key_id specified # No access_key_id specified
plugins.NotifySNS( plugins.NotifySNS(
access_key_id=None, access_key_id=None,
@ -53,14 +54,8 @@ def test_object_initialization():
region_name=TEST_REGION, region_name=TEST_REGION,
targets='+1800555999', targets='+1800555999',
) )
# The entries above are invalid, our code should never reach here
assert(False)
except TypeError: with pytest.raises(TypeError):
# Exception correctly caught
assert(True)
try:
# No secret_access_key specified # No secret_access_key specified
plugins.NotifySNS( plugins.NotifySNS(
access_key_id=TEST_ACCESS_KEY_ID, access_key_id=TEST_ACCESS_KEY_ID,
@ -68,14 +63,8 @@ def test_object_initialization():
region_name=TEST_REGION, region_name=TEST_REGION,
targets='+1800555999', targets='+1800555999',
) )
# The entries above are invalid, our code should never reach here
assert(False)
except TypeError: with pytest.raises(TypeError):
# Exception correctly caught
assert(True)
try:
# No region_name specified # No region_name specified
plugins.NotifySNS( plugins.NotifySNS(
access_key_id=TEST_ACCESS_KEY_ID, access_key_id=TEST_ACCESS_KEY_ID,
@ -83,14 +72,8 @@ def test_object_initialization():
region_name=None, region_name=None,
targets='+1800555999', targets='+1800555999',
) )
# The entries above are invalid, our code should never reach here
assert(False)
except TypeError: with pytest.raises(TypeError):
# Exception correctly caught
assert(True)
try:
# No recipients # No recipients
plugins.NotifySNS( plugins.NotifySNS(
access_key_id=TEST_ACCESS_KEY_ID, access_key_id=TEST_ACCESS_KEY_ID,
@ -98,14 +81,8 @@ def test_object_initialization():
region_name=TEST_REGION, region_name=TEST_REGION,
targets=None, targets=None,
) )
# Still valid even without recipients
assert(True)
except TypeError: with pytest.raises(TypeError):
# Exception correctly caught
assert(False)
try:
# No recipients - garbage recipients object # No recipients - garbage recipients object
plugins.NotifySNS( plugins.NotifySNS(
access_key_id=TEST_ACCESS_KEY_ID, access_key_id=TEST_ACCESS_KEY_ID,
@ -113,14 +90,8 @@ def test_object_initialization():
region_name=TEST_REGION, region_name=TEST_REGION,
targets=object(), targets=object(),
) )
# Still valid even without recipients
assert(True)
except TypeError: with pytest.raises(TypeError):
# Exception correctly caught
assert(False)
try:
# The phone number is invalid, and without it, there is nothing # The phone number is invalid, and without it, there is nothing
# to notify # to notify
plugins.NotifySNS( plugins.NotifySNS(
@ -129,15 +100,8 @@ def test_object_initialization():
region_name=TEST_REGION, region_name=TEST_REGION,
targets='+1809', 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: with pytest.raises(TypeError):
# Exception correctly caught
assert(False)
try:
# The phone number is invalid, and without it, there is nothing # The phone number is invalid, and without it, there is nothing
# to notify; we # to notify; we
plugins.NotifySNS( plugins.NotifySNS(
@ -146,13 +110,6 @@ def test_object_initialization():
region_name=TEST_REGION, region_name=TEST_REGION,
targets='#(invalid-topic-because-of-the-brackets)', 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(): def test_url_parsing():
@ -169,13 +126,13 @@ def test_url_parsing():
) )
# Confirm that there were no recipients found # Confirm that there were no recipients found
assert(len(results['targets']) == 0) assert len(results['targets']) == 0
assert('region_name' in results) assert 'region_name' in results
assert(TEST_REGION == results['region_name']) assert TEST_REGION == results['region_name']
assert('access_key_id' in results) assert 'access_key_id' in results
assert(TEST_ACCESS_KEY_ID == results['access_key_id']) assert TEST_ACCESS_KEY_ID == results['access_key_id']
assert('secret_access_key' in results) assert 'secret_access_key' in results
assert(TEST_ACCESS_KEY_SECRET == results['secret_access_key']) assert TEST_ACCESS_KEY_SECRET == results['secret_access_key']
# Detect recipients # Detect recipients
results = plugins.NotifySNS.parse_url('sns://%s/%s/%s/%s/%s/' % ( 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 # Confirm that our recipients were found
assert(len(results['targets']) == 2) assert len(results['targets']) == 2
assert('+18001234567' in results['targets']) assert '+18001234567' in results['targets']
assert('MyTopic' in results['targets']) assert 'MyTopic' in results['targets']
assert('region_name' in results) assert 'region_name' in results
assert(TEST_REGION == results['region_name']) assert TEST_REGION == results['region_name']
assert('access_key_id' in results) assert 'access_key_id' in results
assert(TEST_ACCESS_KEY_ID == results['access_key_id']) assert TEST_ACCESS_KEY_ID == results['access_key_id']
assert('secret_access_key' in results) assert 'secret_access_key' in results
assert(TEST_ACCESS_KEY_SECRET == results['secret_access_key']) assert TEST_ACCESS_KEY_SECRET == results['secret_access_key']
def test_object_parsing(): def test_object_parsing():
@ -209,20 +166,20 @@ def test_object_parsing():
a = Apprise() a = Apprise()
# Now test failing variations of our URL # Now test failing variations of our URL
assert(a.add('sns://') is False) assert a.add('sns://') is False
assert(a.add('sns://nosecret') is False) assert a.add('sns://nosecret') is False
assert(a.add('sns://nosecret/noregion/') is False) assert a.add('sns://nosecret/noregion/') is False
# This is valid, but a rather useless URL; there is nothing to notify # This is valid but without valid recipients, the URL is actually useless
assert(a.add('sns://norecipient/norecipient/us-west-2') is True) assert a.add('sns://norecipient/norecipient/us-west-2') is False
assert(len(a) == 1) assert len(a) == 0
# Parse a good one # Parse a good one
assert(a.add('sns://oh/yeah/us-west-2/abcdtopic/+12223334444') is True) assert a.add('sns://oh/yeah/us-west-2/abcdtopic/+12223334444') is True
assert(len(a) == 2) assert len(a) == 1
assert(a.add('sns://oh/yeah/us-west-2/12223334444') is True) assert a.add('sns://oh/yeah/us-west-2/12223334444') is True
assert(len(a) == 3) assert len(a) == 2
def test_aws_response_handling(): def test_aws_response_handling():
@ -232,25 +189,25 @@ def test_aws_response_handling():
""" """
# Not a string # Not a string
response = plugins.NotifySNS.aws_response_to_dict(None) response = plugins.NotifySNS.aws_response_to_dict(None)
assert(response['type'] is None) assert response['type'] is None
assert(response['request_id'] is None) assert response['request_id'] is None
# Invalid XML # Invalid XML
response = plugins.NotifySNS.aws_response_to_dict( response = plugins.NotifySNS.aws_response_to_dict(
'<Bad Response xmlns="http://sns.amazonaws.com/doc/2010-03-31/">') '<Bad Response xmlns="http://sns.amazonaws.com/doc/2010-03-31/">')
assert(response['type'] is None) assert response['type'] is None
assert(response['request_id'] is None) assert response['request_id'] is None
# Single Element in XML # Single Element in XML
response = plugins.NotifySNS.aws_response_to_dict( response = plugins.NotifySNS.aws_response_to_dict(
'<SingleElement></SingleElement>') '<SingleElement></SingleElement>')
assert(response['type'] == 'SingleElement') assert response['type'] == 'SingleElement'
assert(response['request_id'] is None) assert response['request_id'] is None
# Empty String # Empty String
response = plugins.NotifySNS.aws_response_to_dict('') response = plugins.NotifySNS.aws_response_to_dict('')
assert(response['type'] is None) assert response['type'] is None
assert(response['request_id'] is None) assert response['request_id'] is None
response = plugins.NotifySNS.aws_response_to_dict( response = plugins.NotifySNS.aws_response_to_dict(
""" """
@ -263,9 +220,9 @@ def test_aws_response_handling():
</ResponseMetadata> </ResponseMetadata>
</PublishResponse> </PublishResponse>
""") """)
assert(response['type'] == 'PublishResponse') assert response['type'] == 'PublishResponse'
assert(response['request_id'] == 'dc258024-d0e6-56bb-af1b-d4fe5f4181a4') assert response['request_id'] == 'dc258024-d0e6-56bb-af1b-d4fe5f4181a4'
assert(response['message_id'] == '5e16935a-d1fb-5a31-a716-c7805e5c1d2e') assert response['message_id'] == '5e16935a-d1fb-5a31-a716-c7805e5c1d2e'
response = plugins.NotifySNS.aws_response_to_dict( response = plugins.NotifySNS.aws_response_to_dict(
""" """
@ -278,9 +235,9 @@ def test_aws_response_handling():
</ResponseMetadata> </ResponseMetadata>
</CreateTopicResponse> </CreateTopicResponse>
""") """)
assert(response['type'] == 'CreateTopicResponse') assert response['type'] == 'CreateTopicResponse'
assert(response['request_id'] == '604bef0f-369c-50c5-a7a4-bbd474c83d6a') assert response['request_id'] == '604bef0f-369c-50c5-a7a4-bbd474c83d6a'
assert(response['topic_arn'] == 'arn:aws:sns:us-east-1:000000000000:abcd') assert response['topic_arn'] == 'arn:aws:sns:us-east-1:000000000000:abcd'
response = plugins.NotifySNS.aws_response_to_dict( response = plugins.NotifySNS.aws_response_to_dict(
""" """
@ -294,12 +251,12 @@ def test_aws_response_handling():
<RequestId>b5614883-babe-56ca-93b2-1c592ba6191e</RequestId> <RequestId>b5614883-babe-56ca-93b2-1c592ba6191e</RequestId>
</ErrorResponse> </ErrorResponse>
""") """)
assert(response['type'] == 'ErrorResponse') assert response['type'] == 'ErrorResponse'
assert(response['request_id'] == 'b5614883-babe-56ca-93b2-1c592ba6191e') assert response['request_id'] == 'b5614883-babe-56ca-93b2-1c592ba6191e'
assert(response['error_type'] == 'Sender') assert response['error_type'] == 'Sender'
assert(response['error_code'] == 'InvalidParameter') assert response['error_code'] == 'InvalidParameter'
assert(response['error_message'].startswith('Invalid parameter:')) assert response['error_message'].startswith('Invalid parameter:')
assert(response['error_message'].endswith('required parameter')) assert response['error_message'].endswith('required parameter')
@mock.patch('requests.post') @mock.patch('requests.post')
@ -356,7 +313,7 @@ def test_aws_topic_handling(mock_post):
'12223334444/TopicA']) '12223334444/TopicA'])
# CreateTopic fails # CreateTopic fails
assert(a.notify(title='', body='test') is False) assert a.notify(title='', body='test') is False
def post(url, data, **kwargs): def post(url, data, **kwargs):
""" """
@ -383,7 +340,7 @@ def test_aws_topic_handling(mock_post):
mock_post.side_effect = post mock_post.side_effect = post
# Publish fails # Publish fails
assert(a.notify(title='', body='test') is False) assert a.notify(title='', body='test') is False
# Disable our side effect # Disable our side effect
mock_post.side_effect = None mock_post.side_effect = None
@ -395,14 +352,14 @@ def test_aws_topic_handling(mock_post):
# Assign ourselves a new function # Assign ourselves a new function
mock_post.return_value = robj 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 # Handle case where we fails get a bad response
robj = mock.Mock() robj = mock.Mock()
robj.content = '' robj.content = ''
robj.status_code = requests.codes.bad_request robj.status_code = requests.codes.bad_request
mock_post.return_value = robj 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 # Handle case where we get a valid response and TopicARN
robj = mock.Mock() robj = mock.Mock()
@ -410,4 +367,4 @@ def test_aws_topic_handling(mock_post):
robj.status_code = requests.codes.ok robj.status_code = requests.codes.ok
mock_post.return_value = robj mock_post.return_value = robj
# We would have failed to make Post # We would have failed to make Post
assert(a.notify(title='', body='test') is True) assert a.notify(title='', body='test') is True

View File

@ -25,6 +25,7 @@
import six import six
import mock import mock
import pytest
import requests import requests
from json import dumps from json import dumps
from datetime import datetime from datetime import datetime
@ -41,57 +42,40 @@ def test_twitter_plugin_init():
""" """
try: with pytest.raises(TypeError):
plugins.NotifyTwitter( plugins.NotifyTwitter(
ckey=None, csecret=None, akey=None, asecret=None) 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( plugins.NotifyTwitter(
ckey='value', csecret=None, akey=None, asecret=None) ckey='value', csecret=None, akey=None, asecret=None)
assert False
except TypeError:
# csecret not set
assert True
try: with pytest.raises(TypeError):
plugins.NotifyTwitter( plugins.NotifyTwitter(
ckey='value', csecret='value', akey=None, asecret=None) ckey='value', csecret='value', akey=None, asecret=None)
assert False
except TypeError:
# akey not set
assert True
try: with pytest.raises(TypeError):
plugins.NotifyTwitter( plugins.NotifyTwitter(
ckey='value', csecret='value', akey='value', asecret=None) ckey='value', csecret='value', akey='value', asecret=None)
assert False
except TypeError:
# asecret not set
assert True
try: assert isinstance(
plugins.NotifyTwitter( plugins.NotifyTwitter(
ckey='value', csecret='value', akey='value', asecret='value') ckey='value', csecret='value', akey='value', asecret='value'),
assert True plugins.NotifyTwitter,
except TypeError: )
# user not set; but this is okay
# We should not reach here
assert False
try: assert isinstance(
plugins.NotifyTwitter( plugins.NotifyTwitter(
ckey='value', csecret='value', akey='value', asecret='value', ckey='value', csecret='value', akey='value', asecret='value',
user='l2gnux') user='l2gnux'),
# We should initialize properly plugins.NotifyTwitter,
assert True )
except TypeError: # Invalid Target User
# We should not reach here with pytest.raises(TypeError):
assert False plugins.NotifyTwitter(
ckey='value', csecret='value', akey='value', asecret='value',
targets='%G@rB@g3')
@mock.patch('requests.get') @mock.patch('requests.get')

View File

@ -45,293 +45,290 @@ def test_parse_qsd():
"utils: parse_qsd() testing """ "utils: parse_qsd() testing """
result = utils.parse_qsd('a=1&b=&c&d=abcd') result = utils.parse_qsd('a=1&b=&c&d=abcd')
assert(isinstance(result, dict) is True) assert isinstance(result, dict) is True
assert(len(result) == 3) assert len(result) == 3
assert 'qsd' in result assert 'qsd' in result
assert 'qsd+' in result 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 'a' in result['qsd']
assert 'b' in result['qsd'] assert 'b' in result['qsd']
assert 'c' in result['qsd'] assert 'c' in result['qsd']
assert 'd' 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(): def test_parse_url():
"utils: parse_url() testing """ "utils: parse_url() testing """
result = utils.parse_url('http://hostname') result = utils.parse_url('http://hostname')
assert(result['schema'] == 'http') assert result['schema'] == 'http'
assert(result['host'] == 'hostname') assert result['host'] == 'hostname'
assert(result['port'] is None) assert result['port'] is None
assert(result['user'] is None) assert result['user'] is None
assert(result['password'] is None) assert result['password'] is None
assert(result['fullpath'] is None) assert result['fullpath'] is None
assert(result['path'] is None) assert result['path'] is None
assert(result['query'] is None) assert result['query'] is None
assert(result['url'] == 'http://hostname') assert result['url'] == 'http://hostname'
assert(result['qsd'] == {}) assert result['qsd'] == {}
assert(result['qsd-'] == {}) assert result['qsd-'] == {}
assert(result['qsd+'] == {}) assert result['qsd+'] == {}
result = utils.parse_url('http://hostname/') result = utils.parse_url('http://hostname/')
assert(result['schema'] == 'http') assert result['schema'] == 'http'
assert(result['host'] == 'hostname') assert result['host'] == 'hostname'
assert(result['port'] is None) assert result['port'] is None
assert(result['user'] is None) assert result['user'] is None
assert(result['password'] is None) assert result['password'] is None
assert(result['fullpath'] == '/') assert result['fullpath'] == '/'
assert(result['path'] == '/') assert result['path'] == '/'
assert(result['query'] is None) assert result['query'] is None
assert(result['url'] == 'http://hostname/') assert result['url'] == 'http://hostname/'
assert(result['qsd'] == {}) assert result['qsd'] == {}
assert(result['qsd-'] == {}) assert result['qsd-'] == {}
assert(result['qsd+'] == {}) assert result['qsd+'] == {}
result = utils.parse_url('hostname') result = utils.parse_url('hostname')
assert(result['schema'] == 'http') assert result['schema'] == 'http'
assert(result['host'] == 'hostname') assert result['host'] == 'hostname'
assert(result['port'] is None) assert result['port'] is None
assert(result['user'] is None) assert result['user'] is None
assert(result['password'] is None) assert result['password'] is None
assert(result['fullpath'] is None) assert result['fullpath'] is None
assert(result['path'] is None) assert result['path'] is None
assert(result['query'] is None) assert result['query'] is None
assert(result['url'] == 'http://hostname') assert result['url'] == 'http://hostname'
assert(result['qsd'] == {}) assert result['qsd'] == {}
assert(result['qsd-'] == {}) assert result['qsd-'] == {}
assert(result['qsd+'] == {}) assert result['qsd+'] == {}
result = utils.parse_url('http://hostname/?-KeY=Value') result = utils.parse_url('http://hostname/?-KeY=Value')
assert(result['schema'] == 'http') assert result['schema'] == 'http'
assert(result['host'] == 'hostname') assert result['host'] == 'hostname'
assert(result['port'] is None) assert result['port'] is None
assert(result['user'] is None) assert result['user'] is None
assert(result['password'] is None) assert result['password'] is None
assert(result['fullpath'] == '/') assert result['fullpath'] == '/'
assert(result['path'] == '/') assert result['path'] == '/'
assert(result['query'] is None) assert result['query'] is None
assert(result['url'] == 'http://hostname/') assert result['url'] == 'http://hostname/'
assert('-key' in result['qsd']) assert '-key' in result['qsd']
assert(unquote(result['qsd']['-key']) == 'Value') assert unquote(result['qsd']['-key']) == 'Value'
assert('KeY' in result['qsd-']) assert 'KeY' in result['qsd-']
assert(unquote(result['qsd-']['KeY']) == 'Value') assert unquote(result['qsd-']['KeY']) == 'Value'
assert(result['qsd+'] == {}) assert result['qsd+'] == {}
result = utils.parse_url('http://hostname/?+KeY=Value') result = utils.parse_url('http://hostname/?+KeY=Value')
assert(result['schema'] == 'http') assert result['schema'] == 'http'
assert(result['host'] == 'hostname') assert result['host'] == 'hostname'
assert(result['port'] is None) assert result['port'] is None
assert(result['user'] is None) assert result['user'] is None
assert(result['password'] is None) assert result['password'] is None
assert(result['fullpath'] == '/') assert result['fullpath'] == '/'
assert(result['path'] == '/') assert result['path'] == '/'
assert(result['query'] is None) assert result['query'] is None
assert(result['url'] == 'http://hostname/') 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'] == 'Value') assert result['qsd+']['KeY'] == 'Value'
assert(result['qsd-'] == {}) assert result['qsd-'] == {}
result = utils.parse_url( result = utils.parse_url(
'http://hostname/?+KeY=ValueA&-kEy=ValueB&KEY=Value%20+C') 'http://hostname/?+KeY=ValueA&-kEy=ValueB&KEY=Value%20+C')
assert(result['schema'] == 'http') assert result['schema'] == 'http'
assert(result['host'] == 'hostname') assert result['host'] == 'hostname'
assert(result['port'] is None) assert result['port'] is None
assert(result['user'] is None) assert result['user'] is None
assert(result['password'] is None) assert result['password'] is None
assert(result['fullpath'] == '/') assert result['fullpath'] == '/'
assert(result['path'] == '/') assert result['path'] == '/'
assert(result['query'] is None) assert result['query'] is None
assert(result['url'] == 'http://hostname/') 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('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 result['qsd+']['KeY'] == 'ValueA'
assert('kEy' in result['qsd-']) assert 'kEy' in result['qsd-']
assert(result['qsd-']['kEy'] == 'ValueB') assert result['qsd-']['kEy'] == 'ValueB'
assert(result['qsd']['key'] == 'Value C') assert result['qsd']['key'] == 'Value C'
assert(result['qsd']['+key'] == result['qsd+']['KeY']) assert result['qsd']['+key'] == result['qsd+']['KeY']
assert(result['qsd']['-key'] == result['qsd-']['kEy']) assert result['qsd']['-key'] == result['qsd-']['kEy']
result = utils.parse_url('http://hostname////') result = utils.parse_url('http://hostname////')
assert(result['schema'] == 'http') assert result['schema'] == 'http'
assert(result['host'] == 'hostname') assert result['host'] == 'hostname'
assert(result['port'] is None) assert result['port'] is None
assert(result['user'] is None) assert result['user'] is None
assert(result['password'] is None) assert result['password'] is None
assert(result['fullpath'] == '/') assert result['fullpath'] == '/'
assert(result['path'] == '/') assert result['path'] == '/'
assert(result['query'] is None) assert result['query'] is None
assert(result['url'] == 'http://hostname/') assert result['url'] == 'http://hostname/'
assert(result['qsd'] == {}) assert result['qsd'] == {}
assert(result['qsd-'] == {}) assert result['qsd-'] == {}
assert(result['qsd+'] == {}) assert result['qsd+'] == {}
result = utils.parse_url('http://hostname:40////') result = utils.parse_url('http://hostname:40////')
assert(result['schema'] == 'http') assert result['schema'] == 'http'
assert(result['host'] == 'hostname') assert result['host'] == 'hostname'
assert(result['port'] == 40) assert result['port'] == 40
assert(result['user'] is None) assert result['user'] is None
assert(result['password'] is None) assert result['password'] is None
assert(result['fullpath'] == '/') assert result['fullpath'] == '/'
assert(result['path'] == '/') assert result['path'] == '/'
assert(result['query'] is None) assert result['query'] is None
assert(result['url'] == 'http://hostname:40/') assert result['url'] == 'http://hostname:40/'
assert(result['qsd'] == {}) assert result['qsd'] == {}
assert(result['qsd-'] == {}) assert result['qsd-'] == {}
assert(result['qsd+'] == {}) assert result['qsd+'] == {}
result = utils.parse_url('HTTP://HoStNaMe:40/test.php') result = utils.parse_url('HTTP://HoStNaMe:40/test.php')
assert(result['schema'] == 'http') assert result['schema'] == 'http'
assert(result['host'] == 'HoStNaMe') assert result['host'] == 'HoStNaMe'
assert(result['port'] == 40) assert result['port'] == 40
assert(result['user'] is None) assert result['user'] is None
assert(result['password'] is None) assert result['password'] is None
assert(result['fullpath'] == '/test.php') assert result['fullpath'] == '/test.php'
assert(result['path'] == '/') assert result['path'] == '/'
assert(result['query'] == 'test.php') assert result['query'] == 'test.php'
assert(result['url'] == 'http://HoStNaMe:40/test.php') assert result['url'] == 'http://HoStNaMe:40/test.php'
assert(result['qsd'] == {}) assert result['qsd'] == {}
assert(result['qsd-'] == {}) assert result['qsd-'] == {}
assert(result['qsd+'] == {}) assert result['qsd+'] == {}
result = utils.parse_url('HTTPS://user@hostname/test.py') result = utils.parse_url('HTTPS://user@hostname/test.py')
assert(result['schema'] == 'https') assert result['schema'] == 'https'
assert(result['host'] == 'hostname') assert result['host'] == 'hostname'
assert(result['port'] is None) assert result['port'] is None
assert(result['user'] == 'user') assert result['user'] == 'user'
assert(result['password'] is None) assert result['password'] is None
assert(result['fullpath'] == '/test.py') assert result['fullpath'] == '/test.py'
assert(result['path'] == '/') assert result['path'] == '/'
assert(result['query'] == 'test.py') assert result['query'] == 'test.py'
assert(result['url'] == 'https://user@hostname/test.py') assert result['url'] == 'https://user@hostname/test.py'
assert(result['qsd'] == {}) assert result['qsd'] == {}
assert(result['qsd-'] == {}) assert result['qsd-'] == {}
assert(result['qsd+'] == {}) assert result['qsd+'] == {}
result = utils.parse_url(' HTTPS://///user@@@hostname///test.py ') result = utils.parse_url(' HTTPS://///user@@@hostname///test.py ')
assert(result['schema'] == 'https') assert result['schema'] == 'https'
assert(result['host'] == 'hostname') assert result['host'] == 'hostname'
assert(result['port'] is None) assert result['port'] is None
assert(result['user'] == 'user') assert result['user'] == 'user'
assert(result['password'] is None) assert result['password'] is None
assert(result['fullpath'] == '/test.py') assert result['fullpath'] == '/test.py'
assert(result['path'] == '/') assert result['path'] == '/'
assert(result['query'] == 'test.py') assert result['query'] == 'test.py'
assert(result['url'] == 'https://user@hostname/test.py') assert result['url'] == 'https://user@hostname/test.py'
assert(result['qsd'] == {}) assert result['qsd'] == {}
assert(result['qsd-'] == {}) assert result['qsd-'] == {}
assert(result['qsd+'] == {}) assert result['qsd+'] == {}
result = utils.parse_url( result = utils.parse_url(
'HTTPS://user:password@otherHost/full///path/name/', 'HTTPS://user:password@otherHost/full///path/name/',
) )
assert(result['schema'] == 'https') assert result['schema'] == 'https'
assert(result['host'] == 'otherHost') assert result['host'] == 'otherHost'
assert(result['port'] is None) assert result['port'] is None
assert(result['user'] == 'user') assert result['user'] == 'user'
assert(result['password'] == 'password') assert result['password'] == 'password'
assert(result['fullpath'] == '/full/path/name/') assert result['fullpath'] == '/full/path/name/'
assert(result['path'] == '/full/path/name/') assert result['path'] == '/full/path/name/'
assert(result['query'] is None) assert result['query'] is None
assert(result['url'] == 'https://user:password@otherHost/full/path/name/') assert result['url'] == 'https://user:password@otherHost/full/path/name/'
assert(result['qsd'] == {}) assert result['qsd'] == {}
assert(result['qsd-'] == {}) assert result['qsd-'] == {}
assert(result['qsd+'] == {}) assert result['qsd+'] == {}
# Handle garbage # Handle garbage
assert(utils.parse_url(None) is None) assert utils.parse_url(None) is None
result = utils.parse_url( result = utils.parse_url(
'mailto://user:password@otherHost/lead2gold@gmail.com' + 'mailto://user:password@otherHost/lead2gold@gmail.com' +
'?from=test@test.com&name=Chris%20Caron&format=text' '?from=test@test.com&name=Chris%20Caron&format=text'
) )
assert(result['schema'] == 'mailto') assert result['schema'] == 'mailto'
assert(result['host'] == 'otherHost') assert result['host'] == 'otherHost'
assert(result['port'] is None) assert result['port'] is None
assert(result['user'] == 'user') assert result['user'] == 'user'
assert(result['password'] == 'password') assert result['password'] == 'password'
assert(unquote(result['fullpath']) == '/lead2gold@gmail.com') assert unquote(result['fullpath']) == '/lead2gold@gmail.com'
assert(result['path'] == '/') assert result['path'] == '/'
assert(unquote(result['query']) == 'lead2gold@gmail.com') assert unquote(result['query']) == 'lead2gold@gmail.com'
assert(unquote( assert unquote(result['url']) == \
result['url']) == 'mailto://user:password@otherHost/lead2gold@gmail.com'
'mailto://user:password@otherHost/lead2gold@gmail.com') assert len(result['qsd']) == 3
assert(len(result['qsd']) == 3) assert 'name' in result['qsd']
assert('name' in result['qsd']) assert unquote(result['qsd']['name']) == 'Chris Caron'
assert(unquote(result['qsd']['name']) == 'Chris Caron') assert 'from' in result['qsd']
assert('from' in result['qsd']) assert unquote(result['qsd']['from']) == 'test@test.com'
assert(unquote(result['qsd']['from']) == 'test@test.com') assert 'format' in result['qsd']
assert('format' in result['qsd']) assert unquote(result['qsd']['format']) == 'text'
assert(unquote(result['qsd']['format']) == 'text') assert result['qsd-'] == {}
assert(result['qsd-'] == {}) assert result['qsd+'] == {}
assert(result['qsd+'] == {})
# Test Passwords with question marks ?; not supported # Test Passwords with question marks ?; not supported
result = utils.parse_url( result = utils.parse_url(
'http://user:pass.with.?question@host' 'http://user:pass.with.?question@host'
) )
assert(result is None) assert result is None
# just hostnames # just hostnames
result = utils.parse_url( result = utils.parse_url(
'nuxref.com' 'nuxref.com'
) )
assert(result['schema'] == 'http') assert result['schema'] == 'http'
assert(result['host'] == 'nuxref.com') assert result['host'] == 'nuxref.com'
assert(result['port'] is None) assert result['port'] is None
assert(result['user'] is None) assert result['user'] is None
assert(result['password'] is None) assert result['password'] is None
assert(result['fullpath'] is None) assert result['fullpath'] is None
assert(result['path'] is None) assert result['path'] is None
assert(result['query'] is None) assert result['query'] is None
assert(result['url'] == 'http://nuxref.com') assert result['url'] == 'http://nuxref.com'
assert(result['qsd'] == {}) assert result['qsd'] == {}
assert(result['qsd-'] == {}) assert result['qsd-'] == {}
assert(result['qsd+'] == {}) assert result['qsd+'] == {}
# just host and path # just host and path
result = utils.parse_url( result = utils.parse_url('invalid/host')
'invalid/host' assert result['schema'] == 'http'
) assert result['host'] == 'invalid'
assert(result['schema'] == 'http') assert result['port'] is None
assert(result['host'] == 'invalid') assert result['user'] is None
assert(result['port'] is None) assert result['password'] is None
assert(result['user'] is None) assert result['fullpath'] == '/host'
assert(result['password'] is None) assert result['path'] == '/'
assert(result['fullpath'] == '/host') assert result['query'] == 'host'
assert(result['path'] == '/') assert result['url'] == 'http://invalid/host'
assert(result['query'] == 'host') assert result['qsd'] == {}
assert(result['url'] == 'http://invalid/host') assert result['qsd-'] == {}
assert(result['qsd'] == {}) assert result['qsd+'] == {}
assert(result['qsd-'] == {})
assert(result['qsd+'] == {})
# just all out invalid # 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 # A default port of zero is still considered valid, but
# is removed in the response. # is removed in the response.
result = utils.parse_url('http://nuxref.com:0') result = utils.parse_url('http://nuxref.com:0')
assert(result['schema'] == 'http') assert result['schema'] == 'http'
assert(result['host'] == 'nuxref.com') assert result['host'] == 'nuxref.com'
assert(result['port'] is None) assert result['port'] is None
assert(result['user'] is None) assert result['user'] is None
assert(result['password'] is None) assert result['password'] is None
assert(result['fullpath'] is None) assert result['fullpath'] is None
assert(result['path'] is None) assert result['path'] is None
assert(result['query'] is None) assert result['query'] is None
assert(result['url'] == 'http://nuxref.com') assert result['url'] == 'http://nuxref.com'
assert(result['qsd'] == {}) assert result['qsd'] == {}
assert(result['qsd-'] == {}) assert result['qsd-'] == {}
assert(result['qsd+'] == {}) assert result['qsd+'] == {}
# Test some illegal strings # Test some illegal strings
result = utils.parse_url(object, verify_host=False) result = utils.parse_url(object, verify_host=False)
@ -345,7 +342,7 @@ def test_parse_url():
# Do it again without host validation # Do it again without host validation
result = utils.parse_url('test://', verify_host=False) 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 # It's worth noting that the hostname is an empty string and is NEVER set
# to None if it wasn't specified. # to None if it wasn't specified.
assert result['host'] == '' assert result['host'] == ''
@ -423,10 +420,10 @@ def test_parse_url():
assert result['port'] is None assert result['port'] is None
assert result['user'] == '' assert result['user'] == ''
assert result['password'] == '' assert result['password'] == ''
assert(unquote(result['fullpath']) == '/_/@^&/jack.json') assert unquote(result['fullpath']) == '/_/@^&/jack.json'
assert(unquote(result['path']) == '/_/@^&/') assert unquote(result['path']) == '/_/@^&/'
assert result['query'] == 'jack.json' 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'] == {}
assert result['qsd-'] == {} assert result['qsd-'] == {}
@ -434,42 +431,42 @@ def test_parse_url():
def test_parse_bool(): def test_parse_bool():
"utils: parse_bool() testing """ "utils: parse_bool() testing """
assert(utils.parse_bool('Enabled', None) is True) assert utils.parse_bool('Enabled', None) is True
assert(utils.parse_bool('Disabled', None) is False) assert utils.parse_bool('Disabled', None) is False
assert(utils.parse_bool('Allow', None) is True) assert utils.parse_bool('Allow', None) is True
assert(utils.parse_bool('Deny', None) is False) 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('YES', None) is True) assert utils.parse_bool('YES', None) is True
assert(utils.parse_bool('Always', 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('NO', None) is False) assert utils.parse_bool('NO', None) is False
assert(utils.parse_bool('NEVER', 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('tRUe', None) is True) assert utils.parse_bool('tRUe', None) is True
assert(utils.parse_bool('FAlse', None) is False) assert utils.parse_bool('FAlse', None) is False
assert(utils.parse_bool('F', None) is False) assert utils.parse_bool('F', None) is False
assert(utils.parse_bool('T', None) is True) assert utils.parse_bool('T', None) is True
assert(utils.parse_bool('0', None) is False) assert utils.parse_bool('0', None) is False
assert(utils.parse_bool('1', None) is True) assert utils.parse_bool('1', None) is True
assert(utils.parse_bool('True', None) is True) assert utils.parse_bool('True', None) is True
assert(utils.parse_bool('Yes', None) is True) assert utils.parse_bool('Yes', None) is True
assert(utils.parse_bool(1, None) is True) assert utils.parse_bool(1, None) is True
assert(utils.parse_bool(0, None) is False) assert utils.parse_bool(0, 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(False, None) is False
# only the int of 0 will return False since the function # only the int of 0 will return False since the function
# casts this to a boolean # 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 # 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 # 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) # 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 # 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(): def test_is_hostname():
@ -608,14 +605,15 @@ def test_parse_list():
results = utils.parse_list( results = utils.parse_list(
'.mkv,.avi,.divx,.xvid,.mov,.wmv,.mp4,.mpg,.mpeg,.vob,.iso') '.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', '.divx', '.iso', '.mkv', '.mov', '.mpg', '.avi', '.mpeg', '.vob',
'.xvid', '.wmv', '.mp4', '.xvid', '.wmv', '.mp4',
])) ])
class StrangeObject(object): class StrangeObject(object):
def __str__(self): def __str__(self):
return '.avi' return '.avi'
# Now 2 lists with lots of duplicates and other delimiters # Now 2 lists with lots of duplicates and other delimiters
results = utils.parse_list( results = utils.parse_list(
'.mkv,.avi,.divx,.xvid,.mov,.wmv,.mp4,.mpg .mpeg,.vob,,; ;', '.mkv,.avi,.divx,.xvid,.mov,.wmv,.mp4,.mpg .mpeg,.vob,,; ;',
@ -623,10 +621,13 @@ def test_parse_list():
'.vob,.iso', ['.vob', ['.vob', '.mkv', StrangeObject(), ], ], '.vob,.iso', ['.vob', ['.vob', '.mkv', StrangeObject(), ], ],
StrangeObject()) StrangeObject())
assert(results == sorted([ assert results == sorted([
'.divx', '.iso', '.mkv', '.mov', '.mpg', '.avi', '.mpeg', '.vob', '.divx', '.iso', '.mkv', '.mov', '.mpg', '.avi', '.mpeg', '.vob',
'.xvid', '.wmv', '.mp4', '.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 # Now a list with extras we want to add as strings
# empty entries are removed # empty entries are removed
@ -634,10 +635,10 @@ def test_parse_list():
'.divx', '.iso', '.mkv', '.mov', '', ' ', '.avi', '.mpeg', '.vob', '.divx', '.iso', '.mkv', '.mov', '', ' ', '.avi', '.mpeg', '.vob',
'.xvid', '.mp4'], '.mov,.wmv,.mp4,.mpg') '.xvid', '.mp4'], '.mov,.wmv,.mp4,.mpg')
assert(results == sorted([ assert results == sorted([
'.divx', '.wmv', '.iso', '.mkv', '.mov', '.mpg', '.avi', '.vob', '.divx', '.wmv', '.iso', '.mkv', '.mov', '.mpg', '.avi', '.vob',
'.xvid', '.mpeg', '.mp4', '.xvid', '.mpeg', '.mp4',
])) ])
def test_exclusive_match(): def test_exclusive_match():
@ -735,6 +736,46 @@ def test_exclusive_match():
logic='match_me', data=data, match_all='match_me') is True 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<value>[^-]+)-', fmt="{value}") == "abcd"
assert utils.validate_regex(
"- abcd -", r'-(?P<value>[^-]+)-', strip=False,
fmt="{value}") == " abcd "
# String flags supported in addition to numeric
assert utils.validate_regex(
"- abcd -", r'-(?P<value>[^-]+)-', 'i', fmt="{value}") == "abcd"
assert utils.validate_regex(
"- abcd -", r'-(?P<value>[^-]+)-', re.I, fmt="{value}") == "abcd"
# Test multiple flag settings
assert utils.validate_regex(
"- abcd -", r'-(?P<value>[^-]+)-', '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<value>[ABCD]+)-', '-%2gb', fmt="{value}") is None
assert utils.validate_regex(
"- abcd -", r'-(?P<value>[ABCD]+)-', '', fmt="{value}") is None
assert utils.validate_regex(
"- abcd -", r'-(?P<value>[ABCD]+)-', None, fmt="{value}") is None
def test_environ_temporary_change(): def test_environ_temporary_change():
"""utils: environ() testing """utils: environ() testing
""" """

View File

@ -113,37 +113,41 @@ def test_windows_plugin():
obj.duration = 0 obj.duration = 0
# Test URL functionality # 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 # Check that it found our mocked environments
assert(obj._enabled is True) assert obj._enabled is True
# _on_destroy check # _on_destroy check
obj._on_destroy(0, '', 0, 0) obj._on_destroy(0, '', 0, 0)
# test notifications # test notifications
assert(obj.notify(title='title', body='body', assert obj.notify(
notify_type=apprise.NotifyType.INFO) is True) title='title', body='body',
notify_type=apprise.NotifyType.INFO) is True
obj = apprise.Apprise.instantiate( obj = apprise.Apprise.instantiate(
'windows://_/?image=True', suppress_exceptions=False) 'windows://_/?image=True', suppress_exceptions=False)
obj.duration = 0 obj.duration = 0
assert(isinstance(obj.url(), six.string_types) is True) assert isinstance(obj.url(), six.string_types) is True
assert(obj.notify(title='title', body='body', assert obj.notify(
notify_type=apprise.NotifyType.INFO) is True) title='title', body='body',
notify_type=apprise.NotifyType.INFO) is True
obj = apprise.Apprise.instantiate( obj = apprise.Apprise.instantiate(
'windows://_/?image=False', suppress_exceptions=False) 'windows://_/?image=False', suppress_exceptions=False)
obj.duration = 0 obj.duration = 0
assert(isinstance(obj.url(), six.string_types) is True) assert isinstance(obj.url(), six.string_types) is True
assert(obj.notify(title='title', body='body', assert obj.notify(
notify_type=apprise.NotifyType.INFO) is True) title='title', body='body',
notify_type=apprise.NotifyType.INFO) is True
obj = apprise.Apprise.instantiate( obj = apprise.Apprise.instantiate(
'windows://_/?duration=1', suppress_exceptions=False) 'windows://_/?duration=1', suppress_exceptions=False)
assert(isinstance(obj.url(), six.string_types) is True) assert isinstance(obj.url(), six.string_types) is True
assert(obj.notify(title='title', body='body', assert obj.notify(
notify_type=apprise.NotifyType.INFO) is True) title='title', body='body',
notify_type=apprise.NotifyType.INFO) is True
# loads okay # loads okay
assert obj.duration == 1 assert obj.duration == 1
@ -165,20 +169,23 @@ def test_windows_plugin():
# Test our loading of our icon exception; it will still allow the # Test our loading of our icon exception; it will still allow the
# notification to be sent # notification to be sent
win32gui.LoadImage.side_effect = AttributeError win32gui.LoadImage.side_effect = AttributeError
assert(obj.notify(title='title', body='body', assert obj.notify(
notify_type=apprise.NotifyType.INFO) is True) title='title', body='body',
notify_type=apprise.NotifyType.INFO) is True
# Undo our change # Undo our change
win32gui.LoadImage.side_effect = None win32gui.LoadImage.side_effect = None
# Test our global exception handling # Test our global exception handling
win32gui.UpdateWindow.side_effect = AttributeError win32gui.UpdateWindow.side_effect = AttributeError
assert(obj.notify(title='title', body='body', assert obj.notify(
notify_type=apprise.NotifyType.INFO) is False) title='title', body='body',
notify_type=apprise.NotifyType.INFO) is False
# Undo our change # Undo our change
win32gui.UpdateWindow.side_effect = None win32gui.UpdateWindow.side_effect = None
# Toggle our testing for when we can't send notifications because the # Toggle our testing for when we can't send notifications because the
# package has been made unavailable to us # package has been made unavailable to us
obj._enabled = False obj._enabled = False
assert(obj.notify(title='title', body='body', assert obj.notify(
notify_type=apprise.NotifyType.INFO) is False) title='title', body='body',
notify_type=apprise.NotifyType.INFO) is False