Refactored Unit Testing and Dependencies (#483)

This commit is contained in:
Chris Caron 2021-11-25 15:20:22 -05:00 committed by GitHub
parent 1dfde961e8
commit fe83c62669
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
109 changed files with 12130 additions and 9211 deletions

View File

@ -1,5 +1,4 @@
[run]
omit=*/gntp/*
disable_warnings = no-data-collected
branch = True
source =
@ -15,4 +14,4 @@ source =
show_missing = True
skip_covered = True
skip_empty = True
fail_under = 99.0
fail_under = 95.0

View File

@ -11,8 +11,6 @@ matrix:
include:
- python: "2.7"
env: TOXENV=py27
- python: "3.5"
env: TOXENV=py35
- python: "3.6"
env: TOXENV=py36
- python: "3.7"
@ -23,10 +21,15 @@ matrix:
env: TOXENV=py39
- python: "3.9-dev"
env: TOXENV=py39-dev
# PyPy Environments
- python: "pypy2.7-6.0"
env: TOXENV=pypy
- python: "pypy3.5-7.0"
env: TOXENV=pypy3
# An extra environment where additional packages are not installed
- python: "3.9"
env:
- TOXENV=bare
install:
- pip install babel
@ -36,8 +39,10 @@ install:
- pip install codecov
- pip install -r dev-requirements.txt
- pip install -r requirements.txt
- if [[ $TRAVIS_PYTHON_VERSION != 'pypy'* ]]; then travis_retry pip install dbus-python; fi
# bare installs do not include extra package dependencies
- if [[ $TOXENV != 'bare' ]]; then pip install -r all-plugin-requirements.txt; fi
# pypy and bare installs do not include dbus-python
- if [[ $TOXENV != 'bare' ]] && [[ $TRAVIS_PYTHON_VERSION != 'pypy'* ]]; then travis_retry pip install dbus-python; fi
# run tests
script:

View File

@ -0,0 +1,15 @@
#
# Plugin Dependencies
#
# Provides fcm:// and spush://
cryptography
# Provides xmpp:// support
slixmpp; python_version >= '3.7'
# Provides growl:// support
gntp
# Provides mqtt:// support
paho-mqtt

View File

@ -164,6 +164,16 @@ class Apprise(object):
type(url))
return None
if not plugins.SCHEMA_MAP[results['schema']].enabled:
#
# First Plugin Enable Check (Pre Initialization)
#
# Plugin has been disabled at a global level
logger.error(
'%s:// is disabled on this system.', results['schema'])
return None
# Build a list of tags to associate with the newly added notifications
results['tag'] = set(parse_list(tag))
@ -199,6 +209,24 @@ class Apprise(object):
# URL information but don't wrap it in a try catch
plugin = plugins.SCHEMA_MAP[results['schema']](**results)
if not plugin.enabled:
#
# Second Plugin Enable Check (Post Initialization)
#
# Service/Plugin is disabled (on a more local level). This is a
# case where the plugin was initially enabled but then after the
# __init__() was called under the hood something pre-determined
# that it could no longer be used.
# The only downside to doing it this way is services are
# initialized prior to returning the details() if 3rd party tools
# are polling what is available. These services that become
# disabled thereafter are shown initially that they can be used.
logger.error(
'%s:// has become disabled on this system.', results['schema'])
return None
return plugin
def add(self, servers, asset=None, tag=None):
@ -585,7 +613,7 @@ class Apprise(object):
attach=attach
)
def details(self, lang=None):
def details(self, lang=None, show_requirements=False, show_disabled=False):
"""
Returns the details associated with the Apprise object
@ -601,8 +629,27 @@ class Apprise(object):
'asset': self.asset.details(),
}
# to add it's mapping to our hash table
for plugin in set(plugins.SCHEMA_MAP.values()):
# Iterate over our hashed plugins and dynamically build details on
# their status:
content = {
'service_name': getattr(plugin, 'service_name', None),
'service_url': getattr(plugin, 'service_url', None),
'setup_url': getattr(plugin, 'setup_url', None),
# Placeholder - populated below
'details': None
}
# Standard protocol(s) should be None or a tuple
enabled = getattr(plugin, 'enabled', True)
if not show_disabled and not enabled:
# Do not show inactive plugins
continue
elif show_disabled:
# Add current state to response
content['enabled'] = enabled
# Standard protocol(s) should be None or a tuple
protocols = getattr(plugin, 'protocol', None)
@ -614,23 +661,27 @@ class Apprise(object):
if isinstance(secure_protocols, six.string_types):
secure_protocols = (secure_protocols, )
# Add our protocol details to our content
content.update({
'protocols': protocols,
'secure_protocols': secure_protocols,
})
if not lang:
# Simply return our results
details = plugins.details(plugin)
content['details'] = plugins.details(plugin)
if show_requirements:
content['requirements'] = plugins.requirements(plugin)
else:
# Emulate the specified language when returning our results
with self.locale.lang_at(lang):
details = plugins.details(plugin)
content['details'] = plugins.details(plugin)
if show_requirements:
content['requirements'] = plugins.requirements(plugin)
# Build our response object
response['schemas'].append({
'service_name': getattr(plugin, 'service_name', None),
'service_url': getattr(plugin, 'service_url', None),
'setup_url': getattr(plugin, 'setup_url', None),
'protocols': protocols,
'secure_protocols': secure_protocols,
'details': details,
})
response['schemas'].append(content)
return response

View File

@ -26,8 +26,10 @@
import click
import logging
import platform
import six
import sys
import os
import re
from os.path import isfile
from os.path import expanduser
@ -136,6 +138,9 @@ def print_version_msg():
help='Perform a trial run but only prints the notification '
'services to-be triggered to stdout. Notifications are never '
'sent using this mode.')
@click.option('--details', '-l', is_flag=True,
help='Prints details about the current services supported by '
'Apprise.')
@click.option('--recursion-depth', '-R', default=DEFAULT_RECURSION_DEPTH,
type=int,
help='The number of recursive import entries that can be '
@ -153,7 +158,7 @@ def print_version_msg():
metavar='SERVER_URL [SERVER_URL2 [SERVER_URL3]]',)
def main(body, title, config, attach, urls, notification_type, theme, tag,
input_format, dry_run, recursion_depth, verbose, disable_async,
interpret_escapes, debug, version):
details, interpret_escapes, debug, version):
"""
Send a notification to all of the specified servers identified by their
URLs the content provided within the title, body and notification-type.
@ -248,6 +253,78 @@ def main(body, title, config, attach, urls, notification_type, theme, tag,
# Create our Apprise object
a = Apprise(asset=asset, debug=debug, location=ContentLocation.LOCAL)
if details:
# Print details and exit
results = a.details(show_requirements=True, show_disabled=True)
# Sort our results:
plugins = sorted(
results['schemas'], key=lambda i: str(i['service_name']))
for entry in plugins:
protocols = [] if not entry['protocols'] else \
[p for p in entry['protocols']
if isinstance(p, six.string_types)]
protocols.extend(
[] if not entry['secure_protocols'] else
[p for p in entry['secure_protocols']
if isinstance(p, six.string_types)])
if len(protocols) == 1:
# Simplify view by swapping {schema} with the single
# protocol value
# Convert tuple to list
entry['details']['templates'] = \
list(entry['details']['templates'])
for x in range(len(entry['details']['templates'])):
entry['details']['templates'][x] = \
re.sub(
r'^[^}]+}://',
'{}://'.format(protocols[0]),
entry['details']['templates'][x])
click.echo(click.style(
'{} {:<30} '.format(
'+' if entry['enabled'] else '-',
str(entry['service_name'])),
fg="green" if entry['enabled'] else "red", bold=True),
nl=(not entry['enabled'] or len(protocols) == 1))
if not entry['enabled']:
if entry['requirements']['details']:
click.echo(
' ' + str(entry['requirements']['details']))
if entry['requirements']['packages_required']:
click.echo(' Python Packages Required:')
for req in entry['requirements']['packages_required']:
click.echo(' - ' + req)
if entry['requirements']['packages_recommended']:
click.echo(' Python Packages Recommended:')
for req in entry['requirements']['packages_recommended']:
click.echo(' - ' + req)
# new line padding between entries
click.echo()
continue
if len(protocols) > 1:
click.echo('| Schema(s): {}'.format(
', '.join(protocols),
))
prefix = ' - '
click.echo('{}{}'.format(
prefix,
'\n{}'.format(prefix).join(entry['details']['templates'])))
# new line padding between entries
click.echo()
sys.exit(0)
# The priorities of what is accepted are parsed in order below:
# 1. URLs by command line
# 2. Configuration by command line

View File

@ -52,6 +52,54 @@ class NotifyBase(BASE_OBJECT):
This is the base class for all notification services
"""
# An internal flag used to test the state of the plugin. If set to
# False, then the plugin is not used. Plugins can disable themselves
# due to enviroment issues (such as missing libraries, or platform
# dependencies that are not present). By default all plugins are
# enabled.
enabled = True
# Some plugins may require additional packages above what is provided
# already by Apprise.
#
# Use this section to relay this information to the users of the script to
# help guide them with what they need to know if they plan on using your
# plugin. The below configuration should otherwise accomodate all normal
# situations and will not requrie any updating:
requirements = {
# Use the description to provide a human interpretable description of
# what is required to make the plugin work. This is only nessisary
# if there are package dependencies. Setting this to default will
# cause a general response to be returned. Only set this if you plan
# on over-riding the default. Always consider language support here.
# So before providing a value do the following in your code base:
#
# from apprise.AppriseLocale import gettext_lazy as _
#
# 'details': _('My detailed requirements')
'details': None,
# Define any required packages needed for the plugin to run. This is
# an array of strings that simply look like lines residing in a
# `requirements.txt` file...
#
# As an example, an entry may look like:
# 'packages_required': [
# 'cryptography < 3.4`,
# ]
'packages_required': [],
# Recommended packages identify packages that are not required to make
# your plugin work, but would improve it's use or grant it access to
# full functionality (that might otherwise be limited).
# Similar to `packages_required`, you would identify each entry in
# the array as you would in a `requirements.txt` file.
#
# - Do not re-provide entries already in the `packages_required`
'packages_recommended': [],
}
# The services URL
service_url = None
@ -223,6 +271,13 @@ class NotifyBase(BASE_OBJECT):
"""
if not self.enabled:
# Deny notifications issued to services that are disabled
self.logger.warning(
"{} is currently disabled on this system.".format(
self.service_name))
return False
# Prepare attachments if required
if attach is not None and not isinstance(attach, AppriseAttachment):
try:

View File

@ -38,10 +38,6 @@ NOTIFY_DBUS_SUPPORT_ENABLED = False
# Image support is dependant on the GdkPixbuf library being available
NOTIFY_DBUS_IMAGE_SUPPORT = False
# The following are required to hook into the notifications:
NOTIFY_DBUS_INTERFACE = 'org.freedesktop.Notifications'
NOTIFY_DBUS_SETTING_LOCATION = '/org/freedesktop/Notifications'
# Initialize our mainloops
LOOP_GLIB = None
LOOP_QT = None
@ -132,8 +128,19 @@ class NotifyDBus(NotifyBase):
A wrapper for local DBus/Qt Notifications
"""
# Set our global enabled flag
enabled = NOTIFY_DBUS_SUPPORT_ENABLED
requirements = {
# Define our required packaging in order to work
'details': _('libdbus-1.so.x must be installed.')
}
# The default descriptive name associated with the Notification
service_name = 'DBus Notification'
service_name = _('DBus Notification')
# The services URL
service_url = 'http://www.freedesktop.org/Software/dbus/'
# The default protocols
# Python 3 keys() does not return a list object, it's it's own dict_keys()
@ -158,14 +165,9 @@ class NotifyDBus(NotifyBase):
# content to display
body_max_line_count = 10
# This entry is a bit hacky, but it allows us to unit-test this library
# in an environment that simply doesn't have the gnome packages
# available to us. It also allows us to handle situations where the
# packages actually are present but we need to test that they aren't.
# If anyone is seeing this had knows a better way of testing this
# outside of what is defined in test/test_glib_plugin.py, please
# let me know! :)
_enabled = NOTIFY_DBUS_SUPPORT_ENABLED
# The following are required to hook into the notifications:
dbus_interface = 'org.freedesktop.Notifications'
dbus_setting_location = '/org/freedesktop/Notifications'
# Define object templates
templates = (
@ -241,12 +243,6 @@ class NotifyDBus(NotifyBase):
"""
Perform DBus Notification
"""
if not self._enabled or MAINLOOP_MAP[self.schema] is None:
self.logger.warning(
"{} notifications could not be loaded.".format(self.schema))
return False
# Acquire our session
try:
session = SessionBus(mainloop=MAINLOOP_MAP[self.schema])
@ -265,14 +261,14 @@ class NotifyDBus(NotifyBase):
# acquire our dbus object
dbus_obj = session.get_object(
NOTIFY_DBUS_INTERFACE,
NOTIFY_DBUS_SETTING_LOCATION,
self.dbus_interface,
self.dbus_setting_location,
)
# Acquire our dbus interface
dbus_iface = Interface(
dbus_obj,
dbus_interface=NOTIFY_DBUS_INTERFACE,
dbus_interface=self.dbus_interface,
)
# image path

View File

@ -48,7 +48,6 @@
import six
import requests
from json import dumps
from .oauth import GoogleOAuth
from ..NotifyBase import NotifyBase
from ...common import NotifyType
from ...utils import validate_regex
@ -56,6 +55,23 @@ from ...utils import parse_list
from ...AppriseAttachment import AppriseAttachment
from ...AppriseLocale import gettext_lazy as _
# Default our global support flag
NOTIFY_FCM_SUPPORT_ENABLED = False
try:
from .oauth import GoogleOAuth
# We're good to go
NOTIFY_FCM_SUPPORT_ENABLED = True
except ImportError:
# cryptography is the dependency of the .oauth library
# Create a dummy object for init() call to work
class GoogleOAuth(object):
pass
# Our lookup map
FCM_HTTP_ERROR_MAP = {
400: 'A bad request was made to the server.',
@ -88,6 +104,15 @@ class NotifyFCM(NotifyBase):
"""
A wrapper for Google's Firebase Cloud Messaging Notifications
"""
# Set our global enabled flag
enabled = NOTIFY_FCM_SUPPORT_ENABLED
requirements = {
# Define our required packaging in order to work
'packages_required': 'cryptography'
}
# The default descriptive name associated with the Notification
service_name = 'Firebase Cloud Messaging'

View File

@ -52,6 +52,7 @@ except ImportError:
from urllib.parse import urlencode as _urlencode
try:
# Python 3.x
from json.decoder import JSONDecodeError
except ImportError:

View File

@ -78,8 +78,19 @@ class NotifyGnome(NotifyBase):
A wrapper for local Gnome Notifications
"""
# Set our global enabled flag
enabled = NOTIFY_GNOME_SUPPORT_ENABLED
requirements = {
# Define our required packaging in order to work
'details': _('A local Gnome environment is required.')
}
# The default descriptive name associated with the Notification
service_name = 'Gnome Notification'
service_name = _('Gnome Notification')
# The service URL
service_url = 'https://www.gnome.org/'
# The default protocol
protocol = 'gnome'
@ -102,15 +113,6 @@ class NotifyGnome(NotifyBase):
# cause any title (if defined) to get placed into the message body.
title_maxlen = 0
# This entry is a bit hacky, but it allows us to unit-test this library
# in an environment that simply doesn't have the gnome packages
# available to us. It also allows us to handle situations where the
# packages actually are present but we need to test that they aren't.
# If anyone is seeing this had knows a better way of testing this
# outside of what is defined in test/test_gnome_plugin.py, please
# let me know! :)
_enabled = NOTIFY_GNOME_SUPPORT_ENABLED
# Define object templates
templates = (
'{schema}://',
@ -157,11 +159,6 @@ class NotifyGnome(NotifyBase):
Perform Gnome Notification
"""
if not self._enabled:
self.logger.warning(
"Gnome Notifications are not supported by this system.")
return False
try:
# App initialization
Notify.init(self.app_id)

View File

@ -68,6 +68,13 @@ class NotifyGrowl(NotifyBase):
A wrapper to Growl Notifications
"""
# Set our global enabled flag
enabled = NOTIFY_GROWL_SUPPORT_ENABLED
requirements = {
# Define our required packaging in order to work
'packages_required': 'gntp'
}
# The default descriptive name associated with the Notification
service_name = 'Growl'
@ -84,15 +91,6 @@ class NotifyGrowl(NotifyBase):
# Allows the user to specify the NotifyImageSize object
image_size = NotifyImageSize.XY_72
# This entry is a bit hacky, but it allows us to unit-test this library
# in an environment that simply doesn't have the windows packages
# available to us. It also allows us to handle situations where the
# packages actually are present but we need to test that they aren't.
# If anyone is seeing this had knows a better way of testing this
# outside of what is defined in test/test_growl_plugin.py, please
# let me know! :)
_enabled = NOTIFY_GROWL_SUPPORT_ENABLED
# Disable throttle rate for Growl requests since they are normally
# local anyway
request_rate_per_sec = 0
@ -251,13 +249,6 @@ class NotifyGrowl(NotifyBase):
"""
Perform Growl Notification
"""
if not self._enabled:
self.logger.warning(
"Growl Notifications are not supported by this system; "
"`pip install gntp`.")
return False
# Register ourselves with the server if we haven't done so already
if not self.growl and not self.register():
# We failed to register

View File

@ -87,6 +87,14 @@ class NotifyMQTT(NotifyBase):
A wrapper for MQTT Notifications
"""
# Set our global enabled flag
enabled = NOTIFY_MQTT_SUPPORT_ENABLED
requirements = {
# Define our required packaging in order to work
'packages_required': 'paho-mqtt'
}
# The default descriptive name associated with the Notification
service_name = 'MQTT Notification'
@ -110,15 +118,6 @@ class NotifyMQTT(NotifyBase):
# locally hosted anyway
request_rate_per_sec = 0.5
# This entry is a bit hacky, but it allows us to unit-test this library
# in an environment that simply doesn't have the mqtt packages
# available to us. It also allows us to handle situations where the
# packages actually are present but we need to test that they aren't.
# If anyone is seeing this had knows a better way of testing this
# outside of what is defined in test/test_mqtt_plugin.py, please
# let me know! :)
_enabled = NOTIFY_MQTT_SUPPORT_ENABLED
# Port Defaults (unless otherwise specified)
mqtt_insecure_port = 1883
@ -275,10 +274,6 @@ class NotifyMQTT(NotifyBase):
(cert for cert in self.CA_CERTIFICATE_FILE_LOCATIONS
if isfile(cert)), None)
if not self._enabled:
# Nothing more we can do
return
# Set up our MQTT Publisher
try:
# Get our protocol
@ -310,12 +305,6 @@ class NotifyMQTT(NotifyBase):
Perform MQTT Notification
"""
if not self._enabled:
self.logger.warning(
"MQTT Notifications are not supported by this system; "
"`pip install paho-mqtt`.")
return False
if len(self.topics) == 0:
# There were no services to notify
self.logger.warning('There were no MQTT topics to notify.')

View File

@ -36,6 +36,19 @@ from ..common import NotifyType
from ..utils import parse_bool
from ..AppriseLocale import gettext_lazy as _
# Default our global support flag
NOTIFY_MACOSX_SUPPORT_ENABLED = False
if platform.system() == 'Darwin':
# Check this is Mac OS X 10.8, or higher
major, minor = platform.mac_ver()[0].split('.')[:2]
# Toggle our enabled flag if verion is correct and executable
# found. This is done in such a way to provide verbosity to the
# end user so they know why it may or may not work for them.
NOTIFY_MACOSX_SUPPORT_ENABLED = \
(int(major) > 10 or (int(major) == 10 and int(minor) >= 8))
class NotifyMacOSX(NotifyBase):
"""
@ -44,8 +57,22 @@ class NotifyMacOSX(NotifyBase):
Source: https://github.com/julienXX/terminal-notifier
"""
# Set our global enabled flag
enabled = NOTIFY_MACOSX_SUPPORT_ENABLED
requirements = {
# Define our required packaging in order to work
'details': _(
'Only works with Mac OS X 10.8 and higher. Additionally '
' requires that /usr/local/bin/terminal-notifier is locally '
'accessible.')
}
# The default descriptive name associated with the Notification
service_name = 'MacOSX Notification'
service_name = _('MacOSX Notification')
# The services URL
service_url = 'https://github.com/julienXX/terminal-notifier'
# The default protocol
protocol = 'macosx'
@ -100,31 +127,8 @@ class NotifyMacOSX(NotifyBase):
# or not.
self.include_image = include_image
self._enabled = False
if platform.system() == 'Darwin':
# Check this is Mac OS X 10.8, or higher
major, minor = platform.mac_ver()[0].split('.')[:2]
# Toggle our _enabled flag if verion is correct and executable
# found. This is done in such a way to provide verbosity to the
# end user so they know why it may or may not work for them.
if not (int(major) > 10 or (int(major) == 10 and int(minor) >= 8)):
self.logger.warning(
"MacOSX Notifications require your OS to be at least "
"v10.8 (detected {}.{})".format(major, minor))
elif not os.access(self.notify_path, os.X_OK):
self.logger.warning(
"MacOSX Notifications require '{}' to be in place."
.format(self.notify_path))
else:
# We're good to go
self._enabled = True
# Set sound object (no q/a for now)
self.sound = sound
return
def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
@ -132,9 +136,10 @@ class NotifyMacOSX(NotifyBase):
Perform MacOSX Notification
"""
if not self._enabled:
if not os.access(self.notify_path, os.X_OK):
self.logger.warning(
"MacOSX Notifications are not supported by this system.")
"MacOSX Notifications require '{}' to be in place."
.format(self.notify_path))
return False
# Start with our notification path
@ -160,6 +165,9 @@ class NotifyMacOSX(NotifyBase):
# Always call throttle before any remote server i/o is made
self.throttle()
# Capture some output for helpful debugging later on
self.logger.debug('MacOSX CMD: {}'.format(' '.join(cmd)))
# Send our notification
output = subprocess.Popen(
cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)

View File

@ -32,20 +32,37 @@ from ..common import NotifyType
from ..utils import validate_regex
from ..AppriseLocale import gettext_lazy as _
from cryptography.hazmat.primitives import padding
from cryptography.hazmat.primitives.ciphers import Cipher
from cryptography.hazmat.primitives.ciphers import algorithms
from cryptography.hazmat.primitives.ciphers import modes
from cryptography.hazmat.backends import default_backend
from base64 import urlsafe_b64encode
import hashlib
try:
from cryptography.hazmat.primitives import padding
from cryptography.hazmat.primitives.ciphers import Cipher
from cryptography.hazmat.primitives.ciphers import algorithms
from cryptography.hazmat.primitives.ciphers import modes
from cryptography.hazmat.backends import default_backend
# We're good to go!
NOTIFY_SIMPLEPUSH_ENABLED = True
except ImportError:
# cryptography is required in order for this package to work
NOTIFY_SIMPLEPUSH_ENABLED = False
class NotifySimplePush(NotifyBase):
"""
A wrapper for SimplePush Notifications
"""
# Set our global enabled flag
enabled = NOTIFY_SIMPLEPUSH_ENABLED
requirements = {
# Define our required packaging in order to work
'packages_required': 'cryptography'
}
# The default descriptive name associated with the Notification
service_name = 'SimplePush'

View File

@ -56,6 +56,13 @@ class NotifyWindows(NotifyBase):
"""
A wrapper for local Windows Notifications
"""
# Set our global enabled flag
enabled = NOTIFY_WINDOWS_SUPPORT_ENABLED
requirements = {
# Define our required packaging in order to work
'details': _('A local Microsoft Windows environment is required.')
}
# The default descriptive name associated with the Notification
service_name = 'Windows Notification'
@ -80,15 +87,6 @@ class NotifyWindows(NotifyBase):
# The number of seconds to display the popup for
default_popup_duration_sec = 12
# This entry is a bit hacky, but it allows us to unit-test this library
# in an environment that simply doesn't have the windows packages
# available to us. It also allows us to handle situations where the
# packages actually are present but we need to test that they aren't.
# If anyone is seeing this had knows a better way of testing this
# outside of what is defined in test/test_windows_plugin.py, please
# let me know! :)
_enabled = NOTIFY_WINDOWS_SUPPORT_ENABLED
# Define object templates
templates = (
'{schema}://',
@ -144,12 +142,6 @@ class NotifyWindows(NotifyBase):
Perform Windows Notification
"""
if not self._enabled:
self.logger.warning(
"Windows Notifications are not supported by this system; "
"`pip install pywin32`.")
return False
# Always call throttle before any remote server i/o is made
self.throttle()

View File

@ -149,13 +149,8 @@ class SliXmppAdapter(object):
# If the user specified a port, skip SRV resolving, otherwise it is a
# lot easier to let slixmpp handle DNS instead of the user.
if self.port is None:
override_connection = None
else:
override_connection = (self.host, self.port)
# Instruct slixmpp to connect to the XMPP service.
self.xmpp.connect(override_connection, use_ssl=self.secure)
self.override_connection = \
None if not self.port else (self.host, self.port)
# We're good
return True
@ -166,6 +161,11 @@ class SliXmppAdapter(object):
"""
# Instruct slixmpp to connect to the XMPP service.
if not self.xmpp.connect(
self.override_connection, use_ssl=self.secure):
return False
# Run the asyncio event loop, and return once disconnected,
# for any reason.
self.xmpp.process(forever=False)
@ -191,7 +191,8 @@ class SliXmppAdapter(object):
self.before_message()
# The message we wish to send, and the JID that will receive it.
self.xmpp.send_message(mto=target, msubject=self.subject,
self.xmpp.send_message(
mto=target, msubject=self.subject,
mbody=self.body, mtype='chat')
# Using wait=True ensures that the send queue will be

View File

@ -40,10 +40,22 @@ class NotifyXMPP(NotifyBase):
"""
A wrapper for XMPP Notifications
"""
# Set our global enabled flag
enabled = SliXmppAdapter._enabled
requirements = {
# Define our required packaging in order to work
'packages_required': [
"slixmpp; python_version >= '3.7'",
]
}
# The default descriptive name associated with the Notification
service_name = 'XMPP'
# The services URL
service_url = 'https://xmpp.org/'
# The default protocol
protocol = 'xmpp'
@ -56,14 +68,7 @@ class NotifyXMPP(NotifyBase):
# Lower throttle rate for XMPP
request_rate_per_sec = 0.5
# This entry is a bit hacky, but it allows us to unit-test this library
# in an environment that simply doesn't have the slixmpp package
# available to us.
#
# If anyone is seeing this had knows a better way of testing this
# outside of what is defined in test/test_xmpp_plugin.py, please
# let me know! :)
_enabled = SliXmppAdapter._enabled
# Our XMPP Adapter we use to communicate through
_adapter = SliXmppAdapter if SliXmppAdapter._enabled else None
# Define object templates
@ -211,12 +216,6 @@ class NotifyXMPP(NotifyBase):
Perform XMPP Notification
"""
if not self._enabled:
self.logger.warning(
'XMPP Notifications are not supported by this system '
'- install slixmpp.')
return False
# Detect our JID if it isn't otherwise specified
jid = self.jid
password = self.password

View File

@ -439,6 +439,92 @@ def details(plugin):
}
def requirements(plugin):
"""
Provides a list of packages and its requirement details
"""
requirements = {
# Use the description to provide a human interpretable description of
# what is required to make the plugin work. This is only nessisary
# if there are package dependencies
'details': '',
# Define any required packages needed for the plugin to run. This is
# an array of strings that simply look like lines in the
# `requirements.txt` file...
#
# A single string is perfectly acceptable:
# 'packages_required' = 'cryptography'
#
# Multiple entries should look like the following
# 'packages_required' = [
# 'cryptography < 3.4`,
# ]
#
'packages_required': [],
# Recommended packages identify packages that are not required to make
# your plugin work, but would improve it's use or grant it access to
# full functionality (that might otherwise be limited).
# Similar to `packages_required`, you would identify each entry in
# the array as you would in a `requirements.txt` file.
#
# - Do not re-provide entries already in the `packages_required`
'packages_recommended': [],
}
# Populate our template differently if we don't find anything above
if not (hasattr(plugin, 'requirements')
and isinstance(plugin.requirements, dict)):
# We're done early
return requirements
# Get our required packages
_req_packages = plugin.requirements.get('packages_required')
if isinstance(_req_packages, six.string_types):
# Convert to list
_req_packages = [_req_packages]
elif not isinstance(_req_packages, (set, list, tuple)):
# Allow one to set the required packages to None (as an example)
_req_packages = []
requirements['packages_required'] = [str(p) for p in _req_packages]
# Get our recommended packages
_opt_packages = plugin.requirements.get('packages_recommended')
if isinstance(_opt_packages, six.string_types):
# Convert to list
_opt_packages = [_opt_packages]
elif not isinstance(_opt_packages, (set, list, tuple)):
# Allow one to set the recommended packages to None (as an example)
_opt_packages = []
requirements['packages_recommended'] = [str(p) for p in _opt_packages]
# Get our package details
_req_details = plugin.requirements.get('details')
if not _req_details:
if not (_req_packages and _opt_packages):
_req_details = _('No dependencies.')
elif _req_packages:
_req_details = _('Packages are required to function.')
else: # opt_packages
_req_details = \
_('Packages are recommended to improve functionality.')
else:
# Store our details if defined
requirements['details'] = _req_details
# Return our compiled package requirements
return requirements
def url_to_dict(url, secure_logging=True):
"""
Takes an apprise URL and returns the tokens associated with it

View File

@ -5,16 +5,3 @@ pytest
pytest-cov
tox
babel
#
# Plugin Dependencies
#
# Provides xmpp:// support
slixmpp; python_version >= '3.7'
# Provides growl:// support
gntp
# Provides mqtt:// support
paho-mqtt

View File

@ -1,224 +1,141 @@
.\" generated with Ronn/v0.7.3
.\" http://github.com/rtomayko/ronn/tree/0.7.3
.
.TH "APPRISE" "1" "February 2021" "ff" ""
.
.\" generated with Ronn-NG/v0.9.1
.\" http://github.com/apjanke/ronn-ng/tree/0.9.1
.TH "APPRISE" "1" "November 2021" ""
.SH "NAME"
\fBapprise\fR \- Push Notifications that work with just about every platform!
.
.SH "SYNOPSIS"
\fBapprise\fR [\fIoptions\fR\.\.\.] \fIservice\-url\fR\.\.\.
.
\fBapprise\fR [\fIoptions\fR\|\.\|\.\|\.] \fIservice\-url\fR\|\.\|\.\|\.
.br
.
.SH "DESCRIPTION"
\fBApprise\fR allows you to send a notification to \fIalmost all\fR of the most popular notification services available to us today such as: Discord, Telegram, Pushbullet, Slack, Twitter, etc\.
.
.IP "\(bu" 4
.IP "\[ci]" 4
One notification library to rule them all\.
.
.IP "\(bu" 4
.IP "\[ci]" 4
A common and intuitive notification syntax\.
.
.IP "\(bu" 4
.IP "\[ci]" 4
Supports the handling of images (to the notification services that will accept them)\.
.
.IP "" 0
.
.SH "OPTIONS"
The Apprise options are as follows:
.
.P
\fB\-b\fR, \fB\-\-body=\fR\fITEXT\fR: Specify the message body\. If no body is specified then content is read from \fIstdin\fR\.
.
.P
\fB\-t\fR, \fB\-\-title=\fR\fITEXT\fR: Specify the message title\. This field is complete optional\.
.
.P
\fB\-c\fR, \fB\-\-config=\fR\fICONFIG\-URL\fR: Specify one or more configuration locations\.
.
.P
\fB\-a\fR, \fB\-\-attach=\fR\fIATTACH\-URL\fR: Specify one or more file attachment locations\.
.
.P
\fB\-n\fR, \fB\-\-notification\-type=\fR\fITYPE\fR: Specify the message type (default=info)\. Possible values are "info", "success", "failure", and "warning"\.
.
.P
\fB\-i\fR, \fB\-\-input\-format=\fR\fIFORMAT\fR: Specify the input message format (default=text)\. Possible values are "text", "html", and "markdown"\.
.
.P
\fB\-T\fR, \fB\-\-theme=\fRTHEME: Specify the default theme\.
.
.P
\fB\-g\fR, \fB\-\-tag=\fRTAG: Specify one or more tags to filter which services to notify\. Use multiple \fB\-\-tag\fR (\fB\-g\fR) entries to \fBOR\fR the tags together and comma separated to \fBAND\fR them\. If no tags are specified then all services are notified\.
.
.P
\fB\-Da\fR, \fB\-\-disable\-async\fR: Send notifications synchronously (one after the other) instead of all at once\.
.
.P
\fB\-R\fR, \fB\-\-recursion\-depth\fR: he number of recursive import entries that can be loaded from within Apprise configuration\. By default this is set to 1\. If this is set to zero, then import statements found in any configuration is ignored\.
.
.P
\fB\-e\fR, \fB\-\-interpret\-escapes\fR Enable interpretation of backslash escapes\. For example, this would convert sequences such as \en and \er to their respected ascii new\-line and carriage
.
.P
\fB\-d\fR, \fB\-\-dry\-run\fR: Perform a trial run but only prints the notification services to\-be triggered to \fBstdout\fR\. Notifications are never sent using this mode\.
.
.P
return characters prior to the delivery of the notification\.
.
.P
\fB\-l\fR, \fB\-\-details\fR Prints details about the current services supported by Apprise\.
.P
\fB\-v\fR, \fB\-\-verbose\fR: The more of these you specify, the more verbose the output is\. e\.g: \-vvvv
.
.P
\fB\-D\fR, \fB\-\-debug\fR: A debug mode; useful for troubleshooting\.
.
.P
\fB\-V\fR, \fB\-\-version\fR: Display the apprise version and exit\.
.
.P
\fB\-h\fR, \fB\-\-help\fR: Show this message and exit\.
.
.SH "EXIT STATUS"
\fBapprise\fR exits with a status of:
.
.IP "\(bu" 4
.IP "\[ci]" 4
\fB0\fR if all of the notifications were sent successfully\.
.
.IP "\(bu" 4
.IP "\[ci]" 4
\fB1\fR if one or more notifications could not be sent\.
.
.IP "\(bu" 4
.IP "\[ci]" 4
\fB2\fR if there was an error specified on the command line such as not providing an valid argument\.
.
.IP "\(bu" 4
.IP "\[ci]" 4
\fB3\fR if there was one or more Apprise Service URLs successfully loaded but none could be notified due to user filtering (via tags)\.
.
.IP "" 0
.
.SH "SERVICE URLS"
There are to many service URL and combinations to list here\. It\'s best to visit the Apprise GitHub page \fIhttps://github\.com/caronc/apprise/wiki#notification\-services\fR and see what\'s available\.
.
.SH "EXAMPLES"
Send a notification to as many servers as you want to specify as you can easily chain them together:
.
.IP "" 4
.
.nf
$ apprise \-vv \-t "my title" \-b "my notification body" \e
"mailto://myemail:mypass@gmail\.com" \e
"pbul://o\.gn5kj6nfhv736I7jC3cj3QLRiyhgl98b"
.
.fi
.
.IP "" 0
.
.P
If you don\'t specify a \fB\-\-body\fR (\fB\-b\fR) then stdin is used allowing you to use the tool as part of your every day administration:
.
.IP "" 4
.
.nf
$ cat /proc/cpuinfo | apprise \-vv \-t "cpu info" \e
"mailto://myemail:mypass@gmail\.com"
.
.fi
.
.IP "" 0
.
.P
Load in a configuration file which identifies all of your notification service URLs and notify them all:
.
.IP "" 4
.
.nf
$ apprise \-vv \-t "my title" \-b "my notification body" \e
\-\-config=~/apprise\.yml
.
.fi
.
.IP "" 0
.
.P
Load in a configuration file from a remote server that identifies all of your notification service URLs and only notify the ones tagged as \fIdevops\fR\.
.
.IP "" 4
.
.nf
$ apprise \-vv \-t "my title" \-b "my notification body" \e
\-\-config=https://localhost/my/apprise/config \e
\-t devops
.
.fi
.
.IP "" 0
.
.P
Include an attachment:
.
.IP "" 4
.
.nf
$ apprise \-vv \-t "School Assignment" \-b "See attached" \e
\-\-attach=Documents/FinalReport\.docx
.
.fi
.
.IP "" 0
.
.SH "CONFIGURATION"
A configuration file can be in the format of either \fBTEXT\fR or \fBYAML\fR where [TEXT][textconfig] is the easiest and most ideal solution for most users\. However YAML \fIhttps://github\.com/caronc/apprise/wiki/config_yaml\fR configuration files grants the user a bit more leverage and access to some of the internal features of Apprise\. Reguardless of which format you choose, both provide the users the ability to leverage \fBtagging\fR which adds a more rich and powerful notification environment\.
.
.P
Configuration files can be directly referenced via \fBapprise\fR when referencing the \fB\-\-config=\fR (\fB\-c\fR) CLI directive\. You can identify as many as you like on the command line and all of them will be loaded\. You can also point your configuration to a cloud location (by referencing \fBhttp://\fR or \fBhttps://\fR\. By default \fBapprise\fR looks in the following local locations for configuration files and loads them:
.
.IP "" 4
.
.nf
$ ~/\.apprise
$ ~/\.apprise\.yml
$ ~/\.config/apprise
$ ~/\.config/apprise\.yml
.
.fi
.
.IP "" 0
.
.P
If a default configuration file is referenced in any way by the \fBapprise\fR tool, you no longer need to provide it a Service URL\. Usage of the \fBapprise\fR tool simplifies to:
.
.IP "" 4
.
.nf
$ apprise \-vv \-t "my title" \-b "my notification body"
.
.fi
.
.IP "" 0
.
.P
If you leveraged tagging \fIhttps://github\.com/caronc/apprise/wiki/CLI_Usage#label\-leverage\-tagging\fR, you can define all of Apprise Service URLs in your configuration that you want and only specifically notify a subset of them:
.
.IP "" 4
.
.nf
$ apprise \-vv \-t "Will Be Late" \-b "Go ahead and make dinner without me" \e
\-\-tag=family
.
.fi
.
.IP "" 0
.
.SH "BUGS"
If you find any bugs, please make them known at: \fIhttps://github\.com/caronc/apprise/issues\fR
.
.SH "COPYRIGHT"
Apprise is Copyright (C) 2021 Chris Caron \fIlead2gold@gmail\.com\fR

View File

@ -68,6 +68,9 @@ The Apprise options are as follows:
return characters prior to the delivery of the notification.
`-l`, `--details`
Prints details about the current services supported by Apprise.
`-v`, `--verbose`:
The more of these you specify, the more verbose the output is. e.g: -vvvv

View File

@ -1,4 +1,3 @@
cryptography
requests
requests-oauthlib
six

View File

@ -18,6 +18,7 @@ test=pytest
[tool:pytest]
addopts = --verbose -ra
python_files = test/test_*.py
norecursedirs=test/helpers
filterwarnings =
once::Warning
strict = true

View File

@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2020 Chris Caron <lead2gold@gmail.com>
# Copyright (C) 2021 Chris Caron <lead2gold@gmail.com>
# All rights reserved.
#
# This code is licensed under the MIT License.
@ -22,43 +22,6 @@
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
import six
import mock
import requests
from apprise import plugins
# Disable logging for a cleaner testing output
import logging
logging.disable(logging.CRITICAL)
@mock.patch('requests.post')
def test_nextcloud_empty_body(mock_post):
"""
API: NotifyNextcloud() empty body
"""
# Disable Throttling to speed testing
plugins.NotifyBase.request_rate_per_sec = 0
# A response
robj = mock.Mock()
robj.content = ''
robj.status_code = requests.codes.ok
# Prepare Mock
mock_post.return_value = robj
# Variation Initializations
obj = plugins.NotifyNextcloud(
host="localhost", user="admin", password="pass", targets="user")
assert isinstance(obj, plugins.NotifyNextcloud) is True
assert isinstance(obj.url(), six.string_types) is True
# An empty body
assert obj.send(body="") is True
assert 'data' in mock_post.call_args_list[0][1]
assert 'shortMessage' in mock_post.call_args_list[0][1]['data']
# The longMessage argument is not set
assert 'longMessage' not in mock_post.call_args_list[0][1]['data']
import sys
import os
sys.path.append(os.path.join(os.path.dirname(__file__), 'helpers'))

View File

@ -22,42 +22,8 @@
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
from .rest import AppriseURLTester
import six
import mock
import requests
from apprise import plugins
from apprise import Apprise
# Disable logging for a cleaner testing output
import logging
logging.disable(logging.CRITICAL)
@mock.patch('requests.post')
def test_homeassistant_plugin(mock_post):
"""
API: NotifyHomeAssistant() Tests
"""
# Disable Throttling to speed testing
plugins.NotifyBase.request_rate_per_sec = 0
response = mock.Mock()
response.content = ''
response.status_code = requests.codes.ok
# Prepare Mock
mock_post.return_value = response
# Variation Initializations
obj = Apprise.instantiate('hassio://localhost/accesstoken')
assert isinstance(obj, plugins.NotifyHomeAssistant) is True
assert isinstance(obj.url(), six.string_types) is True
# Send Notification
assert obj.send(body="test") is True
assert mock_post.call_count == 1
assert mock_post.call_args_list[0][0][0] == \
'http://localhost:8123/api/services/persistent_notification/create'
__all__ = [
'AppriseURLTester',
]

519
test/helpers/rest.py Normal file
View File

@ -0,0 +1,519 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2021 Chris Caron <lead2gold@gmail.com>
# All rights reserved.
#
# This code is licensed under the MIT License.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files(the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions :
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
import re
import os
import six
import requests
import mock
from json import dumps
from random import choice
from string import ascii_uppercase as str_alpha
from string import digits as str_num
from apprise import plugins
from apprise import NotifyType
from apprise import Apprise
from apprise import AppriseAsset
from apprise import AppriseAttachment
from apprise.common import OverflowMode
# Disable logging for a cleaner testing output
import logging
logging.disable(logging.CRITICAL)
class AppriseURLTester(object):
# Some exception handling we'll use
req_exceptions = (
requests.ConnectionError(
0, 'requests.ConnectionError() not handled'),
requests.RequestException(
0, 'requests.RequestException() not handled'),
requests.HTTPError(
0, 'requests.HTTPError() not handled'),
requests.ReadTimeout(
0, 'requests.ReadTimeout() not handled'),
requests.TooManyRedirects(
0, 'requests.TooManyRedirects() not handled'),
)
# Attachment Testing Directory
__test_var_dir = os.path.join(
os.path.dirname(os.path.dirname(__file__)), 'var')
# Our URLs we'll test against
__tests = []
# Define how many characters exist per line
row = 80
# Some variables we use to control the data we work with
body_len = 1024
title_len = 1024
def __init__(self, tests=None, *args, **kwargs):
"""
Our initialization
"""
# Create a large body and title with random data
self.body = ''.join(
choice(str_alpha + str_num + ' ') for _ in range(self.body_len))
self.body = '\r\n'.join(
[self.body[i: i + self.row]
for i in range(0, len(self.body), self.row)])
# Create our title using random data
self.title = ''.join(
choice(str_alpha + str_num) for _ in range(self.title_len))
if tests:
self.__tests = tests
def add(self, url, meta):
"""
Adds a test suite to our object
"""
self.__tests.append({
'url': url,
'meta': meta,
})
def run_all(self):
"""
Run all of our tests
"""
# iterate over our dictionary and test it out
for (url, meta) in self.__tests:
self.run(url, meta)
@mock.patch('requests.get')
@mock.patch('requests.post')
def run(self, url, meta, mock_post, mock_get):
"""
Run a specific test
"""
# Disable Throttling to speed testing
plugins.NotifyBase.request_rate_per_sec = 0
# Our expected instance
instance = meta.get('instance', None)
# Our expected server objects
_self = meta.get('self', None)
# Our expected Query response (True, False, or exception type)
response = meta.get('response', True)
# Our expected privacy url
# Don't set this if don't need to check it's value
privacy_url = meta.get('privacy_url')
# Our regular expression
url_matches = meta.get('url_matches')
# Allow us to force the server response code to be something other then
# the defaults
requests_response_code = meta.get(
'requests_response_code',
requests.codes.ok if response else requests.codes.not_found,
)
# Allow us to force the server response text to be something other then
# the defaults
requests_response_text = meta.get('requests_response_text')
if not isinstance(requests_response_text, six.string_types):
# Convert to string
requests_response_text = dumps(requests_response_text)
# Whether or not we should include an image with our request; unless
# otherwise specified, we assume that images are to be included
include_image = meta.get('include_image', True)
if include_image:
# a default asset
asset = AppriseAsset()
else:
# Disable images
asset = AppriseAsset(image_path_mask=False, image_url_mask=False)
asset.image_url_logo = None
test_requests_exceptions = meta.get(
'test_requests_exceptions', False)
# Mock our request object
robj = mock.Mock()
robj.content = u''
mock_get.return_value = robj
mock_post.return_value = robj
if test_requests_exceptions is False:
# Handle our default response
mock_post.return_value.status_code = requests_response_code
mock_get.return_value.status_code = requests_response_code
# Handle our default text response
mock_get.return_value.content = requests_response_text
mock_post.return_value.content = requests_response_text
mock_get.return_value.text = requests_response_text
mock_post.return_value.text = requests_response_text
# Ensure there is no side effect set
mock_post.side_effect = None
mock_get.side_effect = None
else:
# Handle exception testing; first we turn the boolean flag
# into a list of exceptions
test_requests_exceptions = self.req_exceptions
try:
# We can now instantiate our object:
obj = Apprise.instantiate(
url, asset=asset, suppress_exceptions=False)
except Exception as e:
# Handle our exception
if instance is None:
print('%s %s' % (url, str(e)))
raise e
if not isinstance(e, instance):
print('%s %s' % (url, str(e)))
raise e
# We're okay if we get here
return
if obj is None:
if instance is not None:
# We're done (assuming this is what we were
# expecting)
print("{} didn't instantiate itself "
"(we expected it to be a {})".format(
url, instance))
assert False
# We're done because we got the results we expected
return
if instance is None:
# Expected None but didn't get it
print('%s instantiated %s (but expected None)' % (
url, str(obj)))
assert False
if not isinstance(obj, instance):
print('%s instantiated %s (but expected %s)' % (
url, type(instance), str(obj)))
assert False
if isinstance(obj, plugins.NotifyBase):
# We loaded okay; now lets make sure we can reverse
# this url
assert isinstance(obj.url(), six.string_types) is True
# Test url() with privacy=True
assert isinstance(
obj.url(privacy=True), six.string_types) is True
# Some Simple Invalid Instance Testing
assert instance.parse_url(None) is None
assert instance.parse_url(object) is None
assert instance.parse_url(42) is None
if privacy_url:
# Assess that our privacy url is as expected
assert obj.url(
privacy=True).startswith(privacy_url)
if url_matches:
# Assess that our URL matches a set regex
assert re.search(url_matches, obj.url())
# Instantiate the exact same object again using the URL
# from the one that was already created properly
obj_cmp = Apprise.instantiate(obj.url())
# Our object should be the same instance as what we had
# originally expected above.
if not isinstance(obj_cmp, plugins.NotifyBase):
# Assert messages are hard to trace back with the
# way these tests work. Just printing before
# throwing our assertion failure makes things
# easier to debug later on
print('TEST FAIL: {} regenerated as {}'.format(
url, obj.url()))
assert False
# Tidy our object
del obj_cmp
if _self:
# Iterate over our expected entries inside of our
# object
for key, val in self.items():
# Test that our object has the desired key
assert hasattr(key, obj) is True
assert getattr(key, obj) == val
try:
self.__notify(url, obj, meta, asset)
except AssertionError:
# Don't mess with these entries
print('%s AssertionError' % url)
raise
# Tidy our object and allow any possible defined deconstructors to
# be executed.
del obj
@mock.patch('requests.get')
@mock.patch('requests.post')
def __notify(self, url, obj, meta, asset, mock_post, mock_get):
"""
Perform notification testing against object specified
"""
#
# Prepare our options
#
# Allow notification type override, otherwise default to INFO
notify_type = meta.get('notify_type', NotifyType.INFO)
# Whether or not we're testing exceptions or not
test_requests_exceptions = meta.get('test_requests_exceptions', False)
# Our expected Query response (True, False, or exception type)
response = meta.get('response', True)
# Our expected Notify response (True or False)
notify_response = meta.get('notify_response', response)
# Our expected Notify Attachment response (True or False)
attach_response = meta.get('attach_response', notify_response)
# Test attachments
# Don't set this if don't need to check it's value
check_attachments = meta.get('check_attachments', True)
# Allow us to force the server response code to be something other then
# the defaults
requests_response_code = meta.get(
'requests_response_code',
requests.codes.ok if response else requests.codes.not_found,
)
# Allow us to force the server response text to be something other then
# the defaults
requests_response_text = meta.get('requests_response_text')
if not isinstance(requests_response_text, six.string_types):
# Convert to string
requests_response_text = dumps(requests_response_text)
# A request
robj = mock.Mock()
robj.content = u''
mock_get.return_value = robj
mock_post.return_value = robj
if test_requests_exceptions is False:
# Handle our default response
mock_post.return_value.status_code = requests_response_code
mock_get.return_value.status_code = requests_response_code
# Handle our default text response
mock_get.return_value.content = requests_response_text
mock_post.return_value.content = requests_response_text
mock_get.return_value.text = requests_response_text
mock_post.return_value.text = requests_response_text
# Ensure there is no side effect set
mock_post.side_effect = None
mock_get.side_effect = None
else:
# Handle exception testing; first we turn the boolean flag
# into a list of exceptions
test_requests_exceptions = self.req_exceptions
try:
if test_requests_exceptions is False:
# Disable throttling
obj.request_rate_per_sec = 0
# check that we're as expected
assert obj.notify(
body=self.body, title=self.title,
notify_type=notify_type) == notify_response
# check that this doesn't change using different overflow
# methods
assert obj.notify(
body=self.body, title=self.title,
notify_type=notify_type,
overflow=OverflowMode.UPSTREAM) == notify_response
assert obj.notify(
body=self.body, title=self.title,
notify_type=notify_type,
overflow=OverflowMode.TRUNCATE) == notify_response
assert obj.notify(
body=self.body, title=self.title,
notify_type=notify_type,
overflow=OverflowMode.SPLIT) == notify_response
#
# Handle varations of the Asset Object missing fields
#
# First make a backup
app_id = asset.app_id
app_desc = asset.app_desc
# now clear records
asset.app_id = None
asset.app_desc = None
# Notify should still work
assert obj.notify(
body=self.body, title=self.title,
notify_type=notify_type) == notify_response
# App ID only
asset.app_id = app_id
asset.app_desc = None
# Notify should still work
assert obj.notify(
body=self.body, title=self.title,
notify_type=notify_type) == notify_response
# App Desc only
asset.app_id = None
asset.app_desc = app_desc
# Notify should still work
assert obj.notify(
body=self.body, title=self.title,
notify_type=notify_type) == notify_response
# Restore
asset.app_id = app_id
asset.app_desc = app_desc
if check_attachments:
# Test single attachment support; even if the service
# doesn't support attachments, it should still
# gracefully ignore the data
attach = os.path.join(
self.__test_var_dir, 'apprise-test.gif')
assert obj.notify(
body=self.body, title=self.title,
notify_type=notify_type,
attach=attach) == attach_response
# Same results should apply to a list of attachments
attach = AppriseAttachment((
os.path.join(self.__test_var_dir, 'apprise-test.gif'),
os.path.join(self.__test_var_dir, 'apprise-test.png'),
os.path.join(self.__test_var_dir, 'apprise-test.jpeg'),
))
assert obj.notify(
body=self.body, title=self.title,
notify_type=notify_type,
attach=attach) == attach_response
else:
# Disable throttling
obj.request_rate_per_sec = 0
for _exception in self.req_exceptions:
mock_post.side_effect = _exception
mock_get.side_effect = _exception
try:
assert obj.notify(
body=self.body, title=self.title,
notify_type=NotifyType.INFO) is False
except AssertionError:
# Don't mess with these entries
raise
except Exception:
# We can't handle this exception type
raise
except AssertionError:
# Don't mess with these entries
raise
except Exception as e:
# Check that we were expecting this exception to happen
try:
if not isinstance(e, response):
raise e
except TypeError:
print('%s Unhandled response %s' % (url, type(e)))
raise e
#
# Do the test again but without a title defined
#
try:
if test_requests_exceptions is False:
# check that we're as expected
assert obj.notify(body='body', notify_type=notify_type) \
== notify_response
else:
for _exception in self.req_exceptions:
mock_post.side_effect = _exception
mock_get.side_effect = _exception
try:
assert obj.notify(
body=self.body,
notify_type=NotifyType.INFO) is False
except AssertionError:
# Don't mess with these entries
raise
except Exception:
# We can't handle this exception type
raise
except AssertionError:
# Don't mess with these entries
raise
except Exception as e:
# Check that we were expecting this exception to happen
if not isinstance(e, response):
raise e
return True

View File

@ -43,6 +43,7 @@ from apprise import NotifyImageSize
from apprise import __version__
from apprise import URLBase
from apprise import PrivacyMode
from apprise.AppriseLocale import LazyTranslation
from apprise.plugins import SCHEMA_MAP
from apprise.plugins import __load_matrix
@ -961,6 +962,143 @@ def test_apprise_asset(tmpdir):
'http://localhost/default/info-256x256.test'
def test_apprise_disabled_plugins():
"""
API: Apprise() Disabled Plugin States
"""
# Reset our matrix
__reset_matrix()
class TestDisabled01Notification(NotifyBase):
"""
This class is used to test a pre-disabled state
"""
# Just flat out disable our service
enabled = False
# we'll use this as a key to make our service easier to find
# in the next part of the testing
service_name = 'na01'
def url(self, **kwargs):
# Support URL
return ''
def notify(self, **kwargs):
# Pretend everything is okay (so we don't break other tests)
return True
SCHEMA_MAP['na01'] = TestDisabled01Notification
class TestDisabled02Notification(NotifyBase):
"""
This class is used to test a post-disabled state
"""
# we'll use this as a key to make our service easier to find
# in the next part of the testing
service_name = 'na02'
def __init__(self, *args, **kwargs):
super(TestDisabled02Notification, self).__init__(**kwargs)
# enable state changes **AFTER** we initialize
self.enabled = False
def url(self, **kwargs):
# Support URL
return ''
def notify(self, **kwargs):
# Pretend everything is okay (so we don't break other tests)
return True
SCHEMA_MAP['na02'] = TestDisabled02Notification
# Create our Apprise instance
a = Apprise()
result = a.details(lang='ca-en', show_disabled=True)
assert isinstance(result, dict)
assert 'schemas' in result
assert len(result['schemas']) == 2
# our na01 is disabled right from the get-go
entry = next((x for x in result['schemas']
if x['service_name'] == 'na01'), None)
assert entry is not None
assert entry['enabled'] is False
plugin = a.instantiate('na01://localhost')
# Object is just flat out disabled... nothing is instatiated
assert plugin is None
# our na02 isn't however until it's initialized; as a result
# it get's returned in our result set
entry = next((x for x in result['schemas']
if x['service_name'] == 'na02'), None)
assert entry is not None
assert entry['enabled'] is True
plugin = a.instantiate('na02://localhost')
# Object isn't disabled until the __init__() call. But this is still
# enough to not instantiate the object:
assert plugin is None
# If we choose to filter our disabled, we can't unfortunately filter those
# that go disabled after instantiation, but we do filter out any that are
# already known to not be enabled:
result = a.details(lang='ca-en', show_disabled=False)
assert isinstance(result, dict)
assert 'schemas' in result
assert len(result['schemas']) == 1
# We'll add a good notification to our list
class TesEnabled01Notification(NotifyBase):
"""
This class is just a simple enabled one
"""
# we'll use this as a key to make our service easier to find
# in the next part of the testing
service_name = 'good'
def url(self, **kwargs):
# Support URL
return ''
def send(self, **kwargs):
# Pretend everything is okay (so we don't break other tests)
return True
SCHEMA_MAP['good'] = TesEnabled01Notification
# The last thing we'll simulate is a case where the plugin is just
# disabled at a later time long into it's life. this is just to allow
# administrators to stop the flow of their notifications for their own
# given reasons.
plugin = a.instantiate('good://localhost')
assert isinstance(plugin, NotifyBase)
# we'll toggle our state
plugin.enabled = False
# As a result, we can now no longer send a notification:
assert plugin.notify("My Message") is False
# As just a proof of how you can toggle the state back:
plugin.enabled = True
# our notifications will go okay now
assert plugin.notify("My Message") is True
# Reset our matrix
__reset_matrix()
__load_matrix()
def test_apprise_details():
"""
API: Apprise() Details
@ -1070,18 +1208,170 @@ def test_apprise_details():
# Support URL
return ''
def notify(self, **kwargs):
def send(self, **kwargs):
# Pretend everything is okay (so we don't break other tests)
return True
# Store our good detail notification in our schema map
SCHEMA_MAP['details'] = TestDetailNotification
# This is a made up class that is just used to verify
class TestReq01Notification(NotifyBase):
"""
This class is used to test various requirement configurations
"""
# Set some requirements
requirements = {
'packages_required': [
'cryptography <= 3.4',
'ultrasync',
],
'packages_recommended': 'django',
}
def url(self, **kwargs):
# Support URL
return ''
def send(self, **kwargs):
# Pretend everything is okay (so we don't break other tests)
return True
SCHEMA_MAP['req01'] = TestReq01Notification
# This is a made up class that is just used to verify
class TestReq02Notification(NotifyBase):
"""
This class is used to test various requirement configurations
"""
# Just not enabled at all
enabled = False
# Set some requirements
requirements = {
# None and/or [] is implied, but jsut to show that the code won't
# crash if explicitly set this way:
'packages_required': None,
'packages_recommended': [
'cryptography <= 3.4',
]
}
def url(self, **kwargs):
# Support URL
return ''
def send(self, **kwargs):
# Pretend everything is okay (so we don't break other tests)
return True
SCHEMA_MAP['req02'] = TestReq02Notification
# This is a made up class that is just used to verify
class TestReq03Notification(NotifyBase):
"""
This class is used to test various requirement configurations
"""
# Set some requirements
requirements = {
# We can over-ride the default details assigned to our plugin if
# specified
'details': _('some specified requirement details'),
# We can set a string value as well (it does not have to be a list)
'packages_recommended': 'cryptography <= 3.4'
}
def url(self, **kwargs):
# Support URL
return ''
def send(self, **kwargs):
# Pretend everything is okay (so we don't break other tests)
return True
SCHEMA_MAP['req03'] = TestReq03Notification
# This is a made up class that is just used to verify
class TestReq04Notification(NotifyBase):
"""
This class is used to test a case where our requirements is fixed
to a None
"""
# This is the same as saying there are no requirements
requirements = None
def url(self, **kwargs):
# Support URL
return ''
def send(self, **kwargs):
# Pretend everything is okay (so we don't break other tests)
return True
SCHEMA_MAP['req04'] = TestReq04Notification
# Create our Apprise instance
a = Apprise()
# Dictionary response
assert isinstance(a.details(), dict)
result = a.details()
assert isinstance(result, dict)
# Test different variations of our call
result = a.details(lang='ca-fr')
assert isinstance(result, dict)
for entry in result['schemas']:
# Verify our key does not exist because we did not ask for it
assert 'enabled' not in entry
assert 'requirements' not in entry
result = a.details(lang='us-en', show_requirements=True)
assert isinstance(result, dict)
for entry in result['schemas']:
# Verify our key does not exist because we did not ask for it
assert 'enabled' not in entry
# Requirements are set for display
assert 'requirements' in entry
assert 'details' in entry['requirements']
assert 'packages_required' in entry['requirements']
assert 'packages_recommended' in entry['requirements']
assert isinstance(entry['requirements']['details'], six.string_types)
assert isinstance(entry['requirements']['packages_required'], list)
assert isinstance(entry['requirements']['packages_recommended'], list)
result = a.details(lang='ca-en', show_disabled=True)
assert isinstance(result, dict)
for entry in result['schemas']:
# Verify that our plugin state is available to us
assert 'enabled' in entry
assert isinstance(entry['enabled'], bool)
# Verify our key does not exist because we did not ask for it
assert 'requirements' not in entry
result = a.details(
lang='ca-fr', show_requirements=True, show_disabled=True)
assert isinstance(result, dict)
for entry in result['schemas']:
# Plugin States are set for display
assert 'enabled' in entry
assert isinstance(entry['enabled'], bool)
# Requirements are set for display
assert 'requirements' in entry
assert 'details' in entry['requirements']
assert 'packages_required' in entry['requirements']
assert 'packages_recommended' in entry['requirements']
assert isinstance(entry['requirements']['details'], six.string_types)
assert isinstance(entry['requirements']['packages_required'], list)
assert isinstance(entry['requirements']['packages_recommended'], list)
# Reset our matrix
__reset_matrix()
@ -1173,7 +1463,8 @@ def test_apprise_details_plugin_verification():
# A Service Name MUST be defined
assert 'service_name' in entry
assert isinstance(entry['service_name'], six.string_types)
assert isinstance(
entry['service_name'], (six.string_types, LazyTranslation))
# Acquire our protocols
protocols = parse_list(

View File

@ -34,6 +34,8 @@ from apprise import NotifyBase
from click.testing import CliRunner
from apprise.plugins import SCHEMA_MAP
from apprise.utils import environ
from apprise.plugins import __load_matrix
from apprise.plugins import __reset_matrix
try:
@ -594,6 +596,195 @@ def test_apprise_cli_nux_env(tmpdir):
assert result.exit_code == 0
def test_apprise_cli_details(tmpdir):
"""
API: Apprise() Disabled Plugin States
"""
runner = CliRunner()
#
# Testing the printout of our details
# --details or -l
#
result = runner.invoke(cli.main, [
'--details',
])
assert result.exit_code == 0
result = runner.invoke(cli.main, [
'-l',
])
assert result.exit_code == 0
# Reset our matrix
__reset_matrix()
# This is a made up class that is just used to verify
class TestReq01Notification(NotifyBase):
"""
This class is used to test various requirement configurations
"""
# Set some requirements
requirements = {
'packages_required': [
'cryptography <= 3.4',
'ultrasync',
],
'packages_recommended': 'django',
}
def url(self, **kwargs):
# Support URL
return ''
def send(self, **kwargs):
# Pretend everything is okay (so we don't break other tests)
return True
SCHEMA_MAP['req01'] = TestReq01Notification
# This is a made up class that is just used to verify
class TestReq02Notification(NotifyBase):
"""
This class is used to test various requirement configurations
"""
# Just not enabled at all
enabled = False
# Set some requirements
requirements = {
# None and/or [] is implied, but jsut to show that the code won't
# crash if explicitly set this way:
'packages_required': None,
'packages_recommended': [
'cryptography <= 3.4',
]
}
def url(self, **kwargs):
# Support URL
return ''
def send(self, **kwargs):
# Pretend everything is okay (so we don't break other tests)
return True
SCHEMA_MAP['req02'] = TestReq02Notification
# This is a made up class that is just used to verify
class TestReq03Notification(NotifyBase):
"""
This class is used to test various requirement configurations
"""
# Set some requirements
requirements = {
# We can over-ride the default details assigned to our plugin if
# specified
'details': _('some specified requirement details'),
# We can set a string value as well (it does not have to be a list)
'packages_recommended': 'cryptography <= 3.4'
}
def url(self, **kwargs):
# Support URL
return ''
def send(self, **kwargs):
# Pretend everything is okay (so we don't break other tests)
return True
SCHEMA_MAP['req03'] = TestReq03Notification
class TestDisabled01Notification(NotifyBase):
"""
This class is used to test a pre-disabled state
"""
# Just flat out disable our service
enabled = False
# we'll use this as a key to make our service easier to find
# in the next part of the testing
service_name = 'na01'
def url(self, **kwargs):
# Support URL
return ''
def notify(self, **kwargs):
# Pretend everything is okay (so we don't break other tests)
return True
SCHEMA_MAP['na01'] = TestDisabled01Notification
class TestDisabled02Notification(NotifyBase):
"""
This class is used to test a post-disabled state
"""
# we'll use this as a key to make our service easier to find
# in the next part of the testing
service_name = 'na02'
def __init__(self, *args, **kwargs):
super(TestDisabled02Notification, self).__init__(**kwargs)
# enable state changes **AFTER** we initialize
self.enabled = False
def url(self, **kwargs):
# Support URL
return ''
def notify(self, **kwargs):
# Pretend everything is okay (so we don't break other tests)
return True
SCHEMA_MAP['na02'] = TestDisabled02Notification
# We'll add a good notification to our list
class TesEnabled01Notification(NotifyBase):
"""
This class is just a simple enabled one
"""
# we'll use this as a key to make our service easier to find
# in the next part of the testing
service_name = 'good'
def url(self, **kwargs):
# Support URL
return ''
def send(self, **kwargs):
# Pretend everything is okay (so we don't break other tests)
return True
SCHEMA_MAP['good'] = TesEnabled01Notification
# Verify that we can pass through all of our different details
result = runner.invoke(cli.main, [
'--details',
])
assert result.exit_code == 0
result = runner.invoke(cli.main, [
'-l',
])
assert result.exit_code == 0
# Reset our matrix
__reset_matrix()
__load_matrix()
@mock.patch('platform.system')
def test_apprise_cli_windows_env(mock_system):
"""

View File

@ -43,7 +43,7 @@ TEST_VAR_DIR = os.path.join(os.path.dirname(__file__), 'var')
@mock.patch('requests.post')
def test_notify_json_plugin_attachments(mock_post):
"""
API: NotifyJSON() Attachments
NotifyJSON() Attachments
"""
# Disable Throttling to speed testing

View File

@ -43,7 +43,7 @@ TEST_VAR_DIR = os.path.join(os.path.dirname(__file__), 'var')
@mock.patch('requests.post')
def test_notify_xml_plugin_attachments(mock_post):
"""
API: NotifyXML() Attachments
NotifyXML() Attachments
"""
# Disable Throttling to speed testing

View File

@ -0,0 +1,166 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2021 Chris Caron <lead2gold@gmail.com>
# All rights reserved.
#
# This code is licensed under the MIT License.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files(the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions :
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
from helpers import AppriseURLTester
from apprise import plugins
import requests
# Disable logging for a cleaner testing output
import logging
logging.disable(logging.CRITICAL)
# Our Testing URLs
apprise_url_tests = (
('apprise://', {
# invalid url (not complete)
'instance': None,
}),
# A a bad url
('apprise://:@/', {
'instance': None,
}),
# No token specified
('apprise://localhost', {
'instance': TypeError,
}),
# invalid token
('apprise://localhost/!', {
'instance': TypeError,
}),
# No token specified (whitespace is trimmed)
('apprise://localhost/%%20', {
'instance': TypeError,
}),
# A valid URL with Token
('apprise://localhost/%s' % ('a' * 32), {
'instance': plugins.NotifyAppriseAPI,
# Our expected url(privacy=True) startswith() response:
'privacy_url': 'apprise://localhost/a...a/',
}),
# A valid URL with Token (using port)
('apprise://localhost:8080/%s' % ('b' * 32), {
'instance': plugins.NotifyAppriseAPI,
# Our expected url(privacy=True) startswith() response:
'privacy_url': 'apprise://localhost:8080/b...b/',
}),
# A secure (https://) reference
('apprises://localhost/%s' % ('c' * 32), {
'instance': plugins.NotifyAppriseAPI,
# Our expected url(privacy=True) startswith() response:
'privacy_url': 'apprises://localhost/c...c/',
}),
# Native URL suport (https)
('https://example.com/path/notify/%s' % ('d' * 32), {
'instance': plugins.NotifyAppriseAPI,
# Our expected url(privacy=True) startswith() response:
'privacy_url': 'apprises://example.com/path/d...d/',
}),
# Native URL suport (http)
('http://example.com/notify/%s' % ('d' * 32), {
'instance': plugins.NotifyAppriseAPI,
# Our expected url(privacy=True) startswith() response:
'privacy_url': 'apprise://example.com/d...d/',
}),
# support to= keyword
('apprises://localhost/?to=%s' % ('e' * 32), {
'instance': plugins.NotifyAppriseAPI,
'privacy_url': 'apprises://localhost/e...e/',
}),
# support token= keyword (even when passed with to=, token over-rides)
('apprise://localhost/?token=%s&to=%s' % ('f' * 32, 'abcd'), {
'instance': plugins.NotifyAppriseAPI,
'privacy_url': 'apprise://localhost/f...f/',
}),
# Test tags
('apprise://localhost/?token=%s&tags=admin,team' % ('abcd'), {
'instance': plugins.NotifyAppriseAPI,
'privacy_url': 'apprise://localhost/a...d/',
}),
# Test Format string
('apprise://user@localhost/mytoken0/?format=markdown', {
'instance': plugins.NotifyAppriseAPI,
'privacy_url': 'apprise://user@localhost/m...0/',
}),
('apprise://user@localhost/mytoken1/', {
'instance': plugins.NotifyAppriseAPI,
'privacy_url': 'apprise://user@localhost/m...1/',
}),
('apprise://localhost:8080/mytoken/', {
'instance': plugins.NotifyAppriseAPI,
}),
('apprise://user:pass@localhost:8080/mytoken2/', {
'instance': plugins.NotifyAppriseAPI,
'privacy_url': 'apprise://user:****@localhost:8080/m...2/',
}),
('apprises://localhost/mytoken/', {
'instance': plugins.NotifyAppriseAPI,
}),
('apprises://user:pass@localhost/mytoken3/', {
'instance': plugins.NotifyAppriseAPI,
# Our expected url(privacy=True) startswith() response:
'privacy_url': 'apprises://user:****@localhost/m...3/',
}),
('apprises://localhost:8080/mytoken4/', {
'instance': plugins.NotifyAppriseAPI,
# Our expected url(privacy=True) startswith() response:
'privacy_url': 'apprises://localhost:8080/m...4/',
}),
('apprises://user:password@localhost:8080/mytoken5/', {
'instance': plugins.NotifyAppriseAPI,
# Our expected url(privacy=True) startswith() response:
'privacy_url': 'apprises://user:****@localhost:8080/m...5/',
}),
('apprises://localhost:8080/path?-HeaderKey=HeaderValue', {
'instance': plugins.NotifyAppriseAPI,
}),
('apprise://localhost/%s' % ('a' * 32), {
'instance': plugins.NotifyAppriseAPI,
# force a failure
'response': False,
'requests_response_code': requests.codes.internal_server_error,
}),
('apprise://localhost/%s' % ('a' * 32), {
'instance': plugins.NotifyAppriseAPI,
# throw a bizzare code forcing us to fail to look it up
'response': False,
'requests_response_code': 999,
}),
('apprise://localhost/%s' % ('a' * 32), {
'instance': plugins.NotifyAppriseAPI,
# Throws a series of connection and transfer exceptions when this flag
# is set and tests that we gracfully handle them
'test_requests_exceptions': True,
}),
)
def test_plugin_apprise_urls():
"""
NotifyAppriseAPI() General Checks
"""
# Run our general tests
AppriseURLTester(tests=apprise_url_tests).run_all()

168
test/test_plugin_boxcar.py Normal file
View File

@ -0,0 +1,168 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2021 Chris Caron <lead2gold@gmail.com>
# All rights reserved.
#
# This code is licensed under the MIT License.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files(the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions :
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
import pytest
import mock
from helpers import AppriseURLTester
from apprise import plugins
from apprise import NotifyType
import requests
# Disable logging for a cleaner testing output
import logging
logging.disable(logging.CRITICAL)
# Our Testing URLs
apprise_url_tests = (
('boxcar://', {
# invalid secret key
'instance': TypeError,
}),
# A a bad url
('boxcar://:@/', {
'instance': TypeError,
}),
# No secret specified
('boxcar://%s' % ('a' * 64), {
'instance': TypeError,
}),
# No access specified (whitespace is trimmed)
('boxcar://%%20/%s' % ('a' * 64), {
'instance': TypeError,
}),
# No secret specified (whitespace is trimmed)
('boxcar://%s/%%20' % ('a' * 64), {
'instance': TypeError,
}),
# Provide both an access and a secret
('boxcar://%s/%s' % ('a' * 64, 'b' * 64), {
'instance': plugins.NotifyBoxcar,
'requests_response_code': requests.codes.created,
# Our expected url(privacy=True) startswith() response:
'privacy_url': 'boxcar://a...a/****/',
}),
# Test without image set
('boxcar://%s/%s?image=True' % ('a' * 64, 'b' * 64), {
'instance': plugins.NotifyBoxcar,
'requests_response_code': requests.codes.created,
# don't include an image in Asset by default
'include_image': False,
}),
('boxcar://%s/%s?image=False' % ('a' * 64, 'b' * 64), {
'instance': plugins.NotifyBoxcar,
'requests_response_code': requests.codes.created,
}),
# our access, secret and device are all 64 characters
# which is what we're doing here
('boxcar://%s/%s/@tag1/tag2///%s/?to=tag3' % (
'a' * 64, 'b' * 64, 'd' * 64), {
'instance': plugins.NotifyBoxcar,
'requests_response_code': requests.codes.created,
}),
# An invalid tag
('boxcar://%s/%s/@%s' % ('a' * 64, 'b' * 64, 't' * 64), {
'instance': plugins.NotifyBoxcar,
'requests_response_code': requests.codes.created,
}),
('boxcar://%s/%s/' % ('a' * 64, 'b' * 64), {
'instance': plugins.NotifyBoxcar,
# force a failure
'response': False,
'requests_response_code': requests.codes.internal_server_error,
}),
('boxcar://%s/%s/' % ('a' * 64, 'b' * 64), {
'instance': plugins.NotifyBoxcar,
# throw a bizzare code forcing us to fail to look it up
'response': False,
'requests_response_code': 999,
}),
('boxcar://%s/%s/' % ('a' * 64, 'b' * 64), {
'instance': plugins.NotifyBoxcar,
# Throws a series of connection and transfer exceptions when this flag
# is set and tests that we gracfully handle them
'test_requests_exceptions': True,
}),
)
def test_plugin_boxcar_urls():
"""
NotifyBoxcar() Apprise URLs
"""
# Run our general tests
AppriseURLTester(tests=apprise_url_tests).run_all()
@mock.patch('requests.get')
@mock.patch('requests.post')
def test_plugin_boxcar_edge_cases(mock_post, mock_get):
"""
NotifyBoxcar() Edge Cases
"""
# Disable Throttling to speed testing
plugins.NotifyBase.request_rate_per_sec = 0
# Generate some generic message types
device = 'A' * 64
tag = '@B' * 63
access = '-' * 64
secret = '_' * 64
# Initializes the plugin with recipients set to None
plugins.NotifyBoxcar(access=access, secret=secret, targets=None)
# Initializes the plugin with a valid access, but invalid access key
with pytest.raises(TypeError):
plugins.NotifyBoxcar(access=None, secret=secret, targets=None)
# Initializes the plugin with a valid access, but invalid secret
with pytest.raises(TypeError):
plugins.NotifyBoxcar(access=access, secret=None, targets=None)
# Initializes the plugin with recipients list
# the below also tests our the variation of recipient types
plugins.NotifyBoxcar(
access=access, secret=secret, targets=[device, tag])
mock_get.return_value = requests.Request()
mock_post.return_value = requests.Request()
mock_post.return_value.status_code = requests.codes.created
mock_get.return_value.status_code = requests.codes.created
# Test notifications without a body or a title
p = plugins.NotifyBoxcar(access=access, secret=secret, targets=None)
assert p.notify(body=None, title=None, notify_type=NotifyType.INFO) is True
# Test comma, separate values
device = 'a' * 64
p = plugins.NotifyBoxcar(
access=access, secret=secret,
targets=','.join([device, device, device]))
assert len(p.device_tokens) == 3

View File

@ -0,0 +1,83 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2021 Chris Caron <lead2gold@gmail.com>
# All rights reserved.
#
# This code is licensed under the MIT License.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files(the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions :
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
from apprise import plugins
from helpers import AppriseURLTester
# Disable logging for a cleaner testing output
import logging
logging.disable(logging.CRITICAL)
# Our Testing URLs
apprise_url_tests = (
('clicksend://', {
# We failed to identify any valid authentication
'instance': TypeError,
}),
('clicksend://:@/', {
# We failed to identify any valid authentication
'instance': TypeError,
}),
('clicksend://user:pass@{}/{}/{}'.format('1' * 10, '2' * 15, 'a' * 13), {
# invalid target numbers; we'll fail to notify anyone
'instance': plugins.NotifyClickSend,
'notify_response': False,
}),
('clicksend://user:pass@{}?batch=yes'.format('3' * 14), {
# valid number
'instance': plugins.NotifyClickSend,
}),
('clicksend://user:pass@{}?batch=yes&to={}'.format('3' * 14, '6' * 14), {
# valid number but using the to= variable
'instance': plugins.NotifyClickSend,
# Our expected url(privacy=True) startswith() response:
'privacy_url': 'clicksend://user:****',
}),
('clicksend://user:pass@{}?batch=no'.format('3' * 14), {
# valid number - no batch
'instance': plugins.NotifyClickSend,
}),
('clicksend://user:pass@{}'.format('3' * 14), {
'instance': plugins.NotifyClickSend,
# throw a bizzare code forcing us to fail to look it up
'response': False,
'requests_response_code': 999,
}),
('clicksend://user:pass@{}'.format('3' * 14), {
'instance': plugins.NotifyClickSend,
# Throws a series of connection and transfer exceptions when this flag
# is set and tests that we gracfully handle them
'test_requests_exceptions': True,
}),
)
def test_plugin_clicksend_urls():
"""
NotifyClickSend() Apprise URLs
"""
# Run our general tests
AppriseURLTester(tests=apprise_url_tests).run_all()

View File

@ -0,0 +1,110 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2021 Chris Caron <lead2gold@gmail.com>
# All rights reserved.
#
# This code is licensed under the MIT License.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files(the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions :
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
import requests
from apprise import plugins
from helpers import AppriseURLTester
# Disable logging for a cleaner testing output
import logging
logging.disable(logging.CRITICAL)
# Our Testing URLs
apprise_url_tests = (
('json://:@/', {
'instance': None,
}),
('json://', {
'instance': None,
}),
('jsons://', {
'instance': None,
}),
('json://localhost', {
'instance': plugins.NotifyJSON,
}),
('json://user:pass@localhost', {
'instance': plugins.NotifyJSON,
# Our expected url(privacy=True) startswith() response:
'privacy_url': 'json://user:****@localhost',
}),
('json://user@localhost', {
'instance': plugins.NotifyJSON,
}),
('json://localhost:8080', {
'instance': plugins.NotifyJSON,
}),
('json://user:pass@localhost:8080', {
'instance': plugins.NotifyJSON,
}),
('jsons://localhost', {
'instance': plugins.NotifyJSON,
}),
('jsons://user:pass@localhost', {
'instance': plugins.NotifyJSON,
}),
('jsons://localhost:8080/path/', {
'instance': plugins.NotifyJSON,
# Our expected url(privacy=True) startswith() response:
'privacy_url': 'jsons://localhost:8080/path/',
}),
('jsons://user:password@localhost:8080', {
'instance': plugins.NotifyJSON,
# Our expected url(privacy=True) startswith() response:
'privacy_url': 'jsons://user:****@localhost:8080',
}),
('json://localhost:8080/path?-HeaderKey=HeaderValue', {
'instance': plugins.NotifyJSON,
}),
('json://user:pass@localhost:8081', {
'instance': plugins.NotifyJSON,
# force a failure
'response': False,
'requests_response_code': requests.codes.internal_server_error,
}),
('json://user:pass@localhost:8082', {
'instance': plugins.NotifyJSON,
# throw a bizzare code forcing us to fail to look it up
'response': False,
'requests_response_code': 999,
}),
('json://user:pass@localhost:8083', {
'instance': plugins.NotifyJSON,
# Throws a series of connection and transfer exceptions when this flag
# is set and tests that we gracfully handle them
'test_requests_exceptions': True,
}),
)
def test_plugin_custom_json_urls():
"""
NotifyJSON() Apprise URLs
"""
# Run our general tests
AppriseURLTester(tests=apprise_url_tests).run_all()

View File

@ -0,0 +1,110 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2021 Chris Caron <lead2gold@gmail.com>
# All rights reserved.
#
# This code is licensed under the MIT License.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files(the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions :
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
import requests
from apprise import plugins
from helpers import AppriseURLTester
# Disable logging for a cleaner testing output
import logging
logging.disable(logging.CRITICAL)
# Our Testing URLs
apprise_url_tests = (
('xml://:@/', {
'instance': None,
}),
('xml://', {
'instance': None,
}),
('xmls://', {
'instance': None,
}),
('xml://localhost', {
'instance': plugins.NotifyXML,
}),
('xml://user@localhost', {
'instance': plugins.NotifyXML,
}),
('xml://user:pass@localhost', {
'instance': plugins.NotifyXML,
# Our expected url(privacy=True) startswith() response:
'privacy_url': 'xml://user:****@localhost',
}),
('xml://localhost:8080', {
'instance': plugins.NotifyXML,
}),
('xml://user:pass@localhost:8080', {
'instance': plugins.NotifyXML,
}),
('xmls://localhost', {
'instance': plugins.NotifyXML,
}),
('xmls://user:pass@localhost', {
'instance': plugins.NotifyXML,
# Our expected url(privacy=True) startswith() response:
'privacy_url': 'xmls://user:****@localhost',
}),
('xmls://localhost:8080/path/', {
'instance': plugins.NotifyXML,
# Our expected url(privacy=True) startswith() response:
'privacy_url': 'xmls://localhost:8080/path/',
}),
('xmls://user:pass@localhost:8080', {
'instance': plugins.NotifyXML,
}),
('xml://localhost:8080/path?-HeaderKey=HeaderValue', {
'instance': plugins.NotifyXML,
}),
('xml://user:pass@localhost:8081', {
'instance': plugins.NotifyXML,
# force a failure
'response': False,
'requests_response_code': requests.codes.internal_server_error,
}),
('xml://user:pass@localhost:8082', {
'instance': plugins.NotifyXML,
# throw a bizzare code forcing us to fail to look it up
'response': False,
'requests_response_code': 999,
}),
('xml://user:pass@localhost:8083', {
'instance': plugins.NotifyXML,
# Throws a series of connection and transfer exceptions when this flag
# is set and tests that we gracfully handle them
'test_requests_exceptions': True,
}),
)
def test_plugin_custom_xml_urls():
"""
NotifyXML() Apprise URLs
"""
# Run our general tests
AppriseURLTester(tests=apprise_url_tests).run_all()

View File

@ -0,0 +1,117 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2021 Chris Caron <lead2gold@gmail.com>
# All rights reserved.
#
# This code is licensed under the MIT License.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files(the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions :
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
from apprise import plugins
from helpers import AppriseURLTester
# Disable logging for a cleaner testing output
import logging
logging.disable(logging.CRITICAL)
# Our Testing URLs
apprise_url_tests = (
('d7sms://', {
# We failed to identify any valid authentication
'instance': TypeError,
}),
('d7sms://:@/', {
# We failed to identify any valid authentication
'instance': TypeError,
}),
('d7sms://user:pass@{}/{}/{}'.format('1' * 10, '2' * 15, 'a' * 13), {
# No valid targets to notify
'instance': plugins.NotifyD7Networks,
# Since there are no targets specified we expect a False return on
# send()
'notify_response': False,
}),
('d7sms://user:pass@{}?batch=yes'.format('3' * 14), {
# valid number
'instance': plugins.NotifyD7Networks,
# Our expected url(privacy=True) startswith() response:
'privacy_url': 'd7sms://user:****@',
}),
('d7sms://user:pass@{}?batch=yes'.format('7' * 14), {
# valid number
'instance': plugins.NotifyD7Networks,
# Test what happens if a batch send fails to return a messageCount
'requests_response_text': {
'data': {
'messageCount': 0,
},
},
# Expected notify() response
'notify_response': False,
}),
('d7sms://user:pass@{}?batch=yes&to={}'.format('3' * 14, '6' * 14), {
# valid number
'instance': plugins.NotifyD7Networks,
}),
('d7sms://user:pass@{}?batch=yes&from=apprise'.format('3' * 14), {
# valid number, utilizing the optional from= variable
'instance': plugins.NotifyD7Networks,
}),
('d7sms://user:pass@{}?batch=yes&source=apprise'.format('3' * 14), {
# valid number, utilizing the optional source= variable (same as from)
'instance': plugins.NotifyD7Networks,
}),
('d7sms://user:pass@{}?priority=invalid'.format('3' * 14), {
# valid number; invalid priority
'instance': plugins.NotifyD7Networks,
}),
('d7sms://user:pass@{}?priority=3'.format('3' * 14), {
# valid number; adjusted priority
'instance': plugins.NotifyD7Networks,
}),
('d7sms://user:pass@{}?priority=high'.format('3' * 14), {
# valid number; adjusted priority (string supported)
'instance': plugins.NotifyD7Networks,
}),
('d7sms://user:pass@{}?batch=no'.format('3' * 14), {
# valid number - no batch
'instance': plugins.NotifyD7Networks,
}),
('d7sms://user:pass@{}'.format('3' * 14), {
'instance': plugins.NotifyD7Networks,
# throw a bizzare code forcing us to fail to look it up
'response': False,
'requests_response_code': 999,
}),
('d7sms://user:pass@{}'.format('3' * 14), {
'instance': plugins.NotifyD7Networks,
# Throws a series of connection and transfer exceptions when this flag
# is set and tests that we gracfully handle them
'test_requests_exceptions': True,
}),
)
def test_plugin_d7networks_urls():
"""
NotifyD7Networks() Apprise URLs
"""
# Run our general tests
AppriseURLTester(tests=apprise_url_tests).run_all()

View File

@ -0,0 +1,105 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2021 Chris Caron <lead2gold@gmail.com>
# All rights reserved.
#
# This code is licensed under the MIT License.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files(the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions :
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
from apprise import plugins
from helpers import AppriseURLTester
# Disable logging for a cleaner testing output
import logging
logging.disable(logging.CRITICAL)
# Our Testing URLs
apprise_url_tests = (
('dingtalk://', {
# No Access Token specified
'instance': TypeError,
}),
('dingtalk://a_bd_/', {
# invalid Access Token
'instance': TypeError,
}),
('dingtalk://12345678', {
# access token
'instance': plugins.NotifyDingTalk,
# Our expected url(privacy=True) startswith() response:
'privacy_url': 'dingtalk://1...8',
}),
('dingtalk://{}/{}'.format('a' * 8, '1' * 14), {
# access token + phone number
'instance': plugins.NotifyDingTalk,
}),
('dingtalk://{}/{}/invalid'.format('a' * 8, '1' * 3), {
# access token + 2 invalid phone numbers
'instance': plugins.NotifyDingTalk,
}),
('dingtalk://{}/?to={}'.format('a' * 8, '1' * 14), {
# access token + phone number using 'to'
'instance': plugins.NotifyDingTalk,
}),
# Test secret via user@
('dingtalk://secret@{}/?to={}'.format('a' * 8, '1' * 14), {
# access token + phone number using 'to'
'instance': plugins.NotifyDingTalk,
# Our expected url(privacy=True) startswith() response:
'privacy_url': 'dingtalk://****@a...a',
}),
# Test secret via secret= and token=
('dingtalk://?token={}&to={}&secret={}'.format(
'b' * 8, '1' * 14, 'a' * 15), {
# access token + phone number using 'to'
'instance': plugins.NotifyDingTalk,
'privacy_url': 'dingtalk://****@b...b',
}),
# Invalid secret
('dingtalk://{}/?to={}&secret=_'.format('a' * 8, '1' * 14), {
'instance': TypeError,
}),
('dingtalk://{}?format=markdown'.format('a' * 8), {
# access token
'instance': plugins.NotifyDingTalk,
}),
('dingtalk://{}'.format('a' * 8), {
'instance': plugins.NotifyDingTalk,
# throw a bizzare code forcing us to fail to look it up
'response': False,
'requests_response_code': 999,
}),
('dingtalk://{}'.format('a' * 8), {
'instance': plugins.NotifyDingTalk,
# Throws a series of connection and transfer exceptions when this flag
# is set and tests that we gracfully handle them
'test_requests_exceptions': True,
}),
)
def test_plugin_dingtalk_urls():
"""
NotifyDingTalk() Apprise URLs
"""
# Run our general tests
AppriseURLTester(tests=apprise_url_tests).run_all()

View File

@ -28,6 +28,7 @@ import six
import mock
import pytest
import requests
from helpers import AppriseURLTester
from apprise import Apprise
from apprise import AppriseAttachment
from apprise import plugins
@ -41,11 +42,129 @@ logging.disable(logging.CRITICAL)
# Attachment Directory
TEST_VAR_DIR = os.path.join(os.path.dirname(__file__), 'var')
# Our Testing URLs
apprise_url_tests = (
('discord://', {
'instance': TypeError,
}),
# An invalid url
('discord://:@/', {
'instance': TypeError,
}),
# No webhook_token specified
('discord://%s' % ('i' * 24), {
'instance': TypeError,
}),
# Provide both an webhook id and a webhook token
('discord://%s/%s' % ('i' * 24, 't' * 64), {
'instance': plugins.NotifyDiscord,
'requests_response_code': requests.codes.no_content,
}),
# Provide a temporary username
('discord://l2g@%s/%s' % ('i' * 24, 't' * 64), {
'instance': plugins.NotifyDiscord,
'requests_response_code': requests.codes.no_content,
}),
# test image= field
('discord://%s/%s?format=markdown&footer=Yes&image=Yes' % (
'i' * 24, 't' * 64), {
'instance': plugins.NotifyDiscord,
'requests_response_code': requests.codes.no_content,
# don't include an image by default
'include_image': False,
}),
('discord://%s/%s?format=markdown&footer=Yes&image=No&fields=no' % (
'i' * 24, 't' * 64), {
'instance': plugins.NotifyDiscord,
'requests_response_code': requests.codes.no_content,
}),
('discord://%s/%s?format=markdown&footer=Yes&image=Yes' % (
'i' * 24, 't' * 64), {
'instance': plugins.NotifyDiscord,
'requests_response_code': requests.codes.no_content,
}),
('https://discord.com/api/webhooks/{}/{}'.format(
'0' * 10, 'B' * 40), {
# Native URL Support, support the provided discord URL from their
# webpage.
'instance': plugins.NotifyDiscord,
'requests_response_code': requests.codes.no_content,
}),
('https://discordapp.com/api/webhooks/{}/{}'.format(
'0' * 10, 'B' * 40), {
# Legacy Native URL Support, support the older URL (to be
# decomissioned on Nov 7th 2020)
'instance': plugins.NotifyDiscord,
'requests_response_code': requests.codes.no_content,
}),
('https://discordapp.com/api/webhooks/{}/{}?footer=yes'.format(
'0' * 10, 'B' * 40), {
# Native URL Support with arguments
'instance': plugins.NotifyDiscord,
'requests_response_code': requests.codes.no_content,
}),
('discord://%s/%s?format=markdown&avatar=No&footer=No' % (
'i' * 24, 't' * 64), {
'instance': plugins.NotifyDiscord,
'requests_response_code': requests.codes.no_content,
}),
# different format support
('discord://%s/%s?format=markdown' % ('i' * 24, 't' * 64), {
'instance': plugins.NotifyDiscord,
'requests_response_code': requests.codes.no_content,
}),
('discord://%s/%s?format=text' % ('i' * 24, 't' * 64), {
'instance': plugins.NotifyDiscord,
'requests_response_code': requests.codes.no_content,
}),
# Test with avatar URL
('discord://%s/%s?avatar_url=http://localhost/test.jpg' % (
'i' * 24, 't' * 64), {
'instance': plugins.NotifyDiscord,
'requests_response_code': requests.codes.no_content,
}),
# Test without image set
('discord://%s/%s' % ('i' * 24, 't' * 64), {
'instance': plugins.NotifyDiscord,
'requests_response_code': requests.codes.no_content,
# don't include an image by default
'include_image': False,
}),
('discord://%s/%s/' % ('a' * 24, 'b' * 64), {
'instance': plugins.NotifyDiscord,
# force a failure
'response': False,
'requests_response_code': requests.codes.internal_server_error,
}),
('discord://%s/%s/' % ('a' * 24, 'b' * 64), {
'instance': plugins.NotifyDiscord,
# throw a bizzare code forcing us to fail to look it up
'response': False,
'requests_response_code': 999,
}),
('discord://%s/%s/' % ('a' * 24, 'b' * 64), {
'instance': plugins.NotifyDiscord,
# Throws a series of connection and transfer exceptions when this flag
# is set and tests that we gracfully handle them
'test_requests_exceptions': True,
}),
)
def test_plugin_discord_urls():
"""
NotifyDiscord() Apprise URLs
"""
# Run our general tests
AppriseURLTester(tests=apprise_url_tests).run_all()
@mock.patch('requests.post')
def test_discord_plugin(mock_post):
def test_plugin_discord_general(mock_post):
"""
API: NotifyDiscord() General Checks
NotifyDiscord() General Checks
"""
# Disable Throttling to speed testing
@ -239,9 +358,9 @@ def test_discord_plugin(mock_post):
@mock.patch('requests.post')
def test_discord_attachments(mock_post):
def test_plugin_discord_attachments(mock_post):
"""
API: NotifyDiscord() Attachment Checks
NotifyDiscord() Attachment Checks
"""
# Disable Throttling to speed testing

View File

@ -243,9 +243,9 @@ TEST_URLS = (
@mock.patch('smtplib.SMTP')
@mock.patch('smtplib.SMTP_SSL')
def test_email_plugin(mock_smtp, mock_smtpssl):
def test_plugin_email(mock_smtp, mock_smtpssl):
"""
API: NotifyEmail Plugin()
NotifyEmail() General Checks
"""
# Disable Throttling to speed testing
@ -403,9 +403,9 @@ def test_email_plugin(mock_smtp, mock_smtpssl):
@mock.patch('smtplib.SMTP')
@mock.patch('smtplib.SMTP_SSL')
def test_webbase_lookup(mock_smtp, mock_smtpssl):
def test_plugin_email_webbase_lookup(mock_smtp, mock_smtpssl):
"""
API: Web Based Lookup Tests
NotifyEmail() Web Based Lookup Tests
"""
@ -445,9 +445,9 @@ def test_webbase_lookup(mock_smtp, mock_smtpssl):
@mock.patch('smtplib.SMTP')
def test_smtplib_init_fail(mock_smtplib):
def test_plugin_email_smtplib_init_fail(mock_smtplib):
"""
API: Test exception handling when calling smtplib.SMTP()
NotifyEmail() Test exception handling when calling smtplib.SMTP()
"""
# Disable Throttling to speed testing
@ -470,9 +470,9 @@ def test_smtplib_init_fail(mock_smtplib):
@mock.patch('smtplib.SMTP')
def test_smtplib_send_okay(mock_smtplib):
def test_plugin_email_smtplib_send_okay(mock_smtplib):
"""
API: Test a successfully sent email
NotifyEmail() Test a successfully sent email
"""
# Disable Throttling to speed testing
@ -538,9 +538,9 @@ def test_smtplib_send_okay(mock_smtplib):
@mock.patch('smtplib.SMTP')
def test_smtplib_internationalization(mock_smtp):
def test_plugin_email_smtplib_internationalization(mock_smtp):
"""
API: Test email handling using internationalization
NotifyEmail() Internationalization Handling
"""
# Disable Throttling to speed testing
@ -610,9 +610,9 @@ def test_smtplib_internationalization(mock_smtp):
notify_type=NotifyType.INFO) is True
def test_email_url_escaping():
def test_plugin_email_url_escaping():
"""
API: Test that user/passwords are properly escaped from URL
NotifyEmail() Test that user/passwords are properly escaped from URL
"""
# quote(' %20')
@ -641,9 +641,9 @@ def test_email_url_escaping():
assert obj.password == ' %20'
def test_email_url_variations():
def test_plugin_email_url_variations():
"""
API: Test email variations to ensure parsing is correct
NotifyEmail() Test URL variations to ensure parsing is correct
"""
# Test variations of username required to be an email address
@ -727,9 +727,9 @@ def test_email_url_variations():
assert obj.from_addr == 'from@example.jp'
def test_email_dict_variations():
def test_plugin_email_dict_variations():
"""
API: Test email dictionary variations to ensure parsing is correct
NotifyEmail() Test email dictionary variations to ensure parsing is correct
"""
# Test variations of username required to be an email address

449
test/test_plugin_emby.py Normal file
View File

@ -0,0 +1,449 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2021 Chris Caron <lead2gold@gmail.com>
# All rights reserved.
#
# This code is licensed under the MIT License.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files(the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions :
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
import mock
from json import dumps
from apprise import Apprise
from apprise import plugins
import requests
from helpers import AppriseURLTester
# Disable logging for a cleaner testing output
import logging
logging.disable(logging.CRITICAL)
# Our Testing URLs
apprise_url_tests = (
# Insecure Request; no hostname specified
('emby://', {
'instance': None,
}),
# Secure Emby Request; no hostname specified
('embys://', {
'instance': None,
}),
# No user specified
('emby://localhost', {
# Missing a username
'instance': TypeError,
}),
('emby://:@/', {
'instance': None,
}),
# Valid Authentication
('emby://l2g@localhost', {
'instance': plugins.NotifyEmby,
# our response will be False because our authentication can't be
# tested very well using this matrix.
'response': False,
}),
('embys://l2g:password@localhost', {
'instance': plugins.NotifyEmby,
# our response will be False because our authentication can't be
# tested very well using this matrix.
'response': False,
# Our expected url(privacy=True) startswith() response:
'privacy_url': 'embys://l2g:****@localhost',
}),
)
def test_plugin_template_urls():
"""
NotifyTemplate() Apprise URLs
"""
# Run our general tests
AppriseURLTester(tests=apprise_url_tests).run_all()
@mock.patch('apprise.plugins.NotifyEmby.sessions')
@mock.patch('apprise.plugins.NotifyEmby.login')
@mock.patch('apprise.plugins.NotifyEmby.logout')
@mock.patch('requests.get')
@mock.patch('requests.post')
def test_plugin_emby_general(mock_post, mock_get, mock_logout,
mock_login, mock_sessions):
"""
NotifyEmby General Tests
"""
# Disable Throttling to speed testing
plugins.NotifyBase.request_rate_per_sec = 0
req = requests.Request()
req.status_code = requests.codes.ok
req.content = ''
mock_get.return_value = req
mock_post.return_value = req
# This is done so we don't obstruct our access_token and user_id values
mock_login.return_value = True
mock_logout.return_value = True
mock_sessions.return_value = {'abcd': {}}
obj = Apprise.instantiate('emby://l2g:l2gpass@localhost?modal=False')
assert isinstance(obj, plugins.NotifyEmby)
assert obj.notify('title', 'body', 'info') is True
obj.access_token = 'abc'
obj.user_id = '123'
# Test Modal support
obj = Apprise.instantiate('emby://l2g:l2gpass@localhost?modal=True')
assert isinstance(obj, plugins.NotifyEmby)
assert obj.notify('title', 'body', 'info') is True
obj.access_token = 'abc'
obj.user_id = '123'
# Test our exception handling
for _exception in AppriseURLTester.req_exceptions:
mock_post.side_effect = _exception
mock_get.side_effect = _exception
# We'll fail to log in each time
assert obj.notify('title', 'body', 'info') is False
# Disable Exceptions
mock_post.side_effect = None
mock_get.side_effect = None
# Our login flat out fails if we don't have proper parseable content
mock_post.return_value.content = u''
mock_get.return_value.content = mock_post.return_value.content
# KeyError handling
mock_post.return_value.status_code = 999
mock_get.return_value.status_code = 999
assert obj.notify('title', 'body', 'info') is False
# General Internal Server Error
mock_post.return_value.status_code = requests.codes.internal_server_error
mock_get.return_value.status_code = requests.codes.internal_server_error
assert obj.notify('title', 'body', 'info') is False
mock_post.return_value.status_code = requests.codes.ok
mock_get.return_value.status_code = requests.codes.ok
mock_get.return_value.content = mock_post.return_value.content
# Disable the port completely
obj.port = None
assert obj.notify('title', 'body', 'info') is True
# An Empty return set (no query is made, but notification will still
# succeed
mock_sessions.return_value = {}
assert obj.notify('title', 'body', 'info') is True
# Tidy our object
del obj
@mock.patch('requests.get')
@mock.patch('requests.post')
def test_plugin_emby_login(mock_post, mock_get):
"""
NotifyEmby() login()
"""
# Disable Throttling to speed testing
plugins.NotifyBase.request_rate_per_sec = 0
# Prepare Mock
mock_get.return_value = requests.Request()
mock_post.return_value = requests.Request()
obj = Apprise.instantiate('emby://l2g:l2gpass@localhost')
assert isinstance(obj, plugins.NotifyEmby)
# Test our exception handling
for _exception in AppriseURLTester.req_exceptions:
mock_post.side_effect = _exception
mock_get.side_effect = _exception
# We'll fail to log in each time
assert obj.login() is False
# Disable Exceptions
mock_post.side_effect = None
mock_get.side_effect = None
# Our login flat out fails if we don't have proper parseable content
mock_post.return_value.content = u''
mock_get.return_value.content = mock_post.return_value.content
# KeyError handling
mock_post.return_value.status_code = 999
mock_get.return_value.status_code = 999
assert obj.login() is False
# General Internal Server Error
mock_post.return_value.status_code = requests.codes.internal_server_error
mock_get.return_value.status_code = requests.codes.internal_server_error
assert obj.login() is False
mock_post.return_value.status_code = requests.codes.ok
mock_get.return_value.status_code = requests.codes.ok
obj = Apprise.instantiate('emby://l2g:l2gpass@localhost:1234')
# Set a different port (outside of default)
assert isinstance(obj, plugins.NotifyEmby)
assert obj.port == 1234
# The login will fail because '' is not a parseable JSON response
assert obj.login() is False
# Disable the port completely
obj.port = None
assert obj.login() is False
# Default port assignments
obj = Apprise.instantiate('emby://l2g:l2gpass@localhost')
assert isinstance(obj, plugins.NotifyEmby)
assert obj.port == 8096
# The login will (still) fail because '' is not a parseable JSON response
assert obj.login() is False
# Our login flat out fails if we don't have proper parseable content
mock_post.return_value.content = dumps({
u'AccessToken': u'0000-0000-0000-0000',
})
mock_get.return_value.content = mock_post.return_value.content
obj = Apprise.instantiate('emby://l2g:l2gpass@localhost')
assert isinstance(obj, plugins.NotifyEmby)
# The login will fail because the 'User' or 'Id' field wasn't parsed
assert obj.login() is False
# Our text content (we intentionally reverse the 2 locations
# that store the same thing; we do this so we can test which
# one it defaults to if both are present
mock_post.return_value.content = dumps({
u'User': {
u'Id': u'abcd123',
},
u'Id': u'123abc',
u'AccessToken': u'0000-0000-0000-0000',
})
mock_get.return_value.content = mock_post.return_value.content
obj = Apprise.instantiate('emby://l2g:l2gpass@localhost')
assert isinstance(obj, plugins.NotifyEmby)
# Login
assert obj.login() is True
assert obj.user_id == '123abc'
assert obj.access_token == '0000-0000-0000-0000'
# We're going to log in a second time which checks that we logout
# first before logging in again. But this time we'll scrap the
# 'Id' area and use the one found in the User area if detected
mock_post.return_value.content = dumps({
u'User': {
u'Id': u'abcd123',
},
u'AccessToken': u'0000-0000-0000-0000',
})
mock_get.return_value.content = mock_post.return_value.content
# Login
assert obj.login() is True
assert obj.user_id == 'abcd123'
assert obj.access_token == '0000-0000-0000-0000'
@mock.patch('apprise.plugins.NotifyEmby.login')
@mock.patch('apprise.plugins.NotifyEmby.logout')
@mock.patch('requests.get')
@mock.patch('requests.post')
def test_plugin_emby_sessions(mock_post, mock_get, mock_logout, mock_login):
"""
NotifyEmby() sessions()
"""
# Disable Throttling to speed testing
plugins.NotifyBase.request_rate_per_sec = 0
# Prepare Mock
mock_get.return_value = requests.Request()
mock_post.return_value = requests.Request()
# This is done so we don't obstruct our access_token and user_id values
mock_login.return_value = True
mock_logout.return_value = True
obj = Apprise.instantiate('emby://l2g:l2gpass@localhost')
assert isinstance(obj, plugins.NotifyEmby)
obj.access_token = 'abc'
obj.user_id = '123'
# Test our exception handling
for _exception in AppriseURLTester.req_exceptions:
mock_post.side_effect = _exception
mock_get.side_effect = _exception
# We'll fail to log in each time
sessions = obj.sessions()
assert isinstance(sessions, dict) is True
assert len(sessions) == 0
# Disable Exceptions
mock_post.side_effect = None
mock_get.side_effect = None
# Our login flat out fails if we don't have proper parseable content
mock_post.return_value.content = u''
mock_get.return_value.content = mock_post.return_value.content
# KeyError handling
mock_post.return_value.status_code = 999
mock_get.return_value.status_code = 999
sessions = obj.sessions()
assert isinstance(sessions, dict) is True
assert len(sessions) == 0
# General Internal Server Error
mock_post.return_value.status_code = requests.codes.internal_server_error
mock_get.return_value.status_code = requests.codes.internal_server_error
sessions = obj.sessions()
assert isinstance(sessions, dict) is True
assert len(sessions) == 0
mock_post.return_value.status_code = requests.codes.ok
mock_get.return_value.status_code = requests.codes.ok
mock_get.return_value.content = mock_post.return_value.content
# Disable the port completely
obj.port = None
sessions = obj.sessions()
assert isinstance(sessions, dict) is True
assert len(sessions) == 0
# Let's get some results
mock_post.return_value.content = dumps([
{
u'Id': u'abc123',
},
{
u'Id': u'def456',
},
{
u'InvalidEntry': None,
},
])
mock_get.return_value.content = mock_post.return_value.content
sessions = obj.sessions(user_controlled=True)
assert isinstance(sessions, dict) is True
assert len(sessions) == 2
# Test it without setting user-controlled sessions
sessions = obj.sessions(user_controlled=False)
assert isinstance(sessions, dict) is True
assert len(sessions) == 2
# Triggers an authentication failure
obj.user_id = None
mock_login.return_value = False
sessions = obj.sessions()
assert isinstance(sessions, dict) is True
assert len(sessions) == 0
@mock.patch('apprise.plugins.NotifyEmby.login')
@mock.patch('requests.get')
@mock.patch('requests.post')
def test_plugin_emby_logout(mock_post, mock_get, mock_login):
"""
NotifyEmby() logout()
"""
# Disable Throttling to speed testing
plugins.NotifyBase.request_rate_per_sec = 0
# Prepare Mock
mock_get.return_value = requests.Request()
mock_post.return_value = requests.Request()
# This is done so we don't obstruct our access_token and user_id values
mock_login.return_value = True
obj = Apprise.instantiate('emby://l2g:l2gpass@localhost')
assert isinstance(obj, plugins.NotifyEmby)
obj.access_token = 'abc'
obj.user_id = '123'
# Test our exception handling
for _exception in AppriseURLTester.req_exceptions:
mock_post.side_effect = _exception
mock_get.side_effect = _exception
# We'll fail to log in each time
obj.logout()
obj.access_token = 'abc'
obj.user_id = '123'
# Disable Exceptions
mock_post.side_effect = None
mock_get.side_effect = None
# Our login flat out fails if we don't have proper parseable content
mock_post.return_value.content = u''
mock_get.return_value.content = mock_post.return_value.content
# KeyError handling
mock_post.return_value.status_code = 999
mock_get.return_value.status_code = 999
obj.logout()
obj.access_token = 'abc'
obj.user_id = '123'
# General Internal Server Error
mock_post.return_value.status_code = requests.codes.internal_server_error
mock_get.return_value.status_code = requests.codes.internal_server_error
obj.logout()
obj.access_token = 'abc'
obj.user_id = '123'
mock_post.return_value.status_code = requests.codes.ok
mock_get.return_value.status_code = requests.codes.ok
mock_get.return_value.content = mock_post.return_value.content
# Disable the port completely
obj.port = None
# Perform logout
obj.logout()
# Calling logout on an object already logged out
obj.logout()
# Test Python v3.5 LookupError Bug: https://bugs.python.org/issue29288
mock_post.side_effect = LookupError()
mock_get.side_effect = LookupError()
obj.access_token = 'abc'
obj.user_id = '123'
# Tidy object
del obj

190
test/test_plugin_enigma2.py Normal file
View File

@ -0,0 +1,190 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2021 Chris Caron <lead2gold@gmail.com>
# All rights reserved.
#
# This code is licensed under the MIT License.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files(the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions :
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
from apprise import plugins
import requests
from helpers import AppriseURLTester
# Disable logging for a cleaner testing output
import logging
logging.disable(logging.CRITICAL)
# Our Testing URLs
apprise_url_tests = (
('enigma2://:@/', {
'instance': None,
}),
('enigma2://', {
'instance': None,
}),
('enigma2s://', {
'instance': None,
}),
('enigma2://localhost', {
'instance': plugins.NotifyEnigma2,
# This will fail because we're also expecting a server acknowledgement
'notify_response': False,
}),
('enigma2://localhost', {
'instance': plugins.NotifyEnigma2,
# invalid JSON response
'requests_response_text': '{',
'notify_response': False,
}),
('enigma2://localhost', {
'instance': plugins.NotifyEnigma2,
# False is returned
'requests_response_text': {
'result': False
},
'notify_response': False,
}),
('enigma2://localhost', {
'instance': plugins.NotifyEnigma2,
# With the right content, this will succeed
'requests_response_text': {
'result': True
}
}),
('enigma2://user@localhost', {
'instance': plugins.NotifyEnigma2,
'requests_response_text': {
'result': True
}
}),
# Set timeout
('enigma2://user@localhost?timeout=-1', {
'instance': plugins.NotifyEnigma2,
'requests_response_text': {
'result': True
}
}),
# Set timeout
('enigma2://user@localhost?timeout=-1000', {
'instance': plugins.NotifyEnigma2,
'requests_response_text': {
'result': True
}
}),
# Set invalid timeout (defaults to a set value)
('enigma2://user@localhost?timeout=invalid', {
'instance': plugins.NotifyEnigma2,
'requests_response_text': {
'result': True
}
}),
('enigma2://user:pass@localhost', {
'instance': plugins.NotifyEnigma2,
'requests_response_text': {
'result': True
},
# Our expected url(privacy=True) startswith() response:
'privacy_url': 'enigma2://user:****@localhost',
}),
('enigma2://localhost:8080', {
'instance': plugins.NotifyEnigma2,
'requests_response_text': {
'result': True
},
}),
('enigma2://user:pass@localhost:8080', {
'instance': plugins.NotifyEnigma2,
'requests_response_text': {
'result': True
},
}),
('enigma2s://localhost', {
'instance': plugins.NotifyEnigma2,
'requests_response_text': {
'result': True
},
}),
('enigma2s://user:pass@localhost', {
'instance': plugins.NotifyEnigma2,
'requests_response_text': {
'result': True
},
# Our expected url(privacy=True) startswith() response:
'privacy_url': 'enigma2s://user:****@localhost',
}),
('enigma2s://localhost:8080/path/', {
'instance': plugins.NotifyEnigma2,
'requests_response_text': {
'result': True
},
# Our expected url(privacy=True) startswith() response:
'privacy_url': 'enigma2s://localhost:8080/path/',
}),
('enigma2s://user:pass@localhost:8080', {
'instance': plugins.NotifyEnigma2,
'requests_response_text': {
'result': True
},
}),
('enigma2://localhost:8080/path?-HeaderKey=HeaderValue', {
'instance': plugins.NotifyEnigma2,
'requests_response_text': {
'result': True
},
}),
('enigma2://user:pass@localhost:8081', {
'instance': plugins.NotifyEnigma2,
'requests_response_text': {
'result': True
},
# force a failure
'response': False,
'requests_response_code': requests.codes.internal_server_error,
}),
('enigma2://user:pass@localhost:8082', {
'instance': plugins.NotifyEnigma2,
'requests_response_text': {
'result': True
},
# throw a bizzare code forcing us to fail to look it up
'response': False,
'requests_response_code': 999,
}),
('enigma2://user:pass@localhost:8083', {
'instance': plugins.NotifyEnigma2,
'requests_response_text': {
'result': True
},
# Throws a series of connection and transfer exceptions when this flag
# is set and tests that we gracfully handle them
'test_requests_exceptions': True,
}),
)
def test_plugin_enigma2_urls():
"""
NotifyEnigma2() Apprise URLs
"""
# Run our general tests
AppriseURLTester(tests=apprise_url_tests).run_all()

81
test/test_plugin_faast.py Normal file
View File

@ -0,0 +1,81 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2021 Chris Caron <lead2gold@gmail.com>
# All rights reserved.
#
# This code is licensed under the MIT License.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files(the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions :
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
from apprise import plugins
import requests
from helpers import AppriseURLTester
# Disable logging for a cleaner testing output
import logging
logging.disable(logging.CRITICAL)
# Our Testing URLs
apprise_url_tests = (
('faast://', {
'instance': TypeError,
}),
('faast://:@/', {
'instance': TypeError,
}),
# Auth Token specified
('faast://%s' % ('a' * 32), {
'instance': plugins.NotifyFaast,
# Our expected url(privacy=True) startswith() response:
'privacy_url': 'faast://a...a',
}),
('faast://%s' % ('a' * 32), {
'instance': plugins.NotifyFaast,
# don't include an image by default
'include_image': False,
}),
('faast://%s' % ('a' * 32), {
'instance': plugins.NotifyFaast,
# force a failure
'response': False,
'requests_response_code': requests.codes.internal_server_error,
}),
('faast://%s' % ('a' * 32), {
'instance': plugins.NotifyFaast,
# throw a bizzare code forcing us to fail to look it up
'response': False,
'requests_response_code': 999,
}),
('faast://%s' % ('a' * 32), {
'instance': plugins.NotifyFaast,
# Throws a series of connection and transfer exceptions when this flag
# is set and tests that we gracfully handle them
'test_requests_exceptions': True,
}),
)
def test_plugin_faast_urls():
"""
NotifyFaast() Apprise URLs
"""
# Run our general tests
AppriseURLTester(tests=apprise_url_tests).run_all()

View File

@ -24,13 +24,22 @@
# THE SOFTWARE.
import io
import os
import sys
import mock
import pytest
import requests
import json
from apprise import Apprise
from apprise import plugins
from apprise.plugins.NotifyFCM.oauth import GoogleOAuth
from cryptography.exceptions import UnsupportedAlgorithm
from helpers import AppriseURLTester
try:
from apprise.plugins.NotifyFCM.oauth import GoogleOAuth
from cryptography.exceptions import UnsupportedAlgorithm
except ImportError:
# No problem; there is no cryptography support
pass
try:
from json.decoder import JSONDecodeError
@ -39,16 +48,140 @@ except ImportError:
# Python v2.7 Backwards Compatibility support
JSONDecodeError = ValueError
# Disable logging for a cleaner testing output
import logging
logging.disable(logging.CRITICAL)
# Test files for KeyFile Directory
PRIVATE_KEYFILE_DIR = os.path.join(os.path.dirname(__file__), 'var', 'fcm')
# Our Testing URLs
apprise_url_tests = (
('fcm://', {
# We failed to identify any valid authentication
'instance': TypeError,
}),
('fcm://:@/', {
# We failed to identify any valid authentication
'instance': TypeError,
}),
('fcm://project@%20%20/', {
# invalid apikey
'instance': TypeError,
}),
('fcm://apikey/', {
# no project id specified so we operate in legacy mode
'instance': plugins.NotifyFCM,
# but there are no targets specified so we return False
'notify_response': False,
}),
('fcm://apikey/device', {
# Valid device
'instance': plugins.NotifyFCM,
'privacy_url': 'fcm://a...y/device',
}),
('fcm://apikey/#topic', {
# Valid topic
'instance': plugins.NotifyFCM,
'privacy_url': 'fcm://a...y/%23topic',
}),
('fcm://apikey/device?mode=invalid', {
# Valid device, invalid mode
'instance': TypeError,
}),
('fcm://apikey/#topic1/device/%20/', {
# Valid topic, valid device, and invalid entry
'instance': plugins.NotifyFCM,
}),
('fcm://apikey?to=#topic1,device', {
# Test to=
'instance': plugins.NotifyFCM,
}),
('fcm://?apikey=abc123&to=device', {
# Test apikey= to=
'instance': plugins.NotifyFCM,
}),
('fcm://%20?to=device&keyfile=/invalid/path', {
# invalid Project ID
'instance': TypeError,
}),
('fcm://project_id?to=device&keyfile=/invalid/path', {
# Test to= and auto detection of oauth mode
'instance': plugins.NotifyFCM,
# we'll fail to send our notification as a result
'response': False,
}),
('fcm://?to=device&project=project_id&keyfile=/invalid/path', {
# Test project= & to= and auto detection of oauth mode
'instance': plugins.NotifyFCM,
# we'll fail to send our notification as a result
'response': False,
}),
('fcm://project_id?to=device&mode=oauth2', {
# no keyfile was specified
'instance': TypeError,
}),
('fcm://project_id?to=device&mode=oauth2&keyfile=/invalid/path', {
# Same test as above except we explicitly set our oauth2 mode
# Test to= and auto detection of oauth mode
'instance': plugins.NotifyFCM,
# we'll fail to send our notification as a result
'response': False,
}),
('fcm://apikey/#topic1/device/?mode=legacy', {
'instance': plugins.NotifyFCM,
# throw a bizzare code forcing us to fail to look it up
'response': False,
'requests_response_code': 999,
}),
('fcm://apikey/#topic1/device/?mode=legacy', {
'instance': plugins.NotifyFCM,
# Throws a series of connection and transfer exceptions when this flag
# is set and tests that we gracfully handle them
'test_requests_exceptions': True,
}),
('fcm://project/#topic1/device/?mode=oauth2&keyfile=file://{}'.format(
os.path.join(
os.path.dirname(__file__), 'var', 'fcm',
'service_account.json')), {
'instance': plugins.NotifyFCM,
# throw a bizzare code forcing us to fail to look it up
'response': False,
'requests_response_code': 999,
}),
('fcm://projectid/#topic1/device/?mode=oauth2&keyfile=file://{}'.format(
os.path.join(
os.path.dirname(__file__), 'var', 'fcm',
'service_account.json')), {
'instance': plugins.NotifyFCM,
# Throws a series of connection and transfer exceptions when
# this flag is set and tests that we gracfully handle them
'test_requests_exceptions': True,
}),
)
@pytest.mark.skipif(
'cryptography' not in sys.modules, reason="Requires cryptography")
def test_plugin_fcm_urls():
"""
NotifyFCM() Apprise URLs
"""
# Run our general tests
AppriseURLTester(tests=apprise_url_tests).run_all()
@pytest.mark.skipif(
'cryptography' not in sys.modules, reason="Requires cryptography")
@mock.patch('requests.post')
def test_fcm_plugin(mock_post):
def test_plugin_fcm_general(mock_post):
"""
API: NotifyFCM() General Checks
NotifyFCM() General Checks
"""
# Valid Keyfile
path = os.path.join(PRIVATE_KEYFILE_DIR, 'service_account.json')
@ -102,10 +235,12 @@ def test_fcm_plugin(mock_post):
'https://fcm.googleapis.com/v1/projects/mock-project-id/messages:send'
@pytest.mark.skipif(
'cryptography' not in sys.modules, reason="Requires cryptography")
@mock.patch('requests.post')
def test_fcm_keyfile_parse(mock_post):
def test_plugin_fcm_keyfile_parse(mock_post):
"""
API: NotifyFCM() KeyFile Tests
NotifyFCM() KeyFile Tests
"""
# Prepare a good response
@ -256,9 +391,11 @@ def test_fcm_keyfile_parse(mock_post):
assert oauth.access_token is None
def test_fcm_bad_keyfile_parse():
@pytest.mark.skipif(
'cryptography' not in sys.modules, reason="Requires cryptography")
def test_plugin_fcm_bad_keyfile_parse():
"""
API: NotifyFCM() KeyFile Bad Service Account Type Tests
NotifyFCM() KeyFile Bad Service Account Type Tests
"""
path = os.path.join(PRIVATE_KEYFILE_DIR, 'service_account-bad-type.json')
@ -266,9 +403,11 @@ def test_fcm_bad_keyfile_parse():
assert oauth.load(path) is False
def test_fcm_keyfile_missing_entries_parse(tmpdir):
@pytest.mark.skipif(
'cryptography' not in sys.modules, reason="Requires cryptography")
def test_plugin_fcm_keyfile_missing_entries_parse(tmpdir):
"""
API: NotifyFCM() KeyFile Missing Entries Test
NotifyFCM() KeyFile Missing Entries Test
"""
# Prepare a base keyfile reference to use
@ -302,3 +441,22 @@ def test_fcm_keyfile_missing_entries_parse(tmpdir):
path.write('{')
oauth = GoogleOAuth()
assert oauth.load(str(path)) is False
@pytest.mark.skipif(
'cryptography' in sys.modules,
reason="Requires that cryptography NOT be installed")
def test_plugin_fcm_cryptography_import_error():
"""
NotifyFCM Cryptography loading failure
"""
# Prepare a base keyfile reference to use
path = os.path.join(PRIVATE_KEYFILE_DIR, 'service_account.json')
# Attempt to instantiate our object
obj = Apprise.instantiate(
'fcm://mock-project-id/device/#topic/?keyfile={}'.format(str(path)))
# It's not possible because our cryptography depedancy is missing
assert obj is None

178
test/test_plugin_flock.py Normal file
View File

@ -0,0 +1,178 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2021 Chris Caron <lead2gold@gmail.com>
# All rights reserved.
#
# This code is licensed under the MIT License.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files(the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions :
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
import mock
import pytest
import requests
from apprise import plugins
from helpers import AppriseURLTester
# Disable logging for a cleaner testing output
import logging
logging.disable(logging.CRITICAL)
# Our Testing URLs
apprise_url_tests = (
('flock://', {
'instance': TypeError,
}),
# An invalid url
('flock://:@/', {
'instance': TypeError,
}),
# Provide a token
('flock://%s' % ('t' * 24), {
'instance': plugins.NotifyFlock,
}),
# Image handling
('flock://%s?image=True' % ('t' * 24), {
'instance': plugins.NotifyFlock,
# Our expected url(privacy=True) startswith() response:
'privacy_url': 'flock://t...t',
}),
('flock://%s?image=False' % ('t' * 24), {
'instance': plugins.NotifyFlock,
}),
('flock://%s?image=True' % ('t' * 24), {
'instance': plugins.NotifyFlock,
# Run test when image is set to True, but one couldn't actually be
# loaded from the Asset Object.
'include_image': False,
}),
# Test to=
('flock://%s?to=u:%s&format=markdown' % ('i' * 24, 'u' * 12), {
'instance': plugins.NotifyFlock,
}),
# Provide markdown format
('flock://%s?format=markdown' % ('i' * 24), {
'instance': plugins.NotifyFlock,
}),
# Provide text format
('flock://%s?format=text' % ('i' * 24), {
'instance': plugins.NotifyFlock,
}),
# Native URL Support, take the slack URL and still build from it
('https://api.flock.com/hooks/sendMessage/{}/'.format('i' * 24), {
'instance': plugins.NotifyFlock,
}),
# Native URL Support with arguments
('https://api.flock.com/hooks/sendMessage/{}/?format=markdown'.format(
'i' * 24), {
'instance': plugins.NotifyFlock,
}),
# Bot API presumed if one or more targets are specified
# Provide markdown format
('flock://%s/u:%s?format=markdown' % ('i' * 24, 'u' * 12), {
'instance': plugins.NotifyFlock,
}),
# Bot API presumed if one or more targets are specified
# Provide text format
('flock://%s/u:%s?format=html' % ('i' * 24, 'u' * 12), {
'instance': plugins.NotifyFlock,
}),
# Bot API presumed if one or more targets are specified
# u: is optional
('flock://%s/%s?format=text' % ('i' * 24, 'u' * 12), {
'instance': plugins.NotifyFlock,
}),
# Bot API presumed if one or more targets are specified
# Multi-entries
('flock://%s/g:%s/u:%s?format=text' % ('i' * 24, 'g' * 12, 'u' * 12), {
'instance': plugins.NotifyFlock,
}),
# Bot API presumed if one or more targets are specified
# Multi-entries using @ for user and # for channel
('flock://%s/#%s/@%s?format=text' % ('i' * 24, 'g' * 12, 'u' * 12), {
'instance': plugins.NotifyFlock,
}),
# Bot API presumed if one or more targets are specified
# has bad entry
('flock://%s/g:%s/u:%s?format=text' % ('i' * 24, 'g' * 12, 'u' * 10), {
'instance': plugins.NotifyFlock,
}),
# Invalid user/group defined
('flock://%s/g:/u:?format=text' % ('i' * 24), {
'instance': TypeError,
}),
# we don't focus on the invalid length of the user/group fields.
# As a result, the following will load and pass the data upstream
('flock://%s/g:%s/u:%s?format=text' % ('i' * 24, 'g' * 14, 'u' * 10), {
# We will still instantiate the object
'instance': plugins.NotifyFlock,
}),
# Error Testing
('flock://%s/g:%s/u:%s?format=text' % ('i' * 24, 'g' * 12, 'u' * 10), {
'instance': plugins.NotifyFlock,
# force a failure
'response': False,
'requests_response_code': requests.codes.internal_server_error,
}),
('flock://%s/' % ('t' * 24), {
'instance': plugins.NotifyFlock,
# force a failure
'response': False,
'requests_response_code': requests.codes.internal_server_error,
}),
('flock://%s/' % ('t' * 24), {
'instance': plugins.NotifyFlock,
# throw a bizzare code forcing us to fail to look it up
'response': False,
'requests_response_code': 999,
}),
('flock://%s/' % ('t' * 24), {
'instance': plugins.NotifyFlock,
# Throws a series of connection and transfer exceptions when this flag
# is set and tests that we gracfully handle them
'test_requests_exceptions': True,
}),
)
def test_plugin_flock_urls():
"""
NotifyFlock() Apprise URLs
"""
# Run our general tests
AppriseURLTester(tests=apprise_url_tests).run_all()
@mock.patch('requests.get')
@mock.patch('requests.post')
def test_plugin_flock_edge_cases(mock_post, mock_get):
"""
NotifyFlock() Edge Cases
"""
# Disable Throttling to speed testing
plugins.NotifyBase.request_rate_per_sec = 0
# Initializes the plugin with an invalid token
with pytest.raises(TypeError):
plugins.NotifyFlock(token=None)
# Whitespace also acts as an invalid token value
with pytest.raises(TypeError):
plugins.NotifyFlock(token=" ")

View File

@ -24,8 +24,10 @@
# THE SOFTWARE.
import six
import pytest
import mock
import requests
from helpers import AppriseURLTester
from apprise import plugins
from json import dumps
@ -35,12 +37,86 @@ from datetime import datetime
import logging
logging.disable(logging.CRITICAL)
# Our Testing URLs
apprise_url_tests = (
##################################
# NotifyGitter
##################################
('gitter://', {
'instance': TypeError,
}),
('gitter://:@/', {
'instance': TypeError,
}),
# Invalid Token Length
('gitter://%s' % ('a' * 12), {
'instance': TypeError,
}),
# Token specified but no channel
('gitter://%s' % ('a' * 40), {
'instance': TypeError,
}),
# Token + channel
('gitter://%s/apprise' % ('b' * 40), {
'instance': plugins.NotifyGitter,
'response': False,
}),
# include image in post
('gitter://%s/apprise?image=Yes' % ('c' * 40), {
'instance': plugins.NotifyGitter,
'response': False,
# Our expected url(privacy=True) startswith() response:
'privacy_url': 'gitter://c...c/apprise',
}),
# Don't include image in post (this is the default anyway)
('gitter://%s/apprise?image=Yes' % ('d' * 40), {
'instance': plugins.NotifyGitter,
'response': False,
# don't include an image by default
'include_image': False,
}),
# Don't include image in post (this is the default anyway)
('gitter://%s/apprise?image=No' % ('e' * 40), {
'instance': plugins.NotifyGitter,
'response': False,
}),
('gitter://%s/apprise' % ('f' * 40), {
'instance': plugins.NotifyGitter,
# force a failure
'response': False,
'requests_response_code': requests.codes.internal_server_error,
}),
('gitter://%s/apprise' % ('g' * 40), {
'instance': plugins.NotifyGitter,
# throw a bizzare code forcing us to fail to look it up
'response': False,
'requests_response_code': 999,
}),
('gitter://%s/apprise' % ('h' * 40), {
'instance': plugins.NotifyGitter,
# Throws a series of connection and transfer exceptions when this flag
# is set and tests that we gracfully handle them
'test_requests_exceptions': True,
}),
)
def test_plugin_gitter_urls():
"""
NotifyGitter() Apprise URLs
"""
# Run our general tests
AppriseURLTester(tests=apprise_url_tests).run_all()
@mock.patch('requests.get')
@mock.patch('requests.post')
def test_notify_gitter_plugin_general(mock_post, mock_get):
def test_plugin_gitter_general(mock_post, mock_get):
"""
API: NotifyGitter() General Tests
NotifyGitter() General Tests
"""
# Disable Throttling to speed testing
@ -191,3 +267,19 @@ def test_notify_gitter_plugin_general(mock_post, mock_get):
}
}
assert obj.send(body='test body', title='test title') is False
def test_plugin_gitter_edge_cases():
"""
NotifyGitter() Edge Cases
"""
# Define our channels
targets = ['apprise']
# Initializes the plugin with an invalid token
with pytest.raises(TypeError):
plugins.NotifyGitter(token=None, targets=targets)
# Whitespace also acts as an invalid token value
with pytest.raises(TypeError):
plugins.NotifyGitter(token=" ", targets=targets)

View File

@ -58,11 +58,10 @@ from dbus import DBusException # noqa E402
@mock.patch('dbus.ByteArray')
@mock.patch('dbus.Byte')
@mock.patch('dbus.mainloop')
def test_dbus_plugin(mock_mainloop, mock_byte, mock_bytearray,
mock_interface, mock_sessionbus):
def test_plugin_dbus_general(mock_mainloop, mock_byte, mock_bytearray,
mock_interface, mock_sessionbus):
"""
API: NotifyDBus Plugin()
NotifyDBus() General Tests
"""
# Our module base
@ -156,9 +155,6 @@ def test_dbus_plugin(mock_mainloop, mock_byte, mock_bytearray,
assert obj.url().startswith('glib://_/')
obj.duration = 0
# Check that it found our mocked environments
assert obj._enabled is True
# Test our class loading using a series of arguments
with pytest.raises(TypeError):
apprise.plugins.NotifyDBus(**{'schema': 'invalid'})
@ -299,7 +295,7 @@ def test_dbus_plugin(mock_mainloop, mock_byte, mock_bytearray,
# Test our exception handling during initialization
# Toggle our testing for when we can't send notifications because the
# package has been made unavailable to us
obj._enabled = False
obj.enabled = False
assert obj.notify(
title='title', body='body',
notify_type=apprise.NotifyType.INFO) is False
@ -390,21 +386,14 @@ def test_dbus_plugin(mock_mainloop, mock_byte, mock_bytearray,
reload(sys.modules['apprise.Apprise'])
reload(sys.modules['apprise'])
# Create our instance
# We can no longer instantiate an instance because dbus has been
# officialy marked unavailable and thus the module is marked
# as such
obj = apprise.Apprise.instantiate('glib://', suppress_exceptions=False)
assert isinstance(obj, apprise.plugins.NotifyDBus) is True
obj.duration = 0
# Test url() call
assert isinstance(obj.url(), six.string_types) is True
# Our notification fail because the dbus library wasn't present
assert obj.notify(
title='title', body='body',
notify_type=apprise.NotifyType.INFO) is False
assert obj is None
# Since playing with the sys.modules is not such a good idea,
# let's just put it back now :)
# let's just put our old configuration back:
sys.modules['dbus'] = _session_bus
# Reload our modules
reload(sys.modules['apprise.plugins.NotifyDBus'])

View File

@ -27,7 +27,7 @@ import six
import mock
import sys
import types
import pytest
import apprise
try:
@ -46,9 +46,10 @@ import logging
logging.disable(logging.CRITICAL)
def test_gnome_plugin():
@pytest.mark.skipif(sys.version_info.major <= 2, reason="Requires Python 3.x+")
def test_plugin_gnome_general():
"""
API: NotifyGnome Plugin()
NotifyGnome() General Checks
"""
@ -113,10 +114,13 @@ def test_gnome_plugin():
# Create our instance
obj = apprise.Apprise.instantiate('gnome://', suppress_exceptions=False)
assert obj is not None
# Set our duration to 0 to speed up timeouts (for testing)
obj.duration = 0
# Check that it found our mocked environments
assert obj._enabled is True
assert obj.enabled is True
# Test url() call
assert isinstance(obj.url(), six.string_types) is True
@ -207,7 +211,7 @@ def test_gnome_plugin():
# Toggle our testing for when we can't send notifications because the
# package has been made unavailable to us
obj._enabled = False
obj.enabled = False
assert obj.notify(title='title', body='body',
notify_type=apprise.NotifyType.INFO) is False
@ -232,12 +236,7 @@ def test_gnome_plugin():
reload(sys.modules['apprise.Apprise'])
reload(sys.modules['apprise'])
# Create our instance
# We can now no longer load our instance
# The object internally is marked disabled
obj = apprise.Apprise.instantiate('gnome://', suppress_exceptions=False)
assert isinstance(obj, apprise.plugins.NotifyGnome) is True
obj.duration = 0
# Our notifications can not work without our gi library having been
# loaded.
assert obj.notify(title='title', body='body',
notify_type=apprise.NotifyType.INFO) is False
assert obj is None

View File

@ -0,0 +1,93 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2021 Chris Caron <lead2gold@gmail.com>
# All rights reserved.
#
# This code is licensed under the MIT License.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files(the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions :
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
import requests
from apprise import plugins
from helpers import AppriseURLTester
# Disable logging for a cleaner testing output
import logging
logging.disable(logging.CRITICAL)
# Our Testing URLs
apprise_url_tests = (
('gchat://', {
'instance': TypeError,
}),
('gchat://:@/', {
'instance': TypeError,
}),
# Workspace, but not Key or Token
('gchat://workspace', {
'instance': TypeError,
}),
# Workspace and key, but no Token
('gchat://workspace/key/', {
'instance': TypeError,
}),
# Credentials are good
('gchat://workspace/key/token', {
'instance': plugins.NotifyGoogleChat,
'privacy_url': 'gchat://w...e/k...y/t...n',
}),
# Test arguments
('gchat://?workspace=ws&key=mykey&token=mytoken', {
'instance': plugins.NotifyGoogleChat,
'privacy_url': 'gchat://w...s/m...y/m...n',
}),
# Google Native Webhohok URL
('https://chat.googleapis.com/v1/spaces/myworkspace/messages'
'?key=mykey&token=mytoken', {
'instance': plugins.NotifyGoogleChat,
'privacy_url': 'gchat://m...e/m...y/m...n'}),
('gchat://workspace/key/token', {
'instance': plugins.NotifyGoogleChat,
# force a failure
'response': False,
'requests_response_code': requests.codes.internal_server_error,
}),
('gchat://workspace/key/token', {
'instance': plugins.NotifyGoogleChat,
# throw a bizzare code forcing us to fail to look it up
'response': False,
'requests_response_code': 999,
}),
('gchat://workspace/key/token', {
'instance': plugins.NotifyGoogleChat,
# Throws a series of connection and transfer exceptions when this flag
# is set and tests that we gracfully handle them
'test_requests_exceptions': True,
}),
)
def test_plugin_google_chat_urls():
"""
NotifyGoogleChat() Apprise URLs
"""
# Run our general tests
AppriseURLTester(tests=apprise_url_tests).run_all()

121
test/test_plugin_gotify.py Normal file
View File

@ -0,0 +1,121 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2021 Chris Caron <lead2gold@gmail.com>
# All rights reserved.
#
# This code is licensed under the MIT License.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files(the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions :
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
import pytest
import requests
from apprise import plugins
from helpers import AppriseURLTester
# Disable logging for a cleaner testing output
import logging
logging.disable(logging.CRITICAL)
# Our Testing URLs
apprise_url_tests = (
('gotify://', {
'instance': None,
}),
# No token specified
('gotify://hostname', {
'instance': TypeError,
}),
# Provide a hostname and token
('gotify://hostname/%s' % ('t' * 16), {
'instance': plugins.NotifyGotify,
# Our expected url(privacy=True) startswith() response:
'privacy_url': 'gotify://hostname/t...t',
}),
# Provide a hostname, path, and token
('gotify://hostname/a/path/ending/in/a/slash/%s' % ('u' * 16), {
'instance': plugins.NotifyGotify,
# Our expected url(privacy=True) startswith() response:
'privacy_url': 'gotify://hostname/a/path/ending/in/a/slash/u...u/',
}),
# Markdown test
('gotify://hostname/%s?format=markdown' % ('t' * 16), {
'instance': plugins.NotifyGotify,
}),
# Provide a hostname, path, and token
('gotify://hostname/a/path/not/ending/in/a/slash/%s' % ('v' * 16), {
'instance': plugins.NotifyGotify,
# Our expected url(privacy=True) startswith() response:
'privacy_url': 'gotify://hostname/a/path/not/ending/in/a/slash/v...v/',
}),
# Provide a priority
('gotify://hostname/%s?priority=high' % ('i' * 16), {
'instance': plugins.NotifyGotify,
}),
# Provide an invalid priority
('gotify://hostname:8008/%s?priority=invalid' % ('i' * 16), {
'instance': plugins.NotifyGotify,
}),
# An invalid url
('gotify://:@/', {
'instance': None,
}),
('gotify://hostname/%s/' % ('t' * 16), {
'instance': plugins.NotifyGotify,
# force a failure
'response': False,
'requests_response_code': requests.codes.internal_server_error,
}),
('gotifys://localhost/%s/' % ('t' * 16), {
'instance': plugins.NotifyGotify,
# throw a bizzare code forcing us to fail to look it up
'response': False,
'requests_response_code': 999,
}),
('gotify://localhost/%s/' % ('t' * 16), {
'instance': plugins.NotifyGotify,
# Throws a series of connection and transfer exceptions when this flag
# is set and tests that we gracfully handle them
'test_requests_exceptions': True,
}),
)
def test_plugin_gotify_urls():
"""
NotifyGotify() Apprise URLs
"""
# Run our general tests
AppriseURLTester(tests=apprise_url_tests).run_all()
def test_plugin_gotify_edge_cases():
"""
NotifyGotify() Edge Cases
"""
# Initializes the plugin with an invalid token
with pytest.raises(TypeError):
plugins.NotifyGotify(token=None)
# Whitespace also acts as an invalid token value
with pytest.raises(TypeError):
plugins.NotifyGotify(token=" ")

View File

@ -23,29 +23,12 @@
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
import os
import sys
import mock
import six
import pytest
import apprise
# Disable logging for a cleaner testing output
import logging
logging.disable(logging.CRITICAL)
try:
# Python v3.4+
from importlib import reload
except ImportError:
try:
# Python v3.0-v3.3
from imp import reload
except ImportError:
# Python v2.7
pass
try:
from gntp import errors
@ -59,95 +42,36 @@ try:
errors.UnsupportedError(
0, 'gntp.UnsupportedError() not handled'),
)
except ImportError:
# no problem; these tests will be skipped at this point
TEST_GROWL_EXCEPTIONS = tuple()
# no problem; gntp isn't available to us
pass
# Disable logging for a cleaner testing output
import logging
logging.disable(logging.CRITICAL)
@pytest.mark.skipif('gntp' not in sys.modules, reason="requires gntp")
def test_growl_plugin_import_error(tmpdir):
@pytest.mark.skipif(
'gntp' in sys.modules,
reason="Requires that gntp NOT be installed")
def test_plugin_growl_gntp_import_error():
"""
API: NotifyGrowl Plugin() Import Error
NotifyGrowl() Import Error
"""
# This is a really confusing test case; it can probably be done better,
# but this was all I could come up with. Effectively Apprise is will
# still work flawlessly without the gntp dependancy. Since
# gntp is actually required to be installed to run these unit tests
# we need to do some hacky tricks into fooling our test cases that the
# package isn't available.
# So we create a temporary directory called gntp (simulating the
# library itself) and writing an __init__.py in it that does nothing
# but throw an ImportError exception (simulating that the library
# isn't found).
suite = tmpdir.mkdir("gntp")
suite.join("__init__.py").write('')
module_name = 'gntp'
suite.join("{}.py".format(module_name)).write('raise ImportError()')
# The second part of the test is to update our PYTHON_PATH to look
# into this new directory first (before looking where the actual
# valid paths are). This will allow us to override 'JUST' the sleekxmpp
# path.
# Update our path to point to our new test suite
sys.path.insert(0, str(suite))
# We need to remove the gntp modules that have already been loaded
# in memory otherwise they'll just be used instead. Python is smart and
# won't go try and reload everything again if it doesn't have to.
for name in list(sys.modules.keys()):
if name.startswith('{}.'.format(module_name)):
del sys.modules[name]
del sys.modules[module_name]
# The following libraries need to be reloaded to prevent
# TypeError: super(type, obj): obj must be an instance or subtype of type
# This is better explained in this StackOverflow post:
# https://stackoverflow.com/questions/31363311/\
# any-way-to-manually-fix-operation-of-\
# super-after-ipython-reload-avoiding-ty
#
reload(sys.modules['apprise.plugins.NotifyGrowl'])
reload(sys.modules['apprise.plugins'])
reload(sys.modules['apprise.Apprise'])
reload(sys.modules['apprise'])
# This tests that Apprise still works without gntp.
# If the object is disabled, then it can't be instantiated
obj = apprise.Apprise.instantiate('growl://growl.server')
# Growl objects can still be instantiated however
assert obj is not None
# Notifications won't work because gntp did not load
assert obj.notify(
title='test', body='body',
notify_type=apprise.NotifyType.INFO) is False
# Tidy-up / restore things to how they were
# Remove our garbage library
os.unlink(str(suite.join("{}.py".format(module_name))))
# Remove our custom entry into the path
sys.path.remove(str(suite))
# Reload the libraries we care about
reload(sys.modules['apprise.plugins.NotifyGrowl'])
reload(sys.modules['apprise.plugins'])
reload(sys.modules['apprise.Apprise'])
reload(sys.modules['apprise'])
assert obj is None
@pytest.mark.skipif('gntp' not in sys.modules, reason="requires gntp")
@pytest.mark.skipif(
'gntp' not in sys.modules, reason="Requires gntp")
@mock.patch('gntp.notifier.GrowlNotifier')
def test_growl_exception_handling(mock_gntp):
def test_plugin_growl_exception_handling(mock_gntp):
"""
API: NotifyGrowl Exception Handling
NotifyGrowl() Exception Handling
"""
from gntp import errors
TEST_GROWL_EXCEPTIONS = (
errors.NetworkError(
0, 'gntp.ParseError() not handled'),
@ -200,11 +124,11 @@ def test_growl_exception_handling(mock_gntp):
@pytest.mark.skipif(
'gntp' not in sys.modules, reason="requires gntp")
'gntp' not in sys.modules, reason="Requires gntp")
@mock.patch('gntp.notifier.GrowlNotifier')
def test_growl_plugin(mock_gntp):
def test_plugin_growl_general(mock_gntp):
"""
API: NotifyGrowl Plugin()
NotifyGrowl() General Checks
"""

View File

@ -0,0 +1,152 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2021 Chris Caron <lead2gold@gmail.com>
# All rights reserved.
#
# This code is licensed under the MIT License.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files(the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions :
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
import six
import mock
import requests
from apprise import plugins
from apprise import Apprise
from helpers import AppriseURLTester
# Disable logging for a cleaner testing output
import logging
logging.disable(logging.CRITICAL)
# Our Testing URLs
apprise_url_tests = (
('hassio://:@/', {
'instance': TypeError,
}),
('hassio://', {
'instance': TypeError,
}),
('hassios://', {
'instance': TypeError,
}),
# No Long Lived Access Token specified
('hassio://user@localhost', {
'instance': TypeError,
}),
('hassio://localhost/long-lived-access-token', {
'instance': plugins.NotifyHomeAssistant,
}),
('hassio://user:pass@localhost/long-lived-access-token/', {
'instance': plugins.NotifyHomeAssistant,
# Our expected url(privacy=True) startswith() response:
'privacy_url': 'hassio://user:****@localhost/l...n',
}),
('hassio://localhost:80/long-lived-access-token', {
'instance': plugins.NotifyHomeAssistant,
}),
('hassio://user@localhost:8123/llat', {
'instance': plugins.NotifyHomeAssistant,
'privacy_url': 'hassio://user@localhost/l...t',
}),
('hassios://localhost/llat?nid=!%', {
# Invalid notification_id
'instance': TypeError,
}),
('hassios://localhost/llat?nid=abcd', {
# Valid notification_id
'instance': plugins.NotifyHomeAssistant,
}),
('hassios://user:pass@localhost/llat', {
'instance': plugins.NotifyHomeAssistant,
'privacy_url': 'hassios://user:****@localhost/l...t',
}),
('hassios://localhost:8443/path/llat/', {
'instance': plugins.NotifyHomeAssistant,
'privacy_url': 'hassios://localhost:8443/path/l...t',
}),
('hassio://localhost:8123/a/path?accesstoken=llat', {
'instance': plugins.NotifyHomeAssistant,
# Default port; so it's stripped off
# accesstoken was specified as kwarg
'privacy_url': 'hassio://localhost/a/path/l...t',
}),
('hassios://user:password@localhost:80/llat/', {
'instance': plugins.NotifyHomeAssistant,
'privacy_url': 'hassios://user:****@localhost:80',
}),
('hassio://user:pass@localhost:8123/llat', {
'instance': plugins.NotifyHomeAssistant,
# force a failure
'response': False,
'requests_response_code': requests.codes.internal_server_error,
}),
('hassio://user:pass@localhost/llat', {
'instance': plugins.NotifyHomeAssistant,
# throw a bizzare code forcing us to fail to look it up
'response': False,
'requests_response_code': 999,
}),
('hassio://user:pass@localhost/llat', {
'instance': plugins.NotifyHomeAssistant,
# Throws a series of connection and transfer exceptions when this flag
# is set and tests that we gracfully handle them
'test_requests_exceptions': True,
}),
)
def test_plugin_homeassistant_urls():
"""
NotifyHomeAssistant() Apprise URLs
"""
# Run our general tests
AppriseURLTester(tests=apprise_url_tests).run_all()
@mock.patch('requests.post')
def test_plugin_homeassistant_general(mock_post):
"""
NotifyHomeAssistant() General Checks
"""
# Disable Throttling to speed testing
plugins.NotifyBase.request_rate_per_sec = 0
response = mock.Mock()
response.content = ''
response.status_code = requests.codes.ok
# Prepare Mock
mock_post.return_value = response
# Variation Initializations
obj = Apprise.instantiate('hassio://localhost/accesstoken')
assert isinstance(obj, plugins.NotifyHomeAssistant) is True
assert isinstance(obj.url(), six.string_types) is True
# Send Notification
assert obj.send(body="test") is True
assert mock_post.call_count == 1
assert mock_post.call_args_list[0][0][0] == \
'http://localhost:8123/api/services/persistent_notification/create'

206
test/test_plugin_ifttt.py Normal file
View File

@ -0,0 +1,206 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2021 Chris Caron <lead2gold@gmail.com>
# All rights reserved.
#
# This code is licensed under the MIT License.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files(the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions :
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
import pytest
import mock
import requests
from apprise import plugins
from apprise import NotifyType
from helpers import AppriseURLTester
# Disable logging for a cleaner testing output
import logging
logging.disable(logging.CRITICAL)
# Our Testing URLs
apprise_url_tests = (
('ifttt://', {
'instance': TypeError,
}),
('ifttt://:@/', {
'instance': TypeError,
}),
# No User
('ifttt://EventID/', {
'instance': TypeError,
}),
# A nicely formed ifttt url with 1 event and a new key/value store
('ifttt://WebHookID@EventID/?+TemplateKey=TemplateVal', {
'instance': plugins.NotifyIFTTT,
# Our expected url(privacy=True) startswith() response:
'privacy_url': 'ifttt://W...D',
}),
# Test to= in which case we set the host to the webhook id
('ifttt://WebHookID?to=EventID,EventID2', {
'instance': plugins.NotifyIFTTT,
}),
# Removing certain keys:
('ifttt://WebHookID@EventID/?-Value1=&-Value2', {
'instance': plugins.NotifyIFTTT,
}),
# A nicely formed ifttt url with 2 events defined:
('ifttt://WebHookID@EventID/EventID2/', {
'instance': plugins.NotifyIFTTT,
}),
# Support Native URL references
('https://maker.ifttt.com/use/WebHookID/', {
# No EventID specified
'instance': TypeError,
}),
('https://maker.ifttt.com/use/WebHookID/EventID/', {
'instance': plugins.NotifyIFTTT,
}),
# Native URL with arguments
('https://maker.ifttt.com/use/WebHookID/EventID/?-Value1=', {
'instance': plugins.NotifyIFTTT,
}),
# Test website connection failures
('ifttt://WebHookID@EventID', {
'instance': plugins.NotifyIFTTT,
# force a failure
'response': False,
'requests_response_code': requests.codes.internal_server_error,
}),
('ifttt://WebHookID@EventID', {
'instance': plugins.NotifyIFTTT,
# throw a bizzare code forcing us to fail to look it up
'response': False,
'requests_response_code': 999,
}),
('ifttt://WebHookID@EventID', {
'instance': plugins.NotifyIFTTT,
# Throws a series of connection and transfer exceptions when this flag
# is set and tests that we gracfully handle them
'test_requests_exceptions': True,
}),
)
def test_plugin_ifttt_urls():
"""
NotifyIFTTT() Apprise URLs
"""
# Run our general tests
AppriseURLTester(tests=apprise_url_tests).run_all()
@mock.patch('requests.get')
@mock.patch('requests.post')
def test_plugin_ifttt_edge_cases(mock_post, mock_get):
"""
NotifyIFTTT() Edge Cases
"""
# Disable Throttling to speed testing
plugins.NotifyBase.request_rate_per_sec = 0
# Initialize some generic (but valid) tokens
webhook_id = 'webhook_id'
events = ['event1', 'event2']
# Prepare Mock
mock_get.return_value = requests.Request()
mock_post.return_value = requests.Request()
mock_post.return_value.status_code = requests.codes.ok
mock_get.return_value.status_code = requests.codes.ok
mock_get.return_value.content = '{}'
mock_post.return_value.content = '{}'
# No webhook_id specified
with pytest.raises(TypeError):
plugins.NotifyIFTTT(webhook_id=None, events=None)
# Disable Throttling to speed testing
plugins.NotifyBase.request_rate_per_sec = 0
# Initializes the plugin with an invalid webhook id
with pytest.raises(TypeError):
plugins.NotifyIFTTT(webhook_id=None, events=events)
# Whitespace also acts as an invalid webhook id
with pytest.raises(TypeError):
plugins.NotifyIFTTT(webhook_id=" ", events=events)
# No events specified
with pytest.raises(TypeError):
plugins.NotifyIFTTT(webhook_id=webhook_id, events=None)
obj = plugins.NotifyIFTTT(webhook_id=webhook_id, events=events)
assert isinstance(obj, plugins.NotifyIFTTT) is True
assert obj.notify(
body='body', title='title', notify_type=NotifyType.INFO) is True
# Test the addition of tokens
obj = plugins.NotifyIFTTT(
webhook_id=webhook_id, events=events,
add_tokens={'Test': 'ValueA', 'Test2': 'ValueB'})
assert isinstance(obj, plugins.NotifyIFTTT) is True
assert obj.notify(
body='body', title='title', notify_type=NotifyType.INFO) is True
# Invalid del_tokens entry
with pytest.raises(TypeError):
plugins.NotifyIFTTT(
webhook_id=webhook_id, events=events,
del_tokens=plugins.NotifyIFTTT.ifttt_default_title_key)
assert isinstance(obj, plugins.NotifyIFTTT) is True
assert obj.notify(
body='body', title='title', notify_type=NotifyType.INFO) is True
# Test removal of tokens by a list
obj = plugins.NotifyIFTTT(
webhook_id=webhook_id, events=events,
add_tokens={
'MyKey': 'MyValue'
},
del_tokens=(
plugins.NotifyIFTTT.ifttt_default_title_key,
plugins.NotifyIFTTT.ifttt_default_body_key,
plugins.NotifyIFTTT.ifttt_default_type_key))
assert isinstance(obj, plugins.NotifyIFTTT) is True
assert obj.notify(
body='body', title='title', notify_type=NotifyType.INFO) is True
# Test removal of tokens as dict
obj = plugins.NotifyIFTTT(
webhook_id=webhook_id, events=events,
add_tokens={
'MyKey': 'MyValue'
},
del_tokens={
plugins.NotifyIFTTT.ifttt_default_title_key: None,
plugins.NotifyIFTTT.ifttt_default_body_key: None,
plugins.NotifyIFTTT.ifttt_default_type_key: None})
assert isinstance(obj, plugins.NotifyIFTTT) is True

168
test/test_plugin_join.py Normal file
View File

@ -0,0 +1,168 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2021 Chris Caron <lead2gold@gmail.com>
# All rights reserved.
#
# This code is licensed under the MIT License.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files(the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions :
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
import mock
import pytest
import requests
from apprise import plugins
from apprise import NotifyType
from helpers import AppriseURLTester
# Disable logging for a cleaner testing output
import logging
logging.disable(logging.CRITICAL)
# Our Testing URLs
apprise_url_tests = (
('join://', {
'instance': TypeError,
}),
# API Key + bad url
('join://:@/', {
'instance': TypeError,
}),
# APIkey; no device
('join://%s' % ('a' * 32), {
'instance': plugins.NotifyJoin,
}),
# API Key + device (using to=)
('join://%s?to=%s' % ('a' * 32, 'd' * 32), {
'instance': plugins.NotifyJoin,
# Our expected url(privacy=True) startswith() response:
'privacy_url': 'join://a...a/',
}),
# API Key + priority setting
('join://%s?priority=high' % ('a' * 32), {
'instance': plugins.NotifyJoin,
}),
# API Key + invalid priority setting
('join://%s?priority=invalid' % ('a' * 32), {
'instance': plugins.NotifyJoin,
}),
# API Key + priority setting (empty)
('join://%s?priority=' % ('a' * 32), {
'instance': plugins.NotifyJoin,
}),
# API Key + device
('join://%s@%s?image=True' % ('a' * 32, 'd' * 32), {
'instance': plugins.NotifyJoin,
}),
# No image
('join://%s@%s?image=False' % ('a' * 32, 'd' * 32), {
'instance': plugins.NotifyJoin,
}),
# API Key + Device Name
('join://%s/%s' % ('a' * 32, 'My Device'), {
'instance': plugins.NotifyJoin,
}),
# API Key + device
('join://%s/%s' % ('a' * 32, 'd' * 32), {
'instance': plugins.NotifyJoin,
# don't include an image by default
'include_image': False,
}),
# API Key + 2 devices
('join://%s/%s/%s' % ('a' * 32, 'd' * 32, 'e' * 32), {
'instance': plugins.NotifyJoin,
# don't include an image by default
'include_image': False,
}),
# API Key + 1 device and 1 group
('join://%s/%s/%s' % ('a' * 32, 'd' * 32, 'group.chrome'), {
'instance': plugins.NotifyJoin,
}),
('join://%s' % ('a' * 32), {
'instance': plugins.NotifyJoin,
# force a failure
'response': False,
'requests_response_code': requests.codes.internal_server_error,
}),
('join://%s' % ('a' * 32), {
'instance': plugins.NotifyJoin,
# throw a bizzare code forcing us to fail to look it up
'response': False,
'requests_response_code': 999,
}),
('join://%s' % ('a' * 32), {
'instance': plugins.NotifyJoin,
# Throws a series of connection and transfer exceptions when this flag
# is set and tests that we gracfully handle them
'test_requests_exceptions': True,
}),
)
def test_plugin_join_urls():
"""
NotifyJoin() Apprise URLs
"""
# Run our general tests
AppriseURLTester(tests=apprise_url_tests).run_all()
@mock.patch('requests.get')
@mock.patch('requests.post')
def test_plugin_join_edge_cases(mock_post, mock_get):
"""
NotifyJoin() Edge Cases
"""
# Disable Throttling to speed testing
plugins.NotifyBase.request_rate_per_sec = 0
# Generate some generic message types
device = 'A' * 32
group = 'group.chrome'
apikey = 'a' * 32
# Initializes the plugin with devices set to a string
plugins.NotifyJoin(apikey=apikey, targets=group)
# Initializes the plugin with devices set to None
plugins.NotifyJoin(apikey=apikey, targets=None)
# Initializes the plugin with an invalid apikey
with pytest.raises(TypeError):
plugins.NotifyJoin(apikey=None)
# Whitespace also acts as an invalid apikey
with pytest.raises(TypeError):
plugins.NotifyJoin(apikey=" ")
# Initializes the plugin with devices set to a set
p = plugins.NotifyJoin(apikey=apikey, targets=[group, device])
# Prepare our mock responses
req = requests.Request()
req.status_code = requests.codes.created
req.content = ''
mock_get.return_value = req
mock_post.return_value = req
# Test notifications without a body or a title; nothing to send
# so we return False
p.notify(body=None, title=None, notify_type=NotifyType.INFO) is False

View File

@ -0,0 +1,103 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2021 Chris Caron <lead2gold@gmail.com>
# All rights reserved.
#
# This code is licensed under the MIT License.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files(the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions :
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
from apprise import plugins
from helpers import AppriseURLTester
# Disable logging for a cleaner testing output
import logging
logging.disable(logging.CRITICAL)
# Our Testing URLs
apprise_url_tests = (
('kavenegar://', {
# We failed to identify any valid authentication
'instance': TypeError,
}),
('kavenegar://:@/', {
# We failed to identify any valid authentication
'instance': TypeError,
}),
('kavenegar://{}/{}/{}'.format('1' * 10, '2' * 15, 'a' * 13), {
# valid api key and valid authentication
'instance': plugins.NotifyKavenegar,
# Since there are no targets specified we expect a False return on
# send()
'notify_response': False,
}),
('kavenegar://{}/{}'.format('a' * 24, '3' * 14), {
# valid api key and valid number
'instance': plugins.NotifyKavenegar,
# Our expected url(privacy=True) startswith() response:
'privacy_url': 'kavenegar://a...a/',
}),
('kavenegar://{}?to={}'.format('a' * 24, '3' * 14), {
# valid api key and valid number
'instance': plugins.NotifyKavenegar,
# Our expected url(privacy=True) startswith() response:
'privacy_url': 'kavenegar://a...a/',
}),
('kavenegar://{}@{}/{}'.format('1' * 14, 'b' * 24, '3' * 14), {
# valid api key and valid number
'instance': plugins.NotifyKavenegar,
# Our expected url(privacy=True) startswith() response:
'privacy_url': 'kavenegar://{}@b...b/'.format('1' * 14),
}),
('kavenegar://{}@{}/{}'.format('a' * 14, 'b' * 24, '3' * 14), {
# invalid from number
'instance': TypeError,
}),
('kavenegar://{}@{}/{}'.format('3' * 4, 'b' * 24, '3' * 14), {
# invalid from number
'instance': TypeError,
}),
('kavenegar://{}/{}?from={}'.format('b' * 24, '3' * 14, '1' * 14), {
# valid api key and valid number
'instance': plugins.NotifyKavenegar,
# Our expected url(privacy=True) startswith() response:
'privacy_url': 'kavenegar://{}@b...b/'.format('1' * 14),
}),
('kavenegar://{}/{}'.format('b' * 24, '4' * 14), {
'instance': plugins.NotifyKavenegar,
# throw a bizzare code forcing us to fail to look it up
'response': False,
'requests_response_code': 999,
}),
('kavenegar://{}/{}'.format('c' * 24, '5' * 14), {
'instance': plugins.NotifyKavenegar,
# Throws a series of connection and transfer exceptions when this flag
# is set and tests that we gracfully handle them
'test_requests_exceptions': True,
}),
)
def test_plugin_kavenegar_urls():
"""
NotifyKavenegar() Apprise URLs
"""
# Run our general tests
AppriseURLTester(tests=apprise_url_tests).run_all()

116
test/test_plugin_kumulos.py Normal file
View File

@ -0,0 +1,116 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2021 Chris Caron <lead2gold@gmail.com>
# All rights reserved.
#
# This code is licensed under the MIT License.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files(the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions :
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
import pytest
import requests
from apprise import plugins
from helpers import AppriseURLTester
# Disable logging for a cleaner testing output
import logging
logging.disable(logging.CRITICAL)
# a test UUID we can use
UUID4 = '8b799edf-6f98-4d3a-9be7-2862fb4e5752'
# Our Testing URLs
apprise_url_tests = (
('kumulos://', {
# No API or Server Key specified
'instance': TypeError,
}),
('kumulos://:@/', {
# No API or Server Key specified
# We don't have strict host checking on for kumulos, so this URL
# actually becomes parseable and :@ becomes a hostname.
# The below errors because a second token wasn't found
'instance': TypeError,
}),
('kumulos://{}/'.format(UUID4), {
# No server key was specified
'instance': TypeError,
}),
('kumulos://{}/{}/'.format(UUID4, 'w' * 36), {
# Everything is okay
'instance': plugins.NotifyKumulos,
# Our expected url(privacy=True) startswith() response:
'privacy_url': 'kumulos://8...2/w...w/',
}),
('kumulos://{}/{}/'.format(UUID4, 'x' * 36), {
'instance': plugins.NotifyKumulos,
# force a failure
'response': False,
'requests_response_code': requests.codes.internal_server_error,
# Our expected url(privacy=True) startswith() response:
'privacy_url': 'kumulos://8...2/x...x/',
}),
('kumulos://{}/{}/'.format(UUID4, 'y' * 36), {
'instance': plugins.NotifyKumulos,
# throw a bizzare code forcing us to fail to look it up
'response': False,
'requests_response_code': 999,
# Our expected url(privacy=True) startswith() response:
'privacy_url': 'kumulos://8...2/y...y/',
}),
('kumulos://{}/{}/'.format(UUID4, 'z' * 36), {
'instance': plugins.NotifyKumulos,
# Throws a series of connection and transfer exceptions when this flag
# is set and tests that we gracfully handle them
'test_requests_exceptions': True,
}),
)
def test_plugin_kumulos_urls():
"""
NotifyKumulos() Apprise URLs
"""
# Run our general tests
AppriseURLTester(tests=apprise_url_tests).run_all()
def test_plugin_kumulos_edge_cases():
"""
NotifyKumulos() Edge Cases
"""
# Disable Throttling to speed testing
plugins.NotifyBase.request_rate_per_sec = 0
# Invalid API Key
with pytest.raises(TypeError):
plugins.NotifyKumulos(None, None)
with pytest.raises(TypeError):
plugins.NotifyKumulos(" ", None)
# Invalid Server Key
with pytest.raises(TypeError):
plugins.NotifyKumulos("abcd", None)
with pytest.raises(TypeError):
plugins.NotifyKumulos("abcd", " ")

View File

@ -0,0 +1,248 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2021 Chris Caron <lead2gold@gmail.com>
# All rights reserved.
#
# This code is licensed under the MIT License.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files(the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions :
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
import pytest
import requests
from apprise import plugins
from helpers import AppriseURLTester
# Disable logging for a cleaner testing output
import logging
logging.disable(logging.CRITICAL)
# a test UUID we can use
UUID4 = '8b799edf-6f98-4d3a-9be7-2862fb4e5752'
# Our Testing URLs
apprise_url_tests = (
('lametric://', {
# No APIKey or App ID specified
'instance': TypeError,
}),
('lametric://:@/', {
# No APIKey or App ID specified
'instance': TypeError,
}),
('lametric://{}/'.format(
'com.lametric.941c51dff3135bd87aa72db9d855dd50'), {
# No APIKey specified
'instance': TypeError,
}),
('lametric://root:{}@192.168.0.5:8080/'.format(UUID4), {
# Everything is okay; this would be picked up in Device Mode
# We're using a default port and enforcing a special user
'instance': plugins.NotifyLametric,
# Our expected url(privacy=True) startswith() response:
'privacy_url': 'lametric://root:8...2@192.168.0.5/',
}),
('lametric://{}@192.168.0.4:8000/'.format(UUID4), {
# Everything is okay; this would be picked up in Device Mode
# Port is enforced
'instance': plugins.NotifyLametric,
# Our expected url(privacy=True) startswith() response:
'privacy_url': 'lametric://8...2@192.168.0.4:8000/',
}),
('lametric://{}@192.168.0.5/'.format(UUID4), {
# Everything is okay; this would be picked up in Device Mode
'instance': plugins.NotifyLametric,
# Our expected url(privacy=True) startswith() response:
'privacy_url': 'lametric://8...2@192.168.0.5/',
}),
('lametrics://{}@192.168.0.6/?mode=device'.format(UUID4), {
# Everything is okay; Device mode forced
'instance': plugins.NotifyLametric,
# Our expected url(privacy=True) startswith() response:
'privacy_url': 'lametrics://8...2@192.168.0.6/',
}),
# Support Native URL (with Access Token Argument)
('https://developer.lametric.com/api/v1/dev/widget/update/'
'com.lametric.ABCD123/1?token={}=='.format('D' * 88), {
# Everything is okay; Device mode forced
'instance': plugins.NotifyLametric,
# Our expected url(privacy=True) startswith() response:
'privacy_url': 'lametric://D...=@A...3/1/',
}),
('lametric://192.168.2.8/?mode=device&apikey=abc123', {
# Everything is okay; Device mode forced
'instance': plugins.NotifyLametric,
# Our expected url(privacy=True) startswith() response:
'privacy_url': 'lametric://a...3@192.168.2.8/',
}),
('lametrics://{}==@com.lametric.941c51dff3135bd87aa72db9d855dd50/'
'?mode=cloud&app_ver=2'.format('A' * 88), {
# Everything is okay; Cloud mode forced
# We gracefully strip off the com.lametric. part as well
# We also set an application version of 2
'instance': plugins.NotifyLametric,
# Our expected url(privacy=True) startswith() response:
'privacy_url': 'lametric://A...=@9...0/',
}),
('lametrics://{}==@com.lametric.941c51dff3135bd87aa72db9d855dd50/'
'?app_ver=invalid'.format('A' * 88), {
# We set invalid app version
'instance': TypeError,
}),
# our lametric object initialized via argument
('lametric://?app=com.lametric.941c51dff3135bd87aa72db9d855dd50&token={}=='
'&mode=cloud'.format('B' * 88), {
# Everything is okay; Cloud mode forced
'instance': plugins.NotifyLametric,
# Our expected url(privacy=True) startswith() response:
'privacy_url': 'lametric://B...=@9...0/',
}),
('lametrics://{}==@abcd/?mode=cloud&sound=knock&icon_type=info'
'&priority=critical&cycles=10'.format('C' * 88), {
# Cloud mode forced, sound, icon_type, and priority not supported
# with cloud mode so warnings are created
'instance': plugins.NotifyLametric,
# Our expected url(privacy=True) startswith() response:
'privacy_url': 'lametric://C...=@a...d/',
}),
('lametrics://{}@192.168.0.7/?mode=invalid'.format(UUID4), {
# Invalid Mode
'instance': TypeError,
}),
('lametrics://{}@192.168.0.6/?sound=alarm1'.format(UUID4), {
# Device mode with sound set to alarm1
'instance': plugins.NotifyLametric,
}),
('lametrics://{}@192.168.0.7/?sound=bike'.format(UUID4), {
# Device mode with sound set to bicycle using alias
'instance': plugins.NotifyLametric,
# Bike is an alias,
'url_matches': r'sound=bicycle',
}),
('lametrics://{}@192.168.0.8/?sound=invalid!'.format(UUID4), {
# Invalid sounds just produce warnings... object still loads
'instance': plugins.NotifyLametric,
}),
('lametrics://{}@192.168.0.9/?icon_type=alert'.format(UUID4), {
# Icon Type Changed
'instance': plugins.NotifyLametric,
# icon=alert exists somewhere on our generated URL
'url_matches': r'icon_type=alert',
}),
('lametrics://{}@192.168.0.10/?icon_type=invalid'.format(UUID4), {
# Invalid icon types just produce warnings... object still loads
'instance': plugins.NotifyLametric,
}),
('lametric://{}@192.168.1.1/?priority=warning'.format(UUID4), {
# Priority changed
'instance': plugins.NotifyLametric,
}),
('lametrics://{}@192.168.1.2/?priority=invalid'.format(UUID4), {
# Invalid priority just produce warnings... object still loads
'instance': plugins.NotifyLametric,
}),
('lametric://{}@192.168.1.2/?icon=230'.format(UUID4), {
# Our custom icon by it's ID
'instance': plugins.NotifyLametric,
}),
('lametrics://{}@192.168.1.2/?icon=#230'.format(UUID4), {
# Our custom icon by it's ID; the hashtag at the front is ignored
'instance': plugins.NotifyLametric,
}),
('lametric://{}@192.168.1.2/?icon=Heart'.format(UUID4), {
# Our custom icon; the hashtag at the front is ignored
'instance': plugins.NotifyLametric,
}),
('lametric://{}@192.168.1.2/?icon=#'.format(UUID4), {
# a hashtag and nothing else
'instance': plugins.NotifyLametric,
}),
('lametric://{}@192.168.1.2/?icon=#%20%20%20'.format(UUID4), {
# a hashtag and some spaces
'instance': plugins.NotifyLametric,
}),
('lametric://{}@192.168.1.3/?cycles=2'.format(UUID4), {
# Cycles changed
'instance': plugins.NotifyLametric,
}),
('lametric://{}@192.168.1.4/?cycles=-1'.format(UUID4), {
# Cycles changed (out of range)
'instance': plugins.NotifyLametric,
}),
('lametrics://{}@192.168.1.5/?cycles=invalid'.format(UUID4), {
# Invalid priority just produce warnings... object still loads
'instance': plugins.NotifyLametric,
}),
('lametric://{}@example.com/'.format(UUID4), {
'instance': plugins.NotifyLametric,
# force a failure
'response': False,
'requests_response_code': requests.codes.internal_server_error,
# Our expected url(privacy=True) startswith() response:
'privacy_url': 'lametric://8...2@example.com/',
}),
('lametrics://{}@example.ca/'.format(UUID4), {
'instance': plugins.NotifyLametric,
# throw a bizzare code forcing us to fail to look it up
'response': False,
'requests_response_code': 999,
# Our expected url(privacy=True) startswith() response:
'privacy_url': 'lametrics://8...2@example.ca/',
}),
('lametrics://{}@example.net/'.format(UUID4), {
'instance': plugins.NotifyLametric,
# Throws a series of connection and transfer exceptions when this flag
# is set and tests that we gracfully handle them
'test_requests_exceptions': True,
}),
)
def test_plugin_lametric_urls():
"""
NotifyLametric() Apprise URLs
"""
# Run our general tests
AppriseURLTester(tests=apprise_url_tests).run_all()
def test_plugin_lametric_edge_cases():
"""
NotifyLametric() Edge Cases
"""
# Initializes the plugin with an invalid API Key
with pytest.raises(TypeError):
plugins.NotifyLametric(apikey=None, mode="device")
# Initializes the plugin with an invalid Client Secret
with pytest.raises(TypeError):
plugins.NotifyLametric(client_id='valid', secret=None, mode="cloud")

View File

@ -25,10 +25,22 @@
import os
import six
import sys
import mock
import apprise
try:
# Python v3.4+
from importlib import reload
except ImportError:
try:
# Python v3.0-v3.3
from imp import reload
except ImportError:
# Python v2.7
pass
# Disable logging for a cleaner testing output
import logging
logging.disable(logging.CRITICAL)
@ -37,9 +49,9 @@ logging.disable(logging.CRITICAL)
@mock.patch('subprocess.Popen')
@mock.patch('platform.system')
@mock.patch('platform.mac_ver')
def test_macosx_plugin(mock_macver, mock_system, mock_popen, tmpdir):
def test_plugin_macosx_general(mock_macver, mock_system, mock_popen, tmpdir):
"""
API: NotifyMacOSX Plugin()
NotifyMacOSX() General Checks
"""
@ -48,8 +60,6 @@ def test_macosx_plugin(mock_macver, mock_system, mock_popen, tmpdir):
script.write('')
# Give execute bit
os.chmod(str(script), 0o755)
# Point our object to our new temporary existing file
apprise.plugins.NotifyMacOSX.notify_path = str(script)
mock_cmd_response = mock.Mock()
# Set a successful response
@ -60,10 +70,18 @@ def test_macosx_plugin(mock_macver, mock_system, mock_popen, tmpdir):
mock_macver.return_value = ('10.8', ('', '', ''), '')
mock_popen.return_value = mock_cmd_response
# Ensure our enviroment is loaded with this configuration
reload(sys.modules['apprise.plugins.NotifyMacOSX'])
reload(sys.modules['apprise.plugins'])
reload(sys.modules['apprise.Apprise'])
reload(sys.modules['apprise'])
# Point our object to our new temporary existing file
apprise.plugins.NotifyMacOSX.notify_path = str(script)
obj = apprise.Apprise.instantiate(
'macosx://_/?image=True', suppress_exceptions=False)
assert isinstance(obj, apprise.plugins.NotifyMacOSX) is True
assert obj._enabled is True
# Test url() call
assert isinstance(obj.url(), six.string_types) is True
@ -79,14 +97,12 @@ def test_macosx_plugin(mock_macver, mock_system, mock_popen, tmpdir):
obj = apprise.Apprise.instantiate(
'macosx://_/?image=True', suppress_exceptions=False)
assert isinstance(obj, apprise.plugins.NotifyMacOSX) is True
assert obj._enabled is True
assert obj.notify(title='title', body='body',
notify_type=apprise.NotifyType.INFO) is True
obj = apprise.Apprise.instantiate(
'macosx://_/?image=False', suppress_exceptions=False)
assert isinstance(obj, apprise.plugins.NotifyMacOSX) is True
assert obj._enabled is True
assert isinstance(obj.url(), six.string_types) is True
assert obj.notify(title='title', body='body',
notify_type=apprise.NotifyType.INFO) is True
@ -95,64 +111,82 @@ def test_macosx_plugin(mock_macver, mock_system, mock_popen, tmpdir):
obj = apprise.Apprise.instantiate(
'macosx://_/?sound=default', suppress_exceptions=False)
assert isinstance(obj, apprise.plugins.NotifyMacOSX) is True
assert obj._enabled is True
assert obj.sound == 'default'
assert isinstance(obj.url(), six.string_types) is True
assert obj.notify(title='title', body='body',
notify_type=apprise.NotifyType.INFO) is True
# Now test cases where our environment isn't set up correctly
# In this case we'll simulate a situation where our binary isn't
# executable
# If our binary is inacccessible (or not executable), we can
# no longer send our notifications
os.chmod(str(script), 0o644)
obj = apprise.Apprise.instantiate(
'macosx://_/?sound=default', suppress_exceptions=False)
assert isinstance(obj, apprise.plugins.NotifyMacOSX) is True
assert obj._enabled is False
assert obj.notify(title='title', body='body',
notify_type=apprise.NotifyType.INFO) is False
# Restore permission
os.chmod(str(script), 0o755)
# Test case where we simply aren't on a mac
mock_system.return_value = 'Linux'
obj = apprise.Apprise.instantiate(
'macosx://_/?sound=default', suppress_exceptions=False)
assert isinstance(obj, apprise.plugins.NotifyMacOSX) is True
assert obj._enabled is False
# But now let's disrupt the path location
obj.notify_path = 'invalid_missing-file'
assert not os.path.isfile(obj.notify_path)
assert obj.notify(title='title', body='body',
notify_type=apprise.NotifyType.INFO) is False
# Restore mac environment
mock_system.return_value = 'Darwin'
# Now we must be Mac OS v10.8 or higher... Test cases where we aren't
mock_macver.return_value = ('10.7', ('', '', ''), '')
obj = apprise.Apprise.instantiate(
'macosx://_/?sound=default', suppress_exceptions=False)
assert isinstance(obj, apprise.plugins.NotifyMacOSX) is True
assert obj._enabled is False
assert obj.notify(title='title', body='body',
notify_type=apprise.NotifyType.INFO) is False
# Restore valid mac environment
mock_macver.return_value = ('10.8', ('', '', ''), '')
mock_macver.return_value = ('9.12', ('', '', ''), '')
obj = apprise.Apprise.instantiate(
'macosx://_/?sound=default', suppress_exceptions=False)
assert isinstance(obj, apprise.plugins.NotifyMacOSX) is True
assert obj._enabled is False
assert obj.notify(title='title', body='body',
notify_type=apprise.NotifyType.INFO) is False
# Restore valid mac environment
mock_macver.return_value = ('10.8', ('', '', ''), '')
# Test cases where the script just flat out fails
mock_cmd_response.returncode = 1
obj = apprise.Apprise.instantiate(
'macosx://', suppress_exceptions=False)
assert isinstance(obj, apprise.plugins.NotifyMacOSX) is True
assert obj._enabled is True
assert obj.notify(title='title', body='body',
notify_type=apprise.NotifyType.INFO) is False
# Restore script return value
mock_cmd_response.returncode = 1
mock_cmd_response.returncode = 0
# Test case where we simply aren't on a mac
mock_system.return_value = 'Linux'
reload(sys.modules['apprise.plugins.NotifyMacOSX'])
reload(sys.modules['apprise.plugins'])
reload(sys.modules['apprise.Apprise'])
reload(sys.modules['apprise'])
# Point our object to our new temporary existing file
apprise.plugins.NotifyMacOSX.notify_path = str(script)
# Our object is disabled
obj = apprise.Apprise.instantiate(
'macosx://_/?sound=default', suppress_exceptions=False)
assert obj is None
# Restore mac environment
mock_system.return_value = 'Darwin'
# Now we must be Mac OS v10.8 or higher...
mock_macver.return_value = ('10.7', ('', '', ''), '')
reload(sys.modules['apprise.plugins.NotifyMacOSX'])
reload(sys.modules['apprise.plugins'])
reload(sys.modules['apprise.Apprise'])
reload(sys.modules['apprise'])
# Point our object to our new temporary existing file
apprise.plugins.NotifyMacOSX.notify_path = str(script)
obj = apprise.Apprise.instantiate(
'macosx://_/?sound=default', suppress_exceptions=False)
assert obj is None
# A newer environment to test edge case where this is tested
mock_macver.return_value = ('9.12', ('', '', ''), '')
reload(sys.modules['apprise.plugins.NotifyMacOSX'])
reload(sys.modules['apprise.plugins'])
reload(sys.modules['apprise.Apprise'])
reload(sys.modules['apprise'])
# Point our object to our new temporary existing file
apprise.plugins.NotifyMacOSX.notify_path = str(script)
# This is just to test that the the minor (in this case .12)
# is only weighed with respect to the major number as wel
# with respect to the versioning
obj = apprise.Apprise.instantiate(
'macosx://_/?sound=default', suppress_exceptions=False)
assert obj is None

View File

@ -27,6 +27,7 @@ import os
import sys
import mock
import requests
from helpers import AppriseURLTester
from apprise import plugins
from apprise import Apprise
from apprise import AppriseAttachment
@ -39,11 +40,145 @@ logging.disable(logging.CRITICAL)
# Attachment Directory
TEST_VAR_DIR = os.path.join(os.path.dirname(__file__), 'var')
# Our Testing URLs
apprise_url_tests = (
('mailgun://', {
'instance': TypeError,
}),
('mailgun://:@/', {
'instance': TypeError,
}),
# No Token specified
('mailgun://user@localhost.localdomain', {
'instance': TypeError,
}),
# Token is valid, but no user name specified
('mailgun://localhost.localdomain/{}-{}-{}'.format(
'a' * 32, 'b' * 8, 'c' * 8), {
'instance': TypeError,
}),
# Invalid from email address
('mailgun://!@localhost.localdomain/{}-{}-{}'.format(
'a' * 32, 'b' * 8, 'c' * 8), {
'instance': TypeError,
}),
# No To email address, but everything else is valid
('mailgun://user@localhost.localdomain/{}-{}-{}'.format(
'a' * 32, 'b' * 8, 'c' * 8), {
'instance': plugins.NotifyMailgun,
}),
('mailgun://user@localhost.localdomain/{}-{}-{}?format=markdown'.format(
'a' * 32, 'b' * 8, 'c' * 8), {
'instance': plugins.NotifyMailgun,
}),
('mailgun://user@localhost.localdomain/{}-{}-{}?format=html'.format(
'a' * 32, 'b' * 8, 'c' * 8), {
'instance': plugins.NotifyMailgun,
}),
('mailgun://user@localhost.localdomain/{}-{}-{}?format=text'.format(
'a' * 32, 'b' * 8, 'c' * 8), {
'instance': plugins.NotifyMailgun,
}),
# valid url with region specified (case insensitve)
('mailgun://user@localhost.localdomain/{}-{}-{}?region=uS'.format(
'a' * 32, 'b' * 8, 'c' * 8), {
'instance': plugins.NotifyMailgun,
}),
# valid url with region specified (case insensitve)
('mailgun://user@localhost.localdomain/{}-{}-{}?region=EU'.format(
'a' * 32, 'b' * 8, 'c' * 8), {
'instance': plugins.NotifyMailgun,
}),
# invalid url with region specified (case insensitve)
('mailgun://user@localhost.localdomain/{}-{}-{}?region=invalid'.format(
'a' * 32, 'b' * 8, 'c' * 8), {
'instance': TypeError,
}),
# headers
('mailgun://user@localhost.localdomain/{}-{}-{}'
'?+X-Customer-Campaign-ID=Apprise'.format(
'a' * 32, 'b' * 8, 'c' * 8), {
'instance': plugins.NotifyMailgun,
}),
# template tokens
('mailgun://user@localhost.localdomain/{}-{}-{}'
'?:name=Chris&:status=admin'.format(
'a' * 32, 'b' * 8, 'c' * 8), {
'instance': plugins.NotifyMailgun,
}),
# bcc and cc
('mailgun://user@localhost.localdomain/{}-{}-{}'
'?bcc=user@example.com&cc=user2@example.com'.format(
'a' * 32, 'b' * 8, 'c' * 8), {
'instance': plugins.NotifyMailgun,
}),
# One To Email address
('mailgun://user@localhost.localdomain/{}-{}-{}/test@example.com'.format(
'a' * 32, 'b' * 8, 'c' * 8), {
'instance': plugins.NotifyMailgun,
}),
('mailgun://user@localhost.localdomain/'
'{}-{}-{}?to=test@example.com'.format(
'a' * 32, 'b' * 8, 'c' * 8), {
'instance': plugins.NotifyMailgun}),
# One To Email address, a from name specified too
('mailgun://user@localhost.localdomain/{}-{}-{}/'
'test@example.com?name="Frodo"'.format(
'a' * 32, 'b' * 8, 'c' * 8), {
'instance': plugins.NotifyMailgun}),
# Invalid 'To' Email address
('mailgun://user@localhost.localdomain/{}-{}-{}/invalid'.format(
'a' * 32, 'b' * 8, 'c' * 8), {
'instance': plugins.NotifyMailgun,
# Expected notify() response
'notify_response': False,
}),
# Multiple 'To', 'Cc', and 'Bcc' addresses (with invalid ones)
('mailgun://user@example.com/{}-{}-{}/{}?bcc={}&cc={}'.format(
'a' * 32, 'b' * 8, 'c' * 8,
'/'.join(('user1@example.com', 'invalid', 'User2:user2@example.com')),
','.join(('user3@example.com', 'i@v', 'User1:user1@example.com')),
','.join(('user4@example.com', 'g@r@b', 'Da:user5@example.com'))), {
'instance': plugins.NotifyMailgun,
}),
('mailgun://user@localhost.localdomain/{}-{}-{}'.format(
'a' * 32, 'b' * 8, 'c' * 8), {
'instance': plugins.NotifyMailgun,
# force a failure
'response': False,
'requests_response_code': requests.codes.internal_server_error,
}),
('mailgun://user@localhost.localdomain/{}-{}-{}'.format(
'a' * 32, 'b' * 8, 'c' * 8), {
'instance': plugins.NotifyMailgun,
# throw a bizzare code forcing us to fail to look it up
'response': False,
'requests_response_code': 999,
}),
('mailgun://user@localhost.localdomain/{}-{}-{}'.format(
'a' * 32, 'b' * 8, 'c' * 8), {
'instance': plugins.NotifyMailgun,
# Throws a series of connection and transfer exceptions when this flag
# is set and tests that we gracfully handle them
'test_requests_exceptions': True,
}),
)
def test_plugin_mailgun_urls():
"""
NotifyMailgun() Apprise URLs
"""
# Run our general tests
AppriseURLTester(tests=apprise_url_tests).run_all()
@mock.patch('requests.post')
def test_notify_mailgun_plugin_attachments(mock_post):
def test_plugin_mailgun_attachments(mock_post):
"""
API: NotifyMailgun() Attachments
NotifyMailgun() Attachments
"""
# Disable Throttling to speed testing

View File

@ -30,17 +30,161 @@ import pytest
from apprise import plugins
from apprise import AppriseAsset
from json import dumps
from helpers import AppriseURLTester
# Disable logging for a cleaner testing output
import logging
logging.disable(logging.CRITICAL)
# Our Testing URLs
apprise_url_tests = (
##################################
# NotifyMatrix
##################################
('matrix://', {
'instance': None,
}),
('matrixs://', {
'instance': None,
}),
('matrix://localhost?mode=off', {
# treats it as a anonymous user to register
'instance': plugins.NotifyMatrix,
# response is false because we have nothing to notify
'response': False,
}),
('matrix://localhost', {
# response is TypeError because we'll try to initialize as
# a t2bot and fail (localhost is too short of a api key)
'instance': TypeError
}),
('matrix://user:pass@localhost/#room1/#room2/#room3', {
'instance': plugins.NotifyMatrix,
'response': False,
'requests_response_code': requests.codes.internal_server_error,
}),
('matrix://user:pass@localhost/#room1/#room2/!room1', {
'instance': plugins.NotifyMatrix,
# throw a bizzare code forcing us to fail to look it up
'response': False,
'requests_response_code': 999,
}),
('matrix://user:pass@localhost:1234/#room', {
'instance': plugins.NotifyMatrix,
# Throws a series of connection and transfer exceptions when this flag
# is set and tests that we gracfully handle them
'test_requests_exceptions': True,
# Our expected url(privacy=True) startswith() response:
'privacy_url': 'matrix://user:****@localhost:1234/',
}),
# Matrix supports webhooks too; the following tests this now:
('matrix://user:token@localhost?mode=matrix&format=text', {
# user and token correctly specified with webhook
'instance': plugins.NotifyMatrix,
'response': False,
}),
('matrix://user:token@localhost?mode=matrix&format=html', {
# user and token correctly specified with webhook
'instance': plugins.NotifyMatrix,
}),
('matrix://user:token@localhost?mode=slack&format=text', {
# user and token correctly specified with webhook
'instance': plugins.NotifyMatrix,
}),
('matrixs://user:token@localhost?mode=SLACK&format=markdown', {
# user and token specified; slack webhook still detected
# despite uppercase characters
'instance': plugins.NotifyMatrix,
}),
('matrix://user@localhost?mode=SLACK&format=markdown&token=mytoken', {
# user and token specified; slack webhook still detected
# despite uppercase characters; token also set on URL as arg
'instance': plugins.NotifyMatrix,
}),
('matrix://_?mode=t2bot&token={}'.format('b' * 64), {
# Testing t2bot initialization and setting the password using the
# token directive
'instance': plugins.NotifyMatrix,
# Our expected url(privacy=True) startswith() response:
'privacy_url': 'matrix://b...b/',
}),
# Image Reference
('matrixs://user:token@localhost?mode=slack&format=markdown&image=True', {
# user and token specified; image set to True
'instance': plugins.NotifyMatrix,
}),
('matrixs://user:token@localhost?mode=slack&format=markdown&image=False', {
# user and token specified; image set to True
'instance': plugins.NotifyMatrix,
}),
('matrixs://user@{}?mode=t2bot&format=markdown&image=True'
.format('a' * 64), {
# user and token specified; image set to True
'instance': plugins.NotifyMatrix}),
('matrix://user@{}?mode=t2bot&format=html&image=False'
.format('z' * 64), {
# user and token specified; image set to True
'instance': plugins.NotifyMatrix}),
# This will default to t2bot because no targets were specified and no
# password
('matrixs://{}'.format('c' * 64), {
'instance': plugins.NotifyMatrix,
# Throws a series of connection and transfer exceptions when this flag
# is set and tests that we gracfully handle them
'test_requests_exceptions': True,
}),
# Test Native URL
('https://webhooks.t2bot.io/api/v1/matrix/hook/{}/'.format('d' * 64), {
# user and token specified; image set to True
'instance': plugins.NotifyMatrix,
}),
('matrix://user:token@localhost?mode=On', {
# invalid webhook specified (unexpected boolean)
'instance': TypeError,
}),
('matrix://token@localhost/?mode=Matrix', {
'instance': plugins.NotifyMatrix,
'response': False,
'requests_response_code': requests.codes.internal_server_error,
}),
('matrix://user:token@localhost/mode=matrix', {
'instance': plugins.NotifyMatrix,
# throw a bizzare code forcing us to fail to look it up
'response': False,
'requests_response_code': 999,
}),
('matrix://token@localhost:8080/?mode=slack', {
'instance': plugins.NotifyMatrix,
# Throws a series of connection and transfer exceptions when this flag
# is set and tests that we gracfully handle them
'test_requests_exceptions': True,
}),
('matrix://{}/?mode=t2bot'.format('b' * 64), {
'instance': plugins.NotifyMatrix,
# Throws a series of connection and transfer exceptions when this flag
# is set and tests that we gracfully handle them
'test_requests_exceptions': True,
}),
)
def test_plugin_matrix_urls():
"""
NotifyMatrix() Apprise URLs
"""
# Run our general tests
AppriseURLTester(tests=apprise_url_tests).run_all()
@mock.patch('requests.get')
@mock.patch('requests.post')
def test_notify_matrix_plugin_general(mock_post, mock_get):
def test_plugin_matrix_general(mock_post, mock_get):
"""
API: NotifyMatrix() General Tests
NotifyMatrix() General Tests
"""
# Disable Throttling to speed testing
@ -193,9 +337,9 @@ def test_notify_matrix_plugin_general(mock_post, mock_get):
@mock.patch('requests.get')
@mock.patch('requests.post')
def test_notify_matrix_plugin_fetch(mock_post, mock_get):
def test_plugin_matrix_fetch(mock_post, mock_get):
"""
API: NotifyMatrix() Server Fetch/API Tests
NotifyMatrix() Server Fetch/API Tests
"""
# Disable Throttling to speed testing
@ -299,9 +443,9 @@ def test_notify_matrix_plugin_fetch(mock_post, mock_get):
@mock.patch('requests.get')
@mock.patch('requests.post')
def test_notify_matrix_plugin_auth(mock_post, mock_get):
def test_plugin_matrix_auth(mock_post, mock_get):
"""
API: NotifyMatrix() Server Authentication
NotifyMatrix() Server Authentication
"""
# Disable Throttling to speed testing
@ -395,9 +539,9 @@ def test_notify_matrix_plugin_auth(mock_post, mock_get):
@mock.patch('requests.get')
@mock.patch('requests.post')
def test_notify_matrix_plugin_rooms(mock_post, mock_get):
def test_plugin_matrix_rooms(mock_post, mock_get):
"""
API: NotifyMatrix() Room Testing
NotifyMatrix() Room Testing
"""
# Disable Throttling to speed testing
@ -582,9 +726,9 @@ def test_notify_matrix_plugin_rooms(mock_post, mock_get):
assert obj._room_id('#abc123:localhost') is None
def test_notify_matrix_url_parsing():
def test_plugin_matrix_url_parsing():
"""
API: NotifyMatrix() URL Testing
NotifyMatrix() URL Testing
"""
result = plugins.NotifyMatrix.parse_url(
@ -604,9 +748,9 @@ def test_notify_matrix_url_parsing():
@mock.patch('requests.get')
@mock.patch('requests.post')
def test_notify_matrix_plugin_image_errors(mock_post, mock_get):
def test_plugin_matrix_image_errors(mock_post, mock_get):
"""
API: NotifyMatrix() Image Error Handling
NotifyMatrix() Image Error Handling
"""

View File

@ -0,0 +1,137 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2021 Chris Caron <lead2gold@gmail.com>
# All rights reserved.
#
# This code is licensed under the MIT License.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files(the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions :
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
import pytest
import requests
from apprise import plugins
from helpers import AppriseURLTester
# Disable logging for a cleaner testing output
import logging
logging.disable(logging.CRITICAL)
# Our Testing URLs
apprise_url_tests = (
('mmost://', {
'instance': None,
}),
('mmosts://', {
'instance': None,
}),
('mmost://:@/', {
'instance': None,
}),
('mmosts://localhost', {
# Thrown because there was no webhook id specified
'instance': TypeError,
}),
('mmost://localhost/3ccdd113474722377935511fc85d3dd4', {
'instance': plugins.NotifyMattermost,
}),
('mmost://user@localhost/3ccdd113474722377935511fc85d3dd4?channel=test', {
'instance': plugins.NotifyMattermost,
}),
('mmost://user@localhost/3ccdd113474722377935511fc85d3dd4?to=test', {
'instance': plugins.NotifyMattermost,
# Our expected url(privacy=True) startswith() response:
'privacy_url': 'mmost://user@localhost/3...4/',
}),
('mmost://localhost/3ccdd113474722377935511fc85d3dd4'
'?to=test&image=True', {
'instance': plugins.NotifyMattermost}),
('mmost://localhost/3ccdd113474722377935511fc85d3dd4' \
'?to=test&image=False', {
'instance': plugins.NotifyMattermost}),
('mmost://localhost/3ccdd113474722377935511fc85d3dd4' \
'?to=test&image=True', {
'instance': plugins.NotifyMattermost,
# don't include an image by default
'include_image': False}),
('mmost://localhost:8080/3ccdd113474722377935511fc85d3dd4', {
'instance': plugins.NotifyMattermost,
# Our expected url(privacy=True) startswith() response:
'privacy_url': 'mmost://localhost:8080/3...4/',
}),
('mmost://localhost:8080/3ccdd113474722377935511fc85d3dd4', {
'instance': plugins.NotifyMattermost,
}),
('mmost://localhost:invalid-port/3ccdd113474722377935511fc85d3dd4', {
'instance': None,
}),
('mmosts://localhost/3ccdd113474722377935511fc85d3dd4', {
'instance': plugins.NotifyMattermost,
}),
# Test our paths
('mmosts://localhost/a/path/3ccdd113474722377935511fc85d3dd4', {
'instance': plugins.NotifyMattermost,
}),
('mmosts://localhost/////3ccdd113474722377935511fc85d3dd4///', {
'instance': plugins.NotifyMattermost,
}),
('mmost://localhost/3ccdd113474722377935511fc85d3dd4', {
'instance': plugins.NotifyMattermost,
# force a failure
'response': False,
'requests_response_code': requests.codes.internal_server_error,
}),
('mmost://localhost/3ccdd113474722377935511fc85d3dd4', {
'instance': plugins.NotifyMattermost,
# throw a bizzare code forcing us to fail to look it up
'response': False,
'requests_response_code': 999,
}),
('mmost://localhost/3ccdd113474722377935511fc85d3dd4', {
'instance': plugins.NotifyMattermost,
# Throws a series of connection and transfer exceptions when this flag
# is set and tests that we gracfully handle them
'test_requests_exceptions': True,
}),
)
def test_plugin_mattermost_urls():
"""
NotifyMattermost() Apprise URLs
"""
# Run our general tests
AppriseURLTester(tests=apprise_url_tests).run_all()
def test_plugin_mattermost_edge_cases():
"""
NotifyMattermost() Edge Cases
"""
# Disable Throttling to speed testing
plugins.NotifyBase.request_rate_per_sec = 0
# Invalid Authorization Token
with pytest.raises(TypeError):
plugins.NotifyMattermost(None)
with pytest.raises(TypeError):
plugins.NotifyMattermost(" ")

View File

@ -0,0 +1,128 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2021 Chris Caron <lead2gold@gmail.com>
# All rights reserved.
#
# This code is licensed under the MIT License.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files(the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions :
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
import mock
import pytest
import requests
from apprise import plugins
from helpers import AppriseURLTester
# Disable logging for a cleaner testing output
import logging
logging.disable(logging.CRITICAL)
# Our Testing URLs
apprise_url_tests = (
('msgbird://', {
# No hostname/apikey specified
'instance': TypeError,
}),
('msgbird://{}/abcd'.format('a' * 25), {
# invalid characters in source phone number
'instance': TypeError,
}),
('msgbird://{}/123'.format('a' * 25), {
# invalid source phone number
'instance': TypeError,
}),
('msgbird://{}/15551232000'.format('a' * 25), {
# target phone number becomes who we text too; all is good
'instance': plugins.NotifyMessageBird,
# Our expected url(privacy=True) startswith() response:
'privacy_url': 'msgbird://a...a/15551232000',
}),
('msgbird://{}/15551232000/abcd'.format('a' * 25), {
# valid credentials
'instance': plugins.NotifyMessageBird,
# Since there are no targets specified we expect a False return on
# send()
'notify_response': False,
}),
('msgbird://{}/15551232000/123'.format('a' * 25), {
# valid credentials
'instance': plugins.NotifyMessageBird,
# Since there are no targets specified we expect a False return on
# send()
'notify_response': False,
}),
('msgbird://{}/?from=15551233000&to=15551232000'.format('a' * 25), {
# reference to to= and from=
'instance': plugins.NotifyMessageBird,
}),
('msgbird://{}/15551232000'.format('a' * 25), {
'instance': plugins.NotifyMessageBird,
# force a failure
'response': False,
'requests_response_code': requests.codes.internal_server_error,
}),
('msgbird://{}/15551232000'.format('a' * 25), {
'instance': plugins.NotifyMessageBird,
# throw a bizzare code forcing us to fail to look it up
'response': False,
'requests_response_code': 999,
}),
('msgbird://{}/15551232000'.format('a' * 25), {
'instance': plugins.NotifyMessageBird,
# Throws a series of connection and transfer exceptions when this flag
# is set and tests that we gracfully handle them
'test_requests_exceptions': True,
}),
)
def test_plugin_messagebird_urls():
"""
NotifyTemplate() Apprise URLs
"""
# Run our general tests
AppriseURLTester(tests=apprise_url_tests).run_all()
@mock.patch('requests.post')
def test_plugin_messagebird_edge_cases(mock_post):
"""
NotifyMessageBird() Edge Cases
"""
# Disable Throttling to speed testing
plugins.NotifyBase.request_rate_per_sec = 0
# Prepare our response
response = requests.Request()
response.status_code = requests.codes.ok
# Prepare Mock
mock_post.return_value = response
# Initialize some generic (but valid) tokens
# authkey = '{}'.format('a' * 24)
source = '+1 (555) 123-3456'
# No apikey specified
with pytest.raises(TypeError):
plugins.NotifyMessageBird(apikey=None, source=source)
with pytest.raises(TypeError):
plugins.NotifyMessageBird(apikey=" ", source=source)

View File

@ -28,107 +28,35 @@ import re
import sys
import ssl
import six
import os
import pytest
# Rebuild our Apprise environment
import apprise
try:
# Python v3.4+
from importlib import reload
except ImportError:
try:
# Python v3.0-v3.3
from imp import reload
except ImportError:
# Python v2.7
pass
# Disable logging for a cleaner testing output
import logging
logging.disable(logging.CRITICAL)
@pytest.mark.skipif('paho' not in sys.modules, reason="requires paho-mqtt")
def test_paho_mqtt_plugin_import_error(tmpdir):
@pytest.mark.skipif(
'paho' in sys.modules,
reason="Requires that cryptography NOT be installed")
@mock.patch('requests.post')
def test_plugin_mqtt_paho_import_error(mock_post):
"""
API: NotifyMQTT Plugin() Import Error
NotifyFCM Cryptography loading failure
"""
# This is a really confusing test case; it can probably be done better,
# but this was all I could come up with. Effectively Apprise is will
# still work flawlessly without the paho dependancy. Since
# paho is actually required to be installed to run these unit tests
# we need to do some hacky tricks into fooling our test cases that the
# package isn't available.
# So we create a temporary directory called paho (simulating the
# library itself) and writing an __init__.py in it that does nothing
# but throw an ImportError exception (simulating that the library
# isn't found).
suite = tmpdir.mkdir("paho")
suite.join("__init__.py").write('')
module_name = 'paho'
suite.join("{}.py".format(module_name)).write('raise ImportError()')
# The second part of the test is to update our PYTHON_PATH to look
# into this new directory first (before looking where the actual
# valid paths are). This will allow us to override 'JUST' the sleekxmpp
# path.
# Update our path to point to our new test suite
sys.path.insert(0, str(suite))
# We need to remove the sleekxmpp modules that have already been loaded
# in memory otherwise they'll just be used instead. Python is smart and
# won't go try and reload everything again if it doesn't have to.
for name in list(sys.modules.keys()):
if name.startswith('{}.'.format(module_name)):
del sys.modules[name]
del sys.modules[module_name]
# The following libraries need to be reloaded to prevent
# TypeError: super(type, obj): obj must be an instance or subtype of type
# This is better explained in this StackOverflow post:
# https://stackoverflow.com/questions/31363311/\
# any-way-to-manually-fix-operation-of-\
# super-after-ipython-reload-avoiding-ty
#
reload(sys.modules['apprise.plugins.NotifyMQTT'])
reload(sys.modules['apprise.plugins'])
reload(sys.modules['apprise.Apprise'])
reload(sys.modules['apprise'])
# This tests that Apprise still works without sleekxmpp.
# XMPP objects can still be instantiated in these cases.
obj = apprise.Apprise.instantiate('mqtt://user:pass@localhost/my/topic')
assert isinstance(obj, apprise.plugins.NotifyMQTT)
# We can still retrieve our url back to us
assert obj.url().startswith('mqtt://user:pass@localhost/my/topic')
# Notifications are not possible
assert obj.notify(body="test") is False
# Tidy-up / restore things to how they were
# Remove our garbage library
os.unlink(str(suite.join("{}.py".format(module_name))))
# Remove our custom entry into the path
sys.path.remove(str(suite))
# Reload the libraries we care about
reload(sys.modules['apprise.plugins.NotifyMQTT'])
reload(sys.modules['apprise.plugins'])
reload(sys.modules['apprise.Apprise'])
reload(sys.modules['apprise'])
# without the library, the object can't be instantiated
obj = apprise.Apprise.instantiate(
'mqtt://user:pass@localhost/my/topic')
assert obj is None
@pytest.mark.skipif(
'paho' not in sys.modules, reason="requires paho-mqtt")
'paho' not in sys.modules, reason="Requires paho-mqtt")
@mock.patch('paho.mqtt.client.Client')
def test_mqtt_plugin(mock_client):
def test_plugin_mqtt_general(mock_client):
"""
API: NotifyMQTT Plugin()
NotifyMQTT() General Checks
"""
# Speed up request rate for testing

151
test/test_plugin_msg91.py Normal file
View File

@ -0,0 +1,151 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2021 Chris Caron <lead2gold@gmail.com>
# All rights reserved.
#
# This code is licensed under the MIT License.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files(the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions :
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
import mock
import pytest
import requests
from apprise import plugins
from helpers import AppriseURLTester
# Disable logging for a cleaner testing output
import logging
logging.disable(logging.CRITICAL)
# Our Testing URLs
apprise_url_tests = (
('msg91://', {
# No hostname/authkey specified
'instance': TypeError,
}),
('msg91://-', {
# Invalid AuthKey
'instance': TypeError,
}),
('msg91://{}'.format('a' * 23), {
# valid AuthKey
'instance': plugins.NotifyMSG91,
# Since there are no targets specified we expect a False return on
# send()
'notify_response': False,
}),
('msg91://{}/123'.format('a' * 23), {
# invalid phone number
'instance': plugins.NotifyMSG91,
# Since there are no targets specified we expect a False return on
# send()
'notify_response': False,
}),
('msg91://{}/abcd'.format('a' * 23), {
# No number to notify
'instance': plugins.NotifyMSG91,
# Since there are no targets specified we expect a False return on
# send()
'notify_response': False,
}),
('msg91://{}/15551232000/?country=invalid'.format('a' * 23), {
# invalid country
'instance': TypeError,
}),
('msg91://{}/15551232000/?country=99'.format('a' * 23), {
# invalid country
'instance': TypeError,
}),
('msg91://{}/15551232000/?route=invalid'.format('a' * 23), {
# invalid route
'instance': TypeError,
}),
('msg91://{}/15551232000/?route=99'.format('a' * 23), {
# invalid route
'instance': TypeError,
}),
('msg91://{}/15551232000'.format('a' * 23), {
# a valid message
'instance': plugins.NotifyMSG91,
# Our expected url(privacy=True) startswith() response:
'privacy_url': 'msg91://a...a/15551232000',
}),
('msg91://{}/?to=15551232000'.format('a' * 23), {
# a valid message
'instance': plugins.NotifyMSG91,
}),
('msg91://{}/15551232000?country=91&route=1'.format('a' * 23), {
# using phone no with no target - we text ourselves in
# this case
'instance': plugins.NotifyMSG91,
}),
('msg91://{}/15551232000'.format('a' * 23), {
# use get args to acomplish the same thing
'instance': plugins.NotifyMSG91,
}),
('msg91://{}/15551232000'.format('a' * 23), {
'instance': plugins.NotifyMSG91,
# throw a bizzare code forcing us to fail to look it up
'response': False,
'requests_response_code': 999,
}),
('msg91://{}/15551232000'.format('a' * 23), {
'instance': plugins.NotifyMSG91,
# Throws a series of connection and transfer exceptions when this flag
# is set and tests that we gracfully handle them
'test_requests_exceptions': True,
}),
)
def test_plugin_msg91_urls():
"""
NotifyMSG91() Apprise URLs
"""
# Run our general tests
AppriseURLTester(tests=apprise_url_tests).run_all()
@mock.patch('requests.post')
def test_plugin_msg91_edge_cases(mock_post):
"""
NotifyMSG91() Edge Cases
"""
# Disable Throttling to speed testing
plugins.NotifyBase.request_rate_per_sec = 0
# Prepare our response
response = requests.Request()
response.status_code = requests.codes.ok
# Prepare Mock
mock_post.return_value = response
# Initialize some generic (but valid) tokens
# authkey = '{}'.format('a' * 24)
target = '+1 (555) 123-3456'
# No authkey specified
with pytest.raises(TypeError):
plugins.NotifyMSG91(authkey=None, targets=target)
with pytest.raises(TypeError):
plugins.NotifyMSG91(authkey=" ", targets=target)

View File

@ -31,16 +31,152 @@ from apprise import Apprise
from apprise import AppriseConfig
from apprise import plugins
from apprise import NotifyType
from helpers import AppriseURLTester
# Disable logging for a cleaner testing output
import logging
logging.disable(logging.CRITICAL)
# a test UUID we can use
UUID4 = '8b799edf-6f98-4d3a-9be7-2862fb4e5752'
# Our Testing URLs
apprise_url_tests = (
##################################
# NotifyMSTeams
##################################
('msteams://', {
# First API Token not specified
'instance': TypeError,
}),
('msteams://:@/', {
# We don't have strict host checking on for msteams, so this URL
# actually becomes parseable and :@ becomes a hostname.
# The below errors because a second token wasn't found
'instance': TypeError,
}),
('msteams://{}'.format(UUID4), {
# Just half of one token 1 provided
'instance': TypeError,
}),
('msteams://{}@{}/'.format(UUID4, UUID4), {
# Just 1 tokens provided
'instance': TypeError,
}),
('msteams://{}@{}/{}'.format(UUID4, UUID4, 'a' * 32), {
# Just 2 tokens provided
'instance': TypeError,
}),
('msteams://{}@{}/{}/{}?t1'.format(UUID4, UUID4, 'b' * 32, UUID4), {
# All tokens provided - we're good
'instance': plugins.NotifyMSTeams,
}),
# Support native URLs
('https://outlook.office.com/webhook/{}@{}/IncomingWebhook/{}/{}'
.format(UUID4, UUID4, 'k' * 32, UUID4), {
# All tokens provided - we're good
'instance': plugins.NotifyMSTeams,
# Our expected url(privacy=True) startswith() response (v1 format)
'privacy_url': 'msteams://8...2/k...k/8...2/'}),
# Support New Native URLs
('https://myteam.webhook.office.com/webhookb2/{}@{}/IncomingWebhook/{}/{}'
.format(UUID4, UUID4, 'm' * 32, UUID4), {
# All tokens provided - we're good
'instance': plugins.NotifyMSTeams,
# Our expected url(privacy=True) startswith() response (v2 format):
'privacy_url': 'msteams://myteam/8...2/m...m/8...2/'}),
# Legacy URL Formatting
('msteams://{}@{}/{}/{}?t2'.format(UUID4, UUID4, 'c' * 32, UUID4), {
# All tokens provided - we're good
'instance': plugins.NotifyMSTeams,
# don't include an image by default
'include_image': False,
}),
# Legacy URL Formatting
('msteams://{}@{}/{}/{}?image=No'.format(UUID4, UUID4, 'd' * 32, UUID4), {
# All tokens provided - we're good no image
'instance': plugins.NotifyMSTeams,
# Our expected url(privacy=True) startswith() response:
'privacy_url': 'msteams://8...2/d...d/8...2/',
}),
# New 2021 URL formatting
('msteams://apprise/{}@{}/{}/{}'.format(
UUID4, UUID4, 'e' * 32, UUID4), {
# All tokens provided - we're good no image
'instance': plugins.NotifyMSTeams,
# Our expected url(privacy=True) startswith() response:
'privacy_url': 'msteams://apprise/8...2/e...e/8...2/',
}),
# New 2021 URL formatting; support team= argument
('msteams://{}@{}/{}/{}?team=teamname'.format(
UUID4, UUID4, 'f' * 32, UUID4), {
# All tokens provided - we're good no image
'instance': plugins.NotifyMSTeams,
# Our expected url(privacy=True) startswith() response:
'privacy_url': 'msteams://teamname/8...2/f...f/8...2/',
}),
# New 2021 URL formatting (forcing v1)
('msteams://apprise/{}@{}/{}/{}?version=1'.format(
UUID4, UUID4, 'e' * 32, UUID4), {
# All tokens provided - we're good
'instance': plugins.NotifyMSTeams,
# Our expected url(privacy=True) startswith() response:
'privacy_url': 'msteams://8...2/e...e/8...2/',
}),
# Invalid versioning
('msteams://apprise/{}@{}/{}/{}?version=999'.format(
UUID4, UUID4, 'e' * 32, UUID4), {
# invalid version
'instance': TypeError,
}),
('msteams://apprise/{}@{}/{}/{}?version=invalid'.format(
UUID4, UUID4, 'e' * 32, UUID4), {
# invalid version
'instance': TypeError,
}),
('msteams://{}@{}/{}/{}?tx'.format(UUID4, UUID4, 'x' * 32, UUID4), {
'instance': plugins.NotifyMSTeams,
# force a failure
'response': False,
'requests_response_code': requests.codes.internal_server_error,
}),
('msteams://{}@{}/{}/{}?ty'.format(UUID4, UUID4, 'y' * 32, UUID4), {
'instance': plugins.NotifyMSTeams,
# throw a bizzare code forcing us to fail to look it up
'response': False,
'requests_response_code': 999,
}),
('msteams://{}@{}/{}/{}?tz'.format(UUID4, UUID4, 'z' * 32, UUID4), {
'instance': plugins.NotifyMSTeams,
# Throws a series of connection and transfer exceptions when this flag
# is set and tests that we gracfully handle them
'test_requests_exceptions': True,
}),
)
def test_plugin_msteams_urls():
"""
NotifyMSTeams() Apprise URLs
"""
# Run our general tests
AppriseURLTester(tests=apprise_url_tests).run_all()
@mock.patch('requests.post')
def test_msteams_templating(mock_post, tmpdir):
def test_plugin_msteams_templating(mock_post, tmpdir):
"""
API: NotifyMSTeams() Templating
NotifyMSTeams() Templating
"""
# Disable Throttling to speed testing
@ -249,7 +385,7 @@ def test_msteams_templating(mock_post, tmpdir):
@mock.patch('requests.post')
def test_msteams_yaml_config(mock_post, tmpdir):
"""
API: NotifyMSTeams() YAML Configuration Entries
NotifyMSTeams() YAML Configuration Entries
"""
@ -515,9 +651,9 @@ def test_msteams_yaml_config(mock_post, tmpdir):
assert len(cfg[0]) == 0
def test_notify_msteams_plugin():
def test_plugin_msteams_edge_cases():
"""
API: NotifyMSTeams() Extra Checks
NotifyMSTeams() Edge Cases
"""
# Initializes the plugin with an invalid token

163
test/test_plugin_nexmo.py Normal file
View File

@ -0,0 +1,163 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2021 Chris Caron <lead2gold@gmail.com>
# All rights reserved.
#
# This code is licensed under the MIT License.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files(the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions :
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
import mock
import pytest
import requests
from json import dumps
from apprise import plugins
from helpers import AppriseURLTester
# Disable logging for a cleaner testing output
import logging
logging.disable(logging.CRITICAL)
# Our Testing URLs
apprise_url_tests = (
('nexmo://', {
# No API Key specified
'instance': TypeError,
}),
('nexmo://:@/', {
# invalid Auth key
'instance': TypeError,
}),
('nexmo://AC{}@12345678'.format('a' * 8), {
# Just a key provided
'instance': TypeError,
}),
('nexmo://AC{}:{}@{}'.format('a' * 8, 'b' * 16, '3' * 9), {
# key and secret provided and from but invalid from no
'instance': TypeError,
}),
('nexmo://AC{}:{}@{}/?ttl=0'.format('b' * 8, 'c' * 16, '3' * 11), {
# Invalid ttl defined
'instance': TypeError,
}),
('nexmo://AC{}:{}@{}'.format('d' * 8, 'e' * 16, 'a' * 11), {
# Invalid source number
'instance': TypeError,
}),
('nexmo://AC{}:{}@{}/123/{}/abcd/'.format(
'f' * 8, 'g' * 16, '3' * 11, '9' * 15), {
# valid everything but target numbers
'instance': plugins.NotifyNexmo,
# Our expected url(privacy=True) startswith() response:
'privacy_url': 'nexmo://A...f:****@',
}),
('nexmo://AC{}:{}@{}'.format('h' * 8, 'i' * 16, '5' * 11), {
# using phone no with no target - we text ourselves in
# this case
'instance': plugins.NotifyNexmo,
}),
('nexmo://_?key=AC{}&secret={}&from={}'.format(
'a' * 8, 'b' * 16, '5' * 11), {
# use get args to acomplish the same thing
'instance': plugins.NotifyNexmo,
}),
('nexmo://_?key=AC{}&secret={}&source={}'.format(
'a' * 8, 'b' * 16, '5' * 11), {
# use get args to acomplish the same thing (use source instead of from)
'instance': plugins.NotifyNexmo,
}),
('nexmo://_?key=AC{}&secret={}&from={}&to={}'.format(
'a' * 8, 'b' * 16, '5' * 11, '7' * 13), {
# use to=
'instance': plugins.NotifyNexmo,
}),
('nexmo://AC{}:{}@{}'.format('a' * 8, 'b' * 16, '6' * 11), {
'instance': plugins.NotifyNexmo,
# throw a bizzare code forcing us to fail to look it up
'response': False,
'requests_response_code': 999,
}),
('nexmo://AC{}:{}@{}'.format('a' * 8, 'b' * 16, '6' * 11), {
'instance': plugins.NotifyNexmo,
# Throws a series of connection and transfer exceptions when this flag
# is set and tests that we gracfully handle them
'test_requests_exceptions': True,
}),
)
def test_plugin_nexmo_urls():
"""
NotifyNexmo() Apprise URLs
"""
# Run our general tests
AppriseURLTester(tests=apprise_url_tests).run_all()
@mock.patch('requests.post')
def test_plugin_nexmo_edge_cases(mock_post):
"""
NotifyNexmo() Edge Cases
"""
# Disable Throttling to speed testing
plugins.NotifyBase.request_rate_per_sec = 0
# Prepare our response
response = requests.Request()
response.status_code = requests.codes.ok
# Prepare Mock
mock_post.return_value = response
# Initialize some generic (but valid) tokens
apikey = 'AC{}'.format('b' * 8)
secret = '{}'.format('b' * 16)
source = '+1 (555) 123-3456'
# No apikey specified
with pytest.raises(TypeError):
plugins.NotifyNexmo(apikey=None, secret=secret, source=source)
with pytest.raises(TypeError):
plugins.NotifyNexmo(apikey=" ", secret=secret, source=source)
# No secret specified
with pytest.raises(TypeError):
plugins.NotifyNexmo(apikey=apikey, secret=None, source=source)
with pytest.raises(TypeError):
plugins.NotifyNexmo(apikey=apikey, secret=" ", source=source)
# a error response
response.status_code = 400
response.content = dumps({
'code': 21211,
'message': "The 'To' number +1234567 is not a valid phone number.",
})
mock_post.return_value = response
# Initialize our object
obj = plugins.NotifyNexmo(
apikey=apikey, secret=secret, source=source)
# We will fail with the above error code
assert obj.notify('title', 'body', 'info') is False

View File

@ -0,0 +1,161 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2020 Chris Caron <lead2gold@gmail.com>
# All rights reserved.
#
# This code is licensed under the MIT License.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files(the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions :
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
import six
import mock
import requests
from apprise import plugins
from helpers import AppriseURLTester
# Disable logging for a cleaner testing output
import logging
logging.disable(logging.CRITICAL)
apprise_url_tests = (
##################################
# NotifyNextcloud
##################################
('ncloud://:@/', {
'instance': None,
}),
('ncloud://', {
'instance': None,
}),
('nclouds://', {
# No hostname
'instance': None,
}),
('ncloud://localhost', {
# No user specified
'instance': TypeError,
}),
('ncloud://user@localhost?to=user1,user2&version=invalid', {
# An invalid version was specified
'instance': TypeError,
}),
('ncloud://user@localhost?to=user1,user2&version=0', {
# An invalid version was specified
'instance': TypeError,
}),
('ncloud://user@localhost?to=user1,user2&version=-23', {
# An invalid version was specified
'instance': TypeError,
}),
('ncloud://localhost/admin', {
'instance': plugins.NotifyNextcloud,
}),
('ncloud://user@localhost/admin', {
'instance': plugins.NotifyNextcloud,
}),
('ncloud://user@localhost?to=user1,user2', {
'instance': plugins.NotifyNextcloud,
}),
('ncloud://user@localhost?to=user1,user2&version=20', {
'instance': plugins.NotifyNextcloud,
}),
('ncloud://user@localhost?to=user1,user2&version=21', {
'instance': plugins.NotifyNextcloud,
}),
('ncloud://user:pass@localhost/user1/user2', {
'instance': plugins.NotifyNextcloud,
# Our expected url(privacy=True) startswith() response:
'privacy_url': 'ncloud://user:****@localhost/user1/user2',
}),
('ncloud://user:pass@localhost:8080/admin', {
'instance': plugins.NotifyNextcloud,
}),
('nclouds://user:pass@localhost/admin', {
'instance': plugins.NotifyNextcloud,
# Our expected url(privacy=True) startswith() response:
'privacy_url': 'nclouds://user:****@localhost/admin',
}),
('nclouds://user:pass@localhost:8080/admin/', {
'instance': plugins.NotifyNextcloud,
}),
('ncloud://localhost:8080/admin?-HeaderKey=HeaderValue', {
'instance': plugins.NotifyNextcloud,
}),
('ncloud://user:pass@localhost:8081/admin', {
'instance': plugins.NotifyNextcloud,
# force a failure
'response': False,
'requests_response_code': requests.codes.internal_server_error,
}),
('ncloud://user:pass@localhost:8082/admin', {
'instance': plugins.NotifyNextcloud,
# throw a bizzare code forcing us to fail to look it up
'response': False,
'requests_response_code': 999,
}),
('ncloud://user:pass@localhost:8083/user1/user2/user3', {
'instance': plugins.NotifyNextcloud,
# Throws a series of connection and transfer exceptions when this flag
# is set and tests that we gracfully handle them
'test_requests_exceptions': True,
}),
)
def test_plugin_nextcloud_urls():
"""
NotifyNextcloud() Apprise URLs
"""
# Run our general tests
AppriseURLTester(tests=apprise_url_tests).run_all()
@mock.patch('requests.post')
def test_plugin_nextcloud_edge_cases(mock_post):
"""
NotifyNextcloud() Edge Cases
"""
# Disable Throttling to speed testing
plugins.NotifyBase.request_rate_per_sec = 0
# A response
robj = mock.Mock()
robj.content = ''
robj.status_code = requests.codes.ok
# Prepare Mock
mock_post.return_value = robj
# Variation Initializations
obj = plugins.NotifyNextcloud(
host="localhost", user="admin", password="pass", targets="user")
assert isinstance(obj, plugins.NotifyNextcloud) is True
assert isinstance(obj.url(), six.string_types) is True
# An empty body
assert obj.send(body="") is True
assert 'data' in mock_post.call_args_list[0][1]
assert 'shortMessage' in mock_post.call_args_list[0][1]['data']
# The longMessage argument is not set
assert 'longMessage' not in mock_post.call_args_list[0][1]['data']

145
test/test_plugin_notica.py Normal file
View File

@ -0,0 +1,145 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2021 Chris Caron <lead2gold@gmail.com>
# All rights reserved.
#
# This code is licensed under the MIT License.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files(the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions :
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
import requests
from apprise import plugins
from helpers import AppriseURLTester
# Disable logging for a cleaner testing output
import logging
logging.disable(logging.CRITICAL)
# Our Testing URLs
apprise_url_tests = (
('notica://', {
'instance': TypeError,
}),
('notica://:@/', {
'instance': TypeError,
}),
# Native URL
('https://notica.us/?%s' % ('z' * 6), {
'instance': plugins.NotifyNotica,
# Our expected url(privacy=True) startswith() response:
'privacy_url': 'notica://z...z/',
}),
# Native URL with additional arguments
('https://notica.us/?%s&overflow=upstream' % ('z' * 6), {
'instance': plugins.NotifyNotica,
# Our expected url(privacy=True) startswith() response:
'privacy_url': 'notica://z...z/',
}),
# Token specified
('notica://%s' % ('a' * 6), {
'instance': plugins.NotifyNotica,
# Our expected url(privacy=True) startswith() response:
'privacy_url': 'notica://a...a/',
}),
# Self-Hosted configuration
('notica://localhost/%s' % ('b' * 6), {
'instance': plugins.NotifyNotica,
}),
('notica://user@localhost/%s' % ('c' * 6), {
'instance': plugins.NotifyNotica,
}),
('notica://user:pass@localhost/%s/' % ('d' * 6), {
'instance': plugins.NotifyNotica,
# Our expected url(privacy=True) startswith() response:
'privacy_url': 'notica://user:****@localhost/d...d',
}),
('notica://user:pass@localhost/a/path/%s/' % ('r' * 6), {
'instance': plugins.NotifyNotica,
# Our expected url(privacy=True) startswith() response:
'privacy_url': 'notica://user:****@localhost/a/path/r...r',
}),
('notica://localhost:8080/%s' % ('a' * 6), {
'instance': plugins.NotifyNotica,
}),
('notica://user:pass@localhost:8080/%s' % ('b' * 6), {
'instance': plugins.NotifyNotica,
}),
('noticas://localhost/%s' % ('j' * 6), {
'instance': plugins.NotifyNotica,
'privacy_url': 'noticas://localhost/j...j',
}),
('noticas://user:pass@localhost/%s' % ('e' * 6), {
'instance': plugins.NotifyNotica,
# Our expected url(privacy=True) startswith() response:
'privacy_url': 'noticas://user:****@localhost/e...e',
}),
('noticas://localhost:8080/path/%s' % ('5' * 6), {
'instance': plugins.NotifyNotica,
'privacy_url': 'noticas://localhost:8080/path/5...5',
}),
('noticas://user:pass@localhost:8080/%s' % ('6' * 6), {
'instance': plugins.NotifyNotica,
}),
('notica://%s' % ('b' * 6), {
'instance': plugins.NotifyNotica,
# don't include an image by default
'include_image': False,
}),
# Test Header overrides
('notica://localhost:8080//%s/?+HeaderKey=HeaderValue' % ('7' * 6), {
'instance': plugins.NotifyNotica,
}),
# Test Depricated Header overrides
('notica://localhost:8080//%s/?-HeaderKey=HeaderValue' % ('7' * 6), {
'instance': plugins.NotifyNotica,
}),
('notica://%s' % ('c' * 6), {
'instance': plugins.NotifyNotica,
# force a failure
'response': False,
'requests_response_code': requests.codes.internal_server_error,
}),
('notica://%s' % ('d' * 7), {
'instance': plugins.NotifyNotica,
# throw a bizzare code forcing us to fail to look it up
'response': False,
'requests_response_code': 999,
}),
('notica://%s' % ('e' * 8), {
'instance': plugins.NotifyNotica,
# Throws a series of connection and transfer exceptions when this flag
# is set and tests that we gracfully handle them
'test_requests_exceptions': True,
}),
)
def test_plugin_notica_urls():
"""
NotifyNotica() Apprise URLs
"""
# Run our general tests
AppriseURLTester(tests=apprise_url_tests).run_all()

View File

@ -0,0 +1,121 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2021 Chris Caron <lead2gold@gmail.com>
# All rights reserved.
#
# This code is licensed under the MIT License.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files(the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions :
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
import requests
from apprise import plugins
from helpers import AppriseURLTester
# Disable logging for a cleaner testing output
import logging
logging.disable(logging.CRITICAL)
# Our Testing URLs
apprise_url_tests = (
('notifico://', {
'instance': TypeError,
}),
('notifico://:@/', {
'instance': TypeError,
}),
('notifico://1234', {
# Just a project id provided (no message token)
'instance': TypeError,
}),
('notifico://abcd/ckhrjW8w672m6HG', {
# an invalid project id provided
'instance': TypeError,
}),
('notifico://1234/ckhrjW8w672m6HG', {
# A project id and message hook provided
'instance': plugins.NotifyNotifico,
}),
('notifico://1234/ckhrjW8w672m6HG?prefix=no', {
# Disable our prefix
'instance': plugins.NotifyNotifico,
}),
('notifico://1234/ckhrjW8w672m6HG?color=yes', {
'instance': plugins.NotifyNotifico,
'notify_type': 'info',
}),
('notifico://1234/ckhrjW8w672m6HG?color=yes', {
'instance': plugins.NotifyNotifico,
'notify_type': 'success',
}),
('notifico://1234/ckhrjW8w672m6HG?color=yes', {
'instance': plugins.NotifyNotifico,
'notify_type': 'warning',
}),
('notifico://1234/ckhrjW8w672m6HG?color=yes', {
'instance': plugins.NotifyNotifico,
'notify_type': 'failure',
}),
('notifico://1234/ckhrjW8w672m6HG?color=yes', {
'instance': plugins.NotifyNotifico,
'notify_type': 'invalid',
}),
('notifico://1234/ckhrjW8w672m6HG?color=no', {
# Test our color flag by having it set to off
'instance': plugins.NotifyNotifico,
# Our expected url(privacy=True) startswith() response:
'privacy_url': 'notifico://1...4/c...G',
}),
# Support Native URLs
('https://n.tkte.ch/h/2144/uJmKaBW9WFk42miB146ci3Kj', {
'instance': plugins.NotifyNotifico,
}),
('notifico://1234/ckhrjW8w672m6HG', {
'instance': plugins.NotifyNotifico,
# don't include an image by default
'include_image': False,
}),
('notifico://1234/ckhrjW8w672m6HG', {
'instance': plugins.NotifyNotifico,
# force a failure
'response': False,
'requests_response_code': requests.codes.internal_server_error,
}),
('notifico://1234/ckhrjW8w672m6HG', {
'instance': plugins.NotifyNotifico,
# throw a bizzare code forcing us to fail to look it up
'response': False,
'requests_response_code': 999,
}),
('notifico://1234/ckhrjW8w672m6HG', {
'instance': plugins.NotifyNotifico,
# Throws a series of connection and transfer exceptions when this flag
# is set and tests that we gracfully handle them
'test_requests_exceptions': True,
}),
)
def test_plugin_notifico_urls():
"""
NotifyNotifico() Apprise URLs
"""
# Run our general tests
AppriseURLTester(tests=apprise_url_tests).run_all()

View File

@ -32,6 +32,7 @@ from datetime import datetime
from json import dumps
from apprise import Apprise
from apprise import plugins
from helpers import AppriseURLTester
# Disable logging for a cleaner testing output
import logging
@ -40,11 +41,151 @@ logging.disable(logging.CRITICAL)
# Attachment Directory
TEST_VAR_DIR = os.path.join(os.path.dirname(__file__), 'var')
# Our Testing URLs
apprise_url_tests = (
##################################
# NotifyOffice365
##################################
('o365://', {
# Missing tenant, client_id, secret, and targets!
'instance': TypeError,
}),
('o365://:@/', {
# invalid url
'instance': TypeError,
}),
('o365://{tenant}:{aid}/{cid}/{secret}/{targets}'.format(
# invalid tenant
tenant=',',
cid='ab-cd-ef-gh',
aid='user@example.com',
secret='abcd/123/3343/@jack/test',
targets='/'.join(['email1@test.ca'])), {
# We're valid and good to go
'instance': TypeError,
}),
('o365://{tenant}:{aid}/{cid}/{secret}/{targets}'.format(
tenant='tenant',
# invalid client id
cid='ab.',
aid='user@example.com',
secret='abcd/123/3343/@jack/test',
targets='/'.join(['email1@test.ca'])), {
# We're valid and good to go
'instance': TypeError,
}),
('o365://{tenant}:{aid}/{cid}/{secret}/{targets}'.format(
tenant='tenant',
cid='ab-cd-ef-gh',
aid='user@example.com',
secret='abcd/123/3343/@jack/test',
targets='/'.join(['email1@test.ca'])), {
# We're valid and good to go
'instance': plugins.NotifyOffice365,
# Test what happens if a batch send fails to return a messageCount
'requests_response_text': {
'expires_in': 2000,
'access_token': 'abcd1234',
},
# Our expected url(privacy=True) startswith() response:
'privacy_url': 'o365://t...t:user@example.com/a...h/' \
'****/email1%40test.ca/'}),
# test our arguments
('o365://_/?oauth_id={cid}&oauth_secret={secret}&tenant={tenant}'
'&to={targets}&from={aid}'.format(
tenant='tenant',
cid='ab-cd-ef-gh',
aid='user@example.com',
secret='abcd/123/3343/@jack/test',
targets='email1@test.ca'),
{
# We're valid and good to go
'instance': plugins.NotifyOffice365,
# Test what happens if a batch send fails to return a messageCount
'requests_response_text': {
'expires_in': 2000,
'access_token': 'abcd1234',
},
# Our expected url(privacy=True) startswith() response:
'privacy_url': 'o365://t...t:user@example.com/a...h/' \
'****/email1%40test.ca/'}),
# Test invalid JSON (no tenant defaults to email domain)
('o365://{tenant}:{aid}/{cid}/{secret}/{targets}'.format(
tenant='tenant',
cid='ab-cd-ef-gh',
aid='user@example.com',
secret='abcd/123/3343/@jack/test',
targets='/'.join(['email1@test.ca'])), {
# We're valid and good to go
'instance': plugins.NotifyOffice365,
# invalid JSON response
'requests_response_text': '{',
'notify_response': False,
}),
# No Targets specified
('o365://{tenant}:{aid}/{cid}/{secret}'.format(
tenant='tenant',
cid='ab-cd-ef-gh',
aid='user@example.com',
secret='abcd/123/3343/@jack/test'), {
# We're valid and good to go
'instance': plugins.NotifyOffice365,
# There were no targets to notify; so we use our own email
'requests_response_text': {
'expires_in': 2000,
'access_token': 'abcd1234',
},
}),
('o365://{tenant}:{aid}/{cid}/{secret}/{targets}'.format(
tenant='tenant',
cid='zz-zz-zz-zz',
aid='user@example.com',
secret='abcd/abc/dcba/@john/test',
targets='/'.join(['email1@test.ca'])), {
'instance': plugins.NotifyOffice365,
# throw a bizzare code forcing us to fail to look it up
'response': False,
'requests_response_code': 999,
}),
('o365://{tenant}:{aid}/{cid}/{secret}/{targets}'.format(
tenant='tenant',
cid='01-12-23-34',
aid='user@example.com',
secret='abcd/321/4321/@test/test',
targets='/'.join(['email1@test.ca'])), {
'instance': plugins.NotifyOffice365,
# Throws a series of connection and transfer exceptions when this flag
# is set and tests that we gracfully handle them
'test_requests_exceptions': True,
}),
)
def test_plugin_office365_urls():
"""
NotifyOffice365() Apprise URLs
"""
# Run our general tests
AppriseURLTester(tests=apprise_url_tests).run_all()
@mock.patch('requests.post')
def test_office365_general(mock_post):
def test_plugin_office365_general(mock_post):
"""
API: NotifyOffice365 Testing
NotifyOffice365() General Testing
"""
# Disable Throttling to speed testing
@ -160,9 +301,9 @@ def test_office365_general(mock_post):
@mock.patch('requests.post')
def test_office365_authentication(mock_post):
def test_plugin_office365_authentication(mock_post):
"""
API: NotifyOffice365 Authentication Testing
NotifyOffice365() Authentication Testing
"""
# Disable Throttling to speed testing

View File

@ -0,0 +1,125 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2021 Chris Caron <lead2gold@gmail.com>
# All rights reserved.
#
# This code is licensed under the MIT License.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files(the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions :
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
from apprise import plugins
from helpers import AppriseURLTester
# Disable logging for a cleaner testing output
import logging
logging.disable(logging.CRITICAL)
# Our Testing URLs
apprise_url_tests = (
('onesignal://', {
# We failed to identify any valid authentication
'instance': TypeError,
}),
('onesignal://:@/', {
# We failed to identify any valid authentication
'instance': TypeError,
}),
('onesignal://apikey/', {
# no app id specified
'instance': TypeError,
}),
('onesignal://appid@%20%20/', {
# invalid apikey
'instance': TypeError,
}),
('onesignal://appid@apikey/playerid/?lang=X', {
# invalid language id (must be 2 characters)
'instance': TypeError,
}),
('onesignal://appid@apikey/', {
# No targets specified; we will initialize but not notify anything
'instance': plugins.NotifyOneSignal,
'notify_response': False,
}),
('onesignal://appid@apikey/playerid', {
# Valid playerid
'instance': plugins.NotifyOneSignal,
'privacy_url': 'onesignal://a...d@a...y/playerid',
}),
('onesignal://appid@apikey/player', {
# Valid player id
'instance': plugins.NotifyOneSignal,
# don't include an image by default
'include_image': False,
}),
('onesignal://appid@apikey/@user?image=no', {
# Valid userid, no image
'instance': plugins.NotifyOneSignal,
}),
('onesignal://appid@apikey/user@email.com/#seg/player/@user/%20/a', {
# Valid email, valid playerid, valid user, invalid entry (%20),
# and too short of an entry (a)
'instance': plugins.NotifyOneSignal,
}),
('onesignal://appid@apikey?to=#segment,playerid', {
# Test to=
'instance': plugins.NotifyOneSignal,
}),
('onesignal://appid@apikey/#segment/@user/?batch=yes', {
# Test batch=
'instance': plugins.NotifyOneSignal,
}),
('onesignal://appid@apikey/#segment/@user/?batch=no', {
# Test batch=
'instance': plugins.NotifyOneSignal,
}),
('onesignal://templateid:appid@apikey/playerid', {
# Test Template ID
'instance': plugins.NotifyOneSignal,
}),
('onesignal://appid@apikey/playerid/?lang=es&subtitle=Sub', {
# Test Language and Subtitle Over-ride
'instance': plugins.NotifyOneSignal,
}),
('onesignal://?apikey=abc&template=tp&app=123&to=playerid', {
# Test Kwargs
'instance': plugins.NotifyOneSignal,
}),
('onesignal://appid@apikey/#segment/playerid/', {
'instance': plugins.NotifyOneSignal,
# throw a bizzare code forcing us to fail to look it up
'response': False,
'requests_response_code': 999,
}),
('onesignal://appid@apikey/#segment/playerid/', {
'instance': plugins.NotifyOneSignal,
# Throws a series of connection and transfer exceptions when this flag
# is set and tests that we gracfully handle them
'test_requests_exceptions': True,
}),
)
def test_plugin_onesignal_urls():
"""
NotifyOneSignal() Apprise URLs
"""
# Run our general tests
AppriseURLTester(tests=apprise_url_tests).run_all()

View File

@ -0,0 +1,135 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2021 Chris Caron <lead2gold@gmail.com>
# All rights reserved.
#
# This code is licensed under the MIT License.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files(the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions :
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
from apprise import plugins
from helpers import AppriseURLTester
# Disable logging for a cleaner testing output
import logging
logging.disable(logging.CRITICAL)
# a test UUID we can use
UUID4 = '8b799edf-6f98-4d3a-9be7-2862fb4e5752'
# Our Testing URLs
apprise_url_tests = (
('opsgenie://', {
# We failed to identify any valid authentication
'instance': TypeError,
}),
('opsgenie://:@/', {
# We failed to identify any valid authentication
'instance': TypeError,
}),
('opsgenie://%20%20/', {
# invalid apikey specified
'instance': TypeError,
}),
('opsgenie://apikey/user/?region=xx', {
# invalid region id
'instance': TypeError,
}),
('opsgenie://apikey/', {
# No targets specified; this is allowed
'instance': plugins.NotifyOpsgenie,
}),
('opsgenie://apikey/user', {
# Valid user
'instance': plugins.NotifyOpsgenie,
'privacy_url': 'opsgenie://a...y/%40user',
}),
('opsgenie://apikey/@user?region=eu', {
# European Region
'instance': plugins.NotifyOpsgenie,
}),
('opsgenie://apikey/@user?entity=A%20Entity', {
# Assign an entity
'instance': plugins.NotifyOpsgenie,
}),
('opsgenie://apikey/@user?alias=An%20Alias', {
# Assign an alias
'instance': plugins.NotifyOpsgenie,
}),
('opsgenie://apikey/@user?priority=p3', {
# Assign our priority
'instance': plugins.NotifyOpsgenie,
}),
('opsgenie://apikey/?tags=comma,separated', {
# Test our our 'tags' (tag is reserved in Apprise) but not 'tags'
# Also test the fact we do not need to define a target
'instance': plugins.NotifyOpsgenie,
}),
('opsgenie://apikey/@user?priority=invalid', {
# Invalid priority (loads using default)
'instance': plugins.NotifyOpsgenie,
}),
('opsgenie://apikey/user@email.com/#team/*sche/^esc/%20/a', {
# Valid user (email), valid schedule, Escalated ID,
# an invalid entry (%20), and too short of an entry (a)
'instance': plugins.NotifyOpsgenie,
}),
('opsgenie://apikey/{}/@{}/#{}/*{}/^{}/'.format(
UUID4, UUID4, UUID4, UUID4, UUID4), {
# similar to the above, except we use the UUID's
'instance': plugins.NotifyOpsgenie,
}),
('opsgenie://apikey?to=#team,user&+key=value&+type=override', {
# Test to= and details (key/value pair) also override 'type'
'instance': plugins.NotifyOpsgenie,
}),
('opsgenie://apikey/#team/@user/?batch=yes', {
# Test batch=
'instance': plugins.NotifyOpsgenie,
}),
('opsgenie://apikey/#team/@user/?batch=no', {
# Test batch=
'instance': plugins.NotifyOpsgenie,
}),
('opsgenie://?apikey=abc&to=user', {
# Test Kwargs
'instance': plugins.NotifyOpsgenie,
}),
('opsgenie://apikey/#team/user/', {
'instance': plugins.NotifyOpsgenie,
# throw a bizzare code forcing us to fail to look it up
'response': False,
'requests_response_code': 999,
}),
('opsgenie://apikey/#topic1/device/', {
'instance': plugins.NotifyOpsgenie,
# Throws a series of connection and transfer exceptions when this flag
# is set and tests that we gracfully handle them
'test_requests_exceptions': True,
}),
)
def test_plugin_opsgenie_urls():
"""
NotifyOpsgenie() Apprise URLs
"""
# Run our general tests
AppriseURLTester(tests=apprise_url_tests).run_all()

View File

@ -0,0 +1,101 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2021 Chris Caron <lead2gold@gmail.com>
# All rights reserved.
#
# This code is licensed under the MIT License.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files(the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions :
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
import requests
from apprise import plugins
from helpers import AppriseURLTester
# Disable logging for a cleaner testing output
import logging
logging.disable(logging.CRITICAL)
# Our Testing URLs
apprise_url_tests = (
('parsep://', {
'instance': None,
}),
# API Key + bad url
('parsep://:@/', {
'instance': None,
}),
# APIkey; no app_id or master_key
('parsep://%s' % ('a' * 32), {
'instance': TypeError,
}),
# APIkey; no master_key
('parsep://app_id@%s' % ('a' * 32), {
'instance': TypeError,
}),
# APIkey; no app_id
('parseps://:master_key@%s' % ('a' * 32), {
'instance': TypeError,
}),
# app_id + master_key (using arguments=)
('parseps://localhost?app_id=%s&master_key=%s' % ('a' * 32, 'd' * 32), {
'instance': plugins.NotifyParsePlatform,
# Our expected url(privacy=True) startswith() response:
'privacy_url': 'parseps://a...a:d...d@localhost',
}),
# Set a device id + custom port
('parsep://app_id:master_key@localhost:8080?device=ios', {
'instance': plugins.NotifyParsePlatform,
}),
# invalid device id
('parsep://app_id:master_key@localhost?device=invalid', {
'instance': TypeError,
}),
# Normal Query
('parseps://app_id:master_key@localhost', {
'instance': plugins.NotifyParsePlatform,
}),
('parseps://app_id:master_key@localhost', {
'instance': plugins.NotifyParsePlatform,
# force a failure
'response': False,
'requests_response_code': requests.codes.internal_server_error,
}),
('parseps://app_id:master_key@localhost', {
'instance': plugins.NotifyParsePlatform,
# throw a bizzare code forcing us to fail to look it up
'response': False,
'requests_response_code': 999,
}),
('parseps://app_id:master_key@localhost', {
'instance': plugins.NotifyParsePlatform,
# Throws a series of connection and transfer exceptions when this flag
# is set and tests that we gracfully handle them
'test_requests_exceptions': True,
}),
)
def test_plugin_parse_platform_urls():
"""
NotifyParsePlatform() Apprise URLs
"""
# Run our general tests
AppriseURLTester(tests=apprise_url_tests).run_all()

View File

@ -0,0 +1,93 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2021 Chris Caron <lead2gold@gmail.com>
# All rights reserved.
#
# This code is licensed under the MIT License.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files(the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions :
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
import requests
from apprise import plugins
from helpers import AppriseURLTester
# Disable logging for a cleaner testing output
import logging
logging.disable(logging.CRITICAL)
# Our Testing URLs
apprise_url_tests = (
('popcorn://', {
# No hostname/apikey specified
'instance': TypeError,
}),
('popcorn://{}/18001231234'.format('_' * 9), {
# invalid apikey
'instance': TypeError,
}),
('popcorn://{}/1232348923489234923489234289-32423'.format('a' * 9), {
# invalid phone number
'instance': plugins.NotifyPopcornNotify,
'notify_response': False,
}),
('popcorn://{}/abc'.format('b' * 9), {
# invalid email
'instance': plugins.NotifyPopcornNotify,
'notify_response': False,
}),
('popcorn://{}/15551232000/user@example.com'.format('c' * 9), {
# value phone and email
'instance': plugins.NotifyPopcornNotify,
}),
('popcorn://{}/15551232000/user@example.com?batch=yes'.format('w' * 9), {
# value phone and email with batch mode set
'instance': plugins.NotifyPopcornNotify,
}),
('popcorn://{}/?to=15551232000'.format('w' * 9), {
# reference to to=
'instance': plugins.NotifyPopcornNotify,
}),
('popcorn://{}/15551232000'.format('x' * 9), {
'instance': plugins.NotifyPopcornNotify,
# force a failure
'response': False,
'requests_response_code': requests.codes.internal_server_error,
}),
('popcorn://{}/15551232000'.format('y' * 9), {
'instance': plugins.NotifyPopcornNotify,
# throw a bizzare code forcing us to fail to look it up
'response': False,
'requests_response_code': 999,
}),
('popcorn://{}/15551232000'.format('z' * 9), {
'instance': plugins.NotifyPopcornNotify,
# Throws a series of connection and transfer exceptions when this flag
# is set and tests that we gracfully handle them
'test_requests_exceptions': True,
}),
)
def test_plugin_popcorn_notify_urls():
"""
NotifyPopcornNotify() Apprise URLs
"""
# Run our general tests
AppriseURLTester(tests=apprise_url_tests).run_all()

142
test/test_plugin_prowl.py Normal file
View File

@ -0,0 +1,142 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2021 Chris Caron <lead2gold@gmail.com>
# All rights reserved.
#
# This code is licensed under the MIT License.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files(the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions :
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
import pytest
import requests
from apprise import plugins
from helpers import AppriseURLTester
# Disable logging for a cleaner testing output
import logging
logging.disable(logging.CRITICAL)
# Our Testing URLs
apprise_url_tests = (
('prowl://', {
'instance': TypeError,
}),
# bad url
('prowl://:@/', {
'instance': TypeError,
}),
# Invalid API Key
('prowl://%s' % ('a' * 20), {
'instance': TypeError,
}),
# Provider Key
('prowl://%s/%s' % ('a' * 40, 'b' * 40), {
'instance': plugins.NotifyProwl,
}),
# Invalid Provider Key
('prowl://%s/%s' % ('a' * 40, 'b' * 20), {
'instance': TypeError,
}),
# APIkey; no device
('prowl://%s' % ('a' * 40), {
'instance': plugins.NotifyProwl,
}),
# API Key
('prowl://%s' % ('a' * 40), {
'instance': plugins.NotifyProwl,
# don't include an image by default
'include_image': False,
}),
# API Key + priority setting
('prowl://%s?priority=high' % ('a' * 40), {
'instance': plugins.NotifyProwl,
}),
# API Key + invalid priority setting
('prowl://%s?priority=invalid' % ('a' * 40), {
'instance': plugins.NotifyProwl,
}),
# API Key + priority setting (empty)
('prowl://%s?priority=' % ('a' * 40), {
'instance': plugins.NotifyProwl,
}),
# API Key + No Provider Key (empty)
('prowl://%s///' % ('w' * 40), {
'instance': plugins.NotifyProwl,
# Our expected url(privacy=True) startswith() response:
'privacy_url': 'prowl://w...w/',
}),
# API Key + Provider Key
('prowl://%s/%s' % ('a' * 40, 'b' * 40), {
'instance': plugins.NotifyProwl,
# Our expected url(privacy=True) startswith() response:
'privacy_url': 'prowl://a...a/b...b',
}),
# API Key + with image
('prowl://%s' % ('a' * 40), {
'instance': plugins.NotifyProwl,
}),
('prowl://%s' % ('a' * 40), {
'instance': plugins.NotifyProwl,
# force a failure
'response': False,
'requests_response_code': requests.codes.internal_server_error,
}),
('prowl://%s' % ('a' * 40), {
'instance': plugins.NotifyProwl,
# throw a bizzare code forcing us to fail to look it up
'response': False,
'requests_response_code': 999,
}),
('prowl://%s' % ('a' * 40), {
'instance': plugins.NotifyProwl,
# Throws a series of connection and transfer exceptions when this flag
# is set and tests that we gracfully handle them
'test_requests_exceptions': True,
}),
)
def test_plugin_prowl():
"""
NotifyProwl() Apprise URLs
"""
# Run our general tests
AppriseURLTester(tests=apprise_url_tests).run_all()
def test_plugin_prowl_edge_cases():
"""
NotifyProwl() Edge Cases
"""
# Initializes the plugin with an invalid apikey
with pytest.raises(TypeError):
plugins.NotifyProwl(apikey=None)
# Whitespace also acts as an invalid apikey value
with pytest.raises(TypeError):
plugins.NotifyProwl(apikey=' ')
# Whitespace also acts as an invalid provider key
with pytest.raises(TypeError):
plugins.NotifyProwl(apikey='abcd', providerkey=object())
with pytest.raises(TypeError):
plugins.NotifyProwl(apikey='abcd', providerkey=' ')

View File

@ -0,0 +1,378 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2020 Chris Caron <lead2gold@gmail.com>
# All rights reserved.
#
# This code is licensed under the MIT License.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files(the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions :
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
import os
import mock
import pytest
import requests
from json import dumps
from apprise import Apprise
from apprise import AppriseAttachment
from apprise import plugins
from helpers import AppriseURLTester
# Disable logging for a cleaner testing output
import logging
logging.disable(logging.CRITICAL)
# Attachment Directory
TEST_VAR_DIR = os.path.join(os.path.dirname(__file__), 'var')
# Our Testing URLs
apprise_url_tests = (
('pbul://', {
'instance': TypeError,
}),
('pbul://:@/', {
'instance': TypeError,
}),
# APIkey
('pbul://%s' % ('a' * 32), {
'instance': plugins.NotifyPushBullet,
'check_attachments': False,
}),
# APIkey; but support attachment response
('pbul://%s' % ('a' * 32), {
'instance': plugins.NotifyPushBullet,
# Test what happens if a batch send fails to return a messageCount
'requests_response_text': {
'file_name': 'cat.jpeg',
'file_type': 'image/jpeg',
'file_url': 'http://file_url',
'upload_url': 'http://upload_url',
},
}),
# APIkey; attachment testing that isn't an image type
('pbul://%s' % ('a' * 32), {
'instance': plugins.NotifyPushBullet,
# Test what happens if a batch send fails to return a messageCount
'requests_response_text': {
'file_name': 'test.pdf',
'file_type': 'application/pdf',
'file_url': 'http://file_url',
'upload_url': 'http://upload_url',
},
}),
# APIkey; attachment testing were expected entry in payload is missing
('pbul://%s' % ('a' * 32), {
'instance': plugins.NotifyPushBullet,
# Test what happens if a batch send fails to return a messageCount
'requests_response_text': {
'file_name': 'test.pdf',
'file_type': 'application/pdf',
'file_url': 'http://file_url',
# upload_url missing
},
# Our Notification calls associated with attachments will fail:
'attach_response': False,
}),
# API Key + channel
('pbul://%s/#channel/' % ('a' * 32), {
'instance': plugins.NotifyPushBullet,
'check_attachments': False,
}),
# API Key + channel (via to=
('pbul://%s/?to=#channel' % ('a' * 32), {
'instance': plugins.NotifyPushBullet,
'check_attachments': False,
}),
# API Key + 2 channels
('pbul://%s/#channel1/#channel2' % ('a' * 32), {
'instance': plugins.NotifyPushBullet,
# Our expected url(privacy=True) startswith() response:
'privacy_url': 'pbul://a...a/',
'check_attachments': False,
}),
# API Key + device
('pbul://%s/device/' % ('a' * 32), {
'instance': plugins.NotifyPushBullet,
'check_attachments': False,
}),
# API Key + 2 devices
('pbul://%s/device1/device2/' % ('a' * 32), {
'instance': plugins.NotifyPushBullet,
'check_attachments': False,
}),
# API Key + email
('pbul://%s/user@example.com/' % ('a' * 32), {
'instance': plugins.NotifyPushBullet,
'check_attachments': False,
}),
# API Key + 2 emails
('pbul://%s/user@example.com/abc@def.com/' % ('a' * 32), {
'instance': plugins.NotifyPushBullet,
'check_attachments': False,
}),
# API Key + Combo
('pbul://%s/device/#channel/user@example.com/' % ('a' * 32), {
'instance': plugins.NotifyPushBullet,
'check_attachments': False,
}),
# ,
('pbul://%s' % ('a' * 32), {
'instance': plugins.NotifyPushBullet,
# force a failure
'response': False,
'requests_response_code': requests.codes.internal_server_error,
'check_attachments': False,
}),
('pbul://%s' % ('a' * 32), {
'instance': plugins.NotifyPushBullet,
# throw a bizzare code forcing us to fail to look it up
'response': False,
'requests_response_code': 999,
'check_attachments': False,
}),
('pbul://%s' % ('a' * 32), {
'instance': plugins.NotifyPushBullet,
# Throws a series of connection and transfer exceptions when this flag
# is set and tests that we gracfully handle them
'test_requests_exceptions': True,
'check_attachments': False,
}),
('pbul://%s' % ('a' * 32), {
'instance': plugins.NotifyPushBullet,
# force a failure
'response': False,
'requests_response_code': requests.codes.internal_server_error,
'check_attachments': False,
}),
('pbul://%s' % ('a' * 32), {
'instance': plugins.NotifyPushBullet,
# throw a bizzare code forcing us to fail to look it up
'response': False,
'requests_response_code': 999,
'check_attachments': False,
}),
('pbul://%s' % ('a' * 32), {
'instance': plugins.NotifyPushBullet,
# Throws a series of connection and transfer exceptions when this flag
# is set and tests that we gracfully handle them
'test_requests_exceptions': True,
'check_attachments': False,
}),
)
def test_plugin_pushbullet_urls():
"""
NotifyPushBullet() Apprise URLs
"""
# Run our general tests
AppriseURLTester(tests=apprise_url_tests).run_all()
@mock.patch('requests.post')
def test_plugin_pushbullet_attachments(mock_post):
"""
NotifyPushBullet() Attachment Checks
"""
# Disable Throttling to speed testing
plugins.NotifyPushBullet.request_rate_per_sec = 0
# Initialize some generic (but valid) tokens
access_token = 't' * 32
# Prepare Mock return object
response = mock.Mock()
response.content = dumps({
"file_name": "cat.jpg",
"file_type": "image/jpeg",
"file_url": "https://dl.pushb.com/abc/cat.jpg",
"upload_url": "https://upload.pushbullet.com/abcd123",
})
response.status_code = requests.codes.ok
mock_post.return_value = response
# prepare our attachment
attach = AppriseAttachment(os.path.join(TEST_VAR_DIR, 'apprise-test.gif'))
# Test our markdown
obj = Apprise.instantiate(
'pbul://{}/?format=markdown'.format(access_token))
# Send a good attachment
assert obj.notify(body="test", attach=attach) is True
# Test our call count
assert mock_post.call_count == 4
# Image Prep
assert mock_post.call_args_list[0][0][0] == \
'https://api.pushbullet.com/v2/upload-request'
assert mock_post.call_args_list[1][0][0] == \
'https://upload.pushbullet.com/abcd123'
# Message
assert mock_post.call_args_list[2][0][0] == \
'https://api.pushbullet.com/v2/pushes'
# Image Send
assert mock_post.call_args_list[3][0][0] == \
'https://api.pushbullet.com/v2/pushes'
# Reset our mock object
mock_post.reset_mock()
# Add another attachment so we drop into the area of the PushBullet code
# that sends remaining attachments (if more detected)
attach.add(os.path.join(TEST_VAR_DIR, 'apprise-test.gif'))
# Send our attachments
assert obj.notify(body="test", attach=attach) is True
# Test our call count
assert mock_post.call_count == 7
# Image Prep
assert mock_post.call_args_list[0][0][0] == \
'https://api.pushbullet.com/v2/upload-request'
assert mock_post.call_args_list[1][0][0] == \
'https://upload.pushbullet.com/abcd123'
assert mock_post.call_args_list[2][0][0] == \
'https://api.pushbullet.com/v2/upload-request'
assert mock_post.call_args_list[3][0][0] == \
'https://upload.pushbullet.com/abcd123'
# Message
assert mock_post.call_args_list[4][0][0] == \
'https://api.pushbullet.com/v2/pushes'
# Image Send
assert mock_post.call_args_list[5][0][0] == \
'https://api.pushbullet.com/v2/pushes'
assert mock_post.call_args_list[6][0][0] == \
'https://api.pushbullet.com/v2/pushes'
# Reset our mock object
mock_post.reset_mock()
# An invalid attachment will cause a failure
path = os.path.join(TEST_VAR_DIR, '/invalid/path/to/an/invalid/file.jpg')
attach = AppriseAttachment(path)
assert obj.notify(body="test", attach=attach) is False
# Test our call count
assert mock_post.call_count == 0
# prepare our attachment
attach = AppriseAttachment(os.path.join(TEST_VAR_DIR, 'apprise-test.gif'))
# Prepare a bad response
bad_response = mock.Mock()
bad_response.content = dumps({
"file_name": "cat.jpg",
"file_type": "image/jpeg",
"file_url": "https://dl.pushb.com/abc/cat.jpg",
"upload_url": "https://upload.pushbullet.com/abcd123",
})
bad_response.status_code = requests.codes.internal_server_error
# Prepare a bad response
bad_json_response = mock.Mock()
bad_json_response.content = '}'
bad_json_response.status_code = requests.codes.ok
# Throw an exception on the first call to requests.post()
mock_post.return_value = None
for side_effect in (requests.RequestException(), OSError(), bad_response):
mock_post.side_effect = side_effect
# We'll fail now because of our error handling
assert obj.send(body="test", attach=attach) is False
# Throw an exception on the second call to requests.post()
for side_effect in (requests.RequestException(), OSError(), bad_response):
mock_post.side_effect = [response, side_effect]
# We'll fail now because of our error handling
assert obj.send(body="test", attach=attach) is False
# Throw an exception on the third call to requests.post()
for side_effect in (requests.RequestException(), OSError(), bad_response):
mock_post.side_effect = [response, response, side_effect]
# We'll fail now because of our error handling
assert obj.send(body="test", attach=attach) is False
# Throw an exception on the forth call to requests.post()
for side_effect in (requests.RequestException(), OSError(), bad_response):
mock_post.side_effect = [response, response, response, side_effect]
# We'll fail now because of our error handling
assert obj.send(body="test", attach=attach) is False
# Test case where we don't get a valid response back
mock_post.side_effect = bad_json_response
# We'll fail because of an invalid json object
assert obj.send(body="test", attach=attach) is False
@mock.patch('requests.get')
@mock.patch('requests.post')
def test_plugin_pushbullet_edge_cases(mock_post, mock_get):
"""
NotifyPushBullet() Edge Cases
"""
# Disable Throttling to speed testing
plugins.NotifyBase.request_rate_per_sec = 0
# Initialize some generic (but valid) tokens
accesstoken = 'a' * 32
# Support strings
recipients = '#chan1,#chan2,device,user@example.com,,,'
# Prepare Mock
mock_get.return_value = requests.Request()
mock_post.return_value = requests.Request()
mock_post.return_value.status_code = requests.codes.ok
mock_get.return_value.status_code = requests.codes.ok
# Invalid Access Token
with pytest.raises(TypeError):
plugins.NotifyPushBullet(accesstoken=None)
with pytest.raises(TypeError):
plugins.NotifyPushBullet(accesstoken=" ")
obj = plugins.NotifyPushBullet(
accesstoken=accesstoken, targets=recipients)
assert isinstance(obj, plugins.NotifyPushBullet) is True
assert len(obj.targets) == 4
obj = plugins.NotifyPushBullet(accesstoken=accesstoken)
assert isinstance(obj, plugins.NotifyPushBullet) is True
# Default is to send to all devices, so there will be a
# recipient here
assert len(obj.targets) == 1
obj = plugins.NotifyPushBullet(accesstoken=accesstoken, targets=set())
assert isinstance(obj, plugins.NotifyPushBullet) is True
# Default is to send to all devices, so there will be a
# recipient here
assert len(obj.targets) == 1

218
test/test_plugin_pushed.py Normal file
View File

@ -0,0 +1,218 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2021 Chris Caron <lead2gold@gmail.com>
# All rights reserved.
#
# This code is licensed under the MIT License.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files(the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions :
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
import mock
import pytest
import requests
from apprise import plugins
from helpers import AppriseURLTester
# Disable logging for a cleaner testing output
import logging
logging.disable(logging.CRITICAL)
# Our Testing URLs
apprise_url_tests = (
('pushed://', {
'instance': TypeError,
}),
# Application Key Only
('pushed://%s' % ('a' * 32), {
'instance': TypeError,
}),
# Invalid URL
('pushed://:@/', {
'instance': TypeError,
}),
# Application Key+Secret
('pushed://%s/%s' % ('a' * 32, 'a' * 64), {
'instance': plugins.NotifyPushed,
}),
# Application Key+Secret + channel
('pushed://%s/%s/#channel/' % ('a' * 32, 'a' * 64), {
'instance': plugins.NotifyPushed,
}),
# Application Key+Secret + channel (via to=)
('pushed://%s/%s?to=channel' % ('a' * 32, 'a' * 64), {
'instance': plugins.NotifyPushed,
# Our expected url(privacy=True) startswith() response:
'privacy_url': 'pushed://a...a/****/',
}),
# Application Key+Secret + dropped entry
('pushed://%s/%s/dropped_value/' % ('a' * 32, 'a' * 64), {
# No entries validated is a fail
'instance': TypeError,
}),
# Application Key+Secret + 2 channels
('pushed://%s/%s/#channel1/#channel2' % ('a' * 32, 'a' * 64), {
'instance': plugins.NotifyPushed,
}),
# Application Key+Secret + User Pushed ID
('pushed://%s/%s/@ABCD/' % ('a' * 32, 'a' * 64), {
'instance': plugins.NotifyPushed,
}),
# Application Key+Secret + 2 devices
('pushed://%s/%s/@ABCD/@DEFG/' % ('a' * 32, 'a' * 64), {
'instance': plugins.NotifyPushed,
}),
# Application Key+Secret + Combo
('pushed://%s/%s/@ABCD/#channel' % ('a' * 32, 'a' * 64), {
'instance': plugins.NotifyPushed,
}),
# ,
('pushed://%s/%s' % ('a' * 32, 'a' * 64), {
'instance': plugins.NotifyPushed,
# force a failure
'response': False,
'requests_response_code': requests.codes.internal_server_error,
}),
('pushed://%s/%s' % ('a' * 32, 'a' * 64), {
'instance': plugins.NotifyPushed,
# throw a bizzare code forcing us to fail to look it up
'response': False,
'requests_response_code': 999,
}),
('pushed://%s/%s' % ('a' * 32, 'a' * 64), {
'instance': plugins.NotifyPushed,
# Throws a series of connection and transfer exceptions when this flag
# is set and tests that we gracfully handle them
'test_requests_exceptions': True,
}),
('pushed://%s/%s' % ('a' * 32, 'a' * 64), {
'instance': plugins.NotifyPushed,
# force a failure
'response': False,
'requests_response_code': requests.codes.internal_server_error,
}),
('pushed://%s/%s' % ('a' * 32, 'a' * 64), {
'instance': plugins.NotifyPushed,
# throw a bizzare code forcing us to fail to look it up
'response': False,
'requests_response_code': 999,
}),
('pushed://%s/%s/#channel' % ('a' * 32, 'a' * 64), {
'instance': plugins.NotifyPushed,
# throw a bizzare code forcing us to fail to look it up
'response': False,
'requests_response_code': 999,
}),
('pushed://%s/%s/@user' % ('a' * 32, 'a' * 64), {
'instance': plugins.NotifyPushed,
# throw a bizzare code forcing us to fail to look it up
'response': False,
'requests_response_code': 999,
}),
('pushed://%s/%s' % ('a' * 32, 'a' * 64), {
'instance': plugins.NotifyPushed,
# Throws a series of connection and transfer exceptions when this flag
# is set and tests that we gracfully handle them
'test_requests_exceptions': True,
}),
)
def test_plugin_pushed_urls():
"""
NotifyPushed() Apprise URLs
"""
# Run our general tests
AppriseURLTester(tests=apprise_url_tests).run_all()
@mock.patch('requests.get')
@mock.patch('requests.post')
def test_plugin_pushed_edge_cases(mock_post, mock_get):
"""
NotifyPushed() Edge Cases
"""
# Disable Throttling to speed testing
plugins.NotifyBase.request_rate_per_sec = 0
# Chat ID
recipients = '@ABCDEFG, @DEFGHIJ, #channel, #channel2'
# Some required input
app_key = 'ABCDEFG'
app_secret = 'ABCDEFG'
# Prepare Mock
mock_get.return_value = requests.Request()
mock_post.return_value = requests.Request()
mock_post.return_value.status_code = requests.codes.ok
mock_get.return_value.status_code = requests.codes.ok
# No application Key specified
with pytest.raises(TypeError):
plugins.NotifyPushed(
app_key=None,
app_secret=app_secret,
recipients=None,
)
with pytest.raises(TypeError):
plugins.NotifyPushed(
app_key=" ",
app_secret=app_secret,
recipients=None,
)
# No application Secret specified
with pytest.raises(TypeError):
plugins.NotifyPushed(
app_key=app_key,
app_secret=None,
recipients=None,
)
with pytest.raises(TypeError):
plugins.NotifyPushed(
app_key=app_key,
app_secret=" ",
)
# recipients list set to (None) is perfectly fine; in this case it will
# notify the App
obj = plugins.NotifyPushed(
app_key=app_key,
app_secret=app_secret,
recipients=None,
)
assert isinstance(obj, plugins.NotifyPushed) is True
assert len(obj.channels) == 0
assert len(obj.users) == 0
obj = plugins.NotifyPushed(
app_key=app_key,
app_secret=app_secret,
targets=recipients,
)
assert isinstance(obj, plugins.NotifyPushed) is True
assert len(obj.channels) == 2
assert len(obj.users) == 2
# Prepare Mock to fail
mock_post.return_value.status_code = requests.codes.internal_server_error
mock_get.return_value.status_code = requests.codes.internal_server_error

112
test/test_plugin_pushjet.py Normal file
View File

@ -0,0 +1,112 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2021 Chris Caron <lead2gold@gmail.com>
# All rights reserved.
#
# This code is licensed under the MIT License.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files(the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions :
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
import pytest
import requests
from apprise import plugins
from helpers import AppriseURLTester
# Disable logging for a cleaner testing output
import logging
logging.disable(logging.CRITICAL)
# Our Testing URLs
apprise_url_tests = (
('pjet://', {
'instance': None,
}),
('pjets://', {
'instance': None,
}),
('pjet://:@/', {
'instance': None,
}),
# You must specify a secret key
('pjet://%s' % ('a' * 32), {
'instance': TypeError,
}),
# The proper way to log in
('pjet://user:pass@localhost/%s' % ('a' * 32), {
'instance': plugins.NotifyPushjet,
}),
# The proper way to log in
('pjets://localhost/%s' % ('a' * 32), {
'instance': plugins.NotifyPushjet,
}),
# Specify your own server with login (secret= MUST be provided)
('pjet://user:pass@localhost?secret=%s' % ('a' * 32), {
'instance': plugins.NotifyPushjet,
# Our expected url(privacy=True) startswith() response:
'privacy_url': 'pjet://user:****@localhost',
}),
# Specify your own server with port
('pjets://localhost:8080/%s' % ('a' * 32), {
'instance': plugins.NotifyPushjet,
}),
('pjets://localhost:8080/%s' % ('a' * 32), {
'instance': plugins.NotifyPushjet,
# force a failure
'response': False,
'requests_response_code': requests.codes.internal_server_error,
}),
('pjets://localhost:4343/%s' % ('a' * 32), {
'instance': plugins.NotifyPushjet,
# throw a bizzare code forcing us to fail to look it up
'response': False,
'requests_response_code': 999,
}),
('pjet://localhost:8081/%s' % ('a' * 32), {
'instance': plugins.NotifyPushjet,
# Throws a series of connection and transfer exceptions when this flag
# is set and tests that we gracfully handle them
'test_requests_exceptions': True,
}),
)
def test_plugin_pushjet_urls():
"""
NotifyPushjet() Apprise URLs
"""
# Run our general tests
AppriseURLTester(tests=apprise_url_tests).run_all()
def test_plugin_pushjet_edge_cases():
"""
NotifyPushjet() Edge Cases
"""
# Disable Throttling to speed testing
plugins.NotifyBase.request_rate_per_sec = 0
# No application Key specified
with pytest.raises(TypeError):
plugins.NotifyPushjet(secret_key=None)
with pytest.raises(TypeError):
plugins.NotifyPushjet(secret_key=" ")

View File

@ -0,0 +1,370 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2020 Chris Caron <lead2gold@gmail.com>
# All rights reserved.
#
# This code is licensed under the MIT License.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files(the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions :
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
import os
import mock
import requests
import pytest
from json import dumps
from apprise import Apprise
from apprise import NotifyType
from apprise import AppriseAttachment
from apprise import plugins
from helpers import AppriseURLTester
# Disable logging for a cleaner testing output
import logging
logging.disable(logging.CRITICAL)
# Attachment Directory
TEST_VAR_DIR = os.path.join(os.path.dirname(__file__), 'var')
# Our Testing URLs
apprise_url_tests = (
('pover://', {
'instance': TypeError,
}),
# bad url
('pover://:@/', {
'instance': TypeError,
}),
# APIkey; no user
('pover://%s' % ('a' * 30), {
'instance': TypeError,
}),
# API Key + invalid sound setting
('pover://%s@%s?sound=invalid' % ('u' * 30, 'a' * 30), {
'instance': TypeError,
}),
# API Key + valid alternate sound picked
('pover://%s@%s?sound=spacealarm' % ('u' * 30, 'a' * 30), {
'instance': plugins.NotifyPushover,
}),
# API Key + valid url_title with url
('pover://%s@%s?url=my-url&url_title=title' % ('u' * 30, 'a' * 30), {
'instance': plugins.NotifyPushover,
}),
# API Key + Valid User
('pover://%s@%s' % ('u' * 30, 'a' * 30), {
'instance': plugins.NotifyPushover,
# don't include an image by default
'include_image': False,
}),
# API Key + Valid User + 1 Device
('pover://%s@%s/DEVICE' % ('u' * 30, 'a' * 30), {
'instance': plugins.NotifyPushover,
}),
# API Key + Valid User + 1 Device (via to=)
('pover://%s@%s?to=DEVICE' % ('u' * 30, 'a' * 30), {
'instance': plugins.NotifyPushover,
}),
# API Key + Valid User + 2 Devices
('pover://%s@%s/DEVICE1/DEVICE2/' % ('u' * 30, 'a' * 30), {
'instance': plugins.NotifyPushover,
# Our expected url(privacy=True) startswith() response:
'privacy_url': 'pover://u...u@a...a',
}),
# API Key + Valid User + invalid device
('pover://%s@%s/%s/' % ('u' * 30, 'a' * 30, 'd' * 30), {
'instance': plugins.NotifyPushover,
# Notify will return False since there is a bad device in our list
'response': False,
}),
# API Key + Valid User + device + invalid device
('pover://%s@%s/DEVICE1/%s/' % ('u' * 30, 'a' * 30, 'd' * 30), {
'instance': plugins.NotifyPushover,
# Notify will return False since there is a bad device in our list
'response': False,
}),
# API Key + priority setting
('pover://%s@%s?priority=high' % ('u' * 30, 'a' * 30), {
'instance': plugins.NotifyPushover,
}),
# API Key + priority setting + html mode
('pover://%s@%s?priority=high&format=html' % ('u' * 30, 'a' * 30), {
'instance': plugins.NotifyPushover,
}),
# API Key + invalid priority setting
('pover://%s@%s?priority=invalid' % ('u' * 30, 'a' * 30), {
'instance': plugins.NotifyPushover,
}),
# API Key + emergency(2) priority setting
('pover://%s@%s?priority=emergency' % ('u' * 30, 'a' * 30), {
'instance': plugins.NotifyPushover,
}),
# API Key + emergency priority setting with retry and expire
('pover://%s@%s?priority=emergency&%s&%s' % ('u' * 30,
'a' * 30,
'retry=30',
'expire=300'), {
'instance': plugins.NotifyPushover,
}),
# API Key + emergency priority setting with text retry
('pover://%s@%s?priority=emergency&%s&%s' % ('u' * 30,
'a' * 30,
'retry=invalid',
'expire=300'), {
'instance': plugins.NotifyPushover,
}),
# API Key + emergency priority setting with text expire
('pover://%s@%s?priority=emergency&%s&%s' % ('u' * 30,
'a' * 30,
'retry=30',
'expire=invalid'), {
'instance': plugins.NotifyPushover,
}),
# API Key + emergency priority setting with invalid expire
('pover://%s@%s?priority=emergency&%s' % ('u' * 30,
'a' * 30,
'expire=100000'), {
'instance': TypeError,
}),
# API Key + emergency priority setting with invalid retry
('pover://%s@%s?priority=emergency&%s' % ('u' * 30,
'a' * 30,
'retry=15'), {
'instance': TypeError,
}),
# API Key + priority setting (empty)
('pover://%s@%s?priority=' % ('u' * 30, 'a' * 30), {
'instance': plugins.NotifyPushover,
}),
('pover://%s@%s' % ('u' * 30, 'a' * 30), {
'instance': plugins.NotifyPushover,
# force a failure
'response': False,
'requests_response_code': requests.codes.internal_server_error,
}),
('pover://%s@%s' % ('u' * 30, 'a' * 30), {
'instance': plugins.NotifyPushover,
# throw a bizzare code forcing us to fail to look it up
'response': False,
'requests_response_code': 999,
}),
('pover://%s@%s' % ('u' * 30, 'a' * 30), {
'instance': plugins.NotifyPushover,
# Throws a series of connection and transfer exceptions when this flag
# is set and tests that we gracfully handle them
'test_requests_exceptions': True,
}),
)
def test_plugin_pushover_urls():
"""
NotifyPushover() Apprise URLs
"""
# Run our general tests
AppriseURLTester(tests=apprise_url_tests).run_all()
@mock.patch('requests.post')
def test_plugin_pushover_attachments(mock_post, tmpdir):
"""
NotifyPushover() Attachment Checks
"""
# Disable Throttling to speed testing
plugins.NotifyBase.request_rate_per_sec = 0
# Initialize some generic (but valid) tokens
user_key = 'u' * 30
api_token = 'a' * 30
# Prepare a good response
response = mock.Mock()
response.content = dumps(
{"status": 1, "request": "647d2300-702c-4b38-8b2f-d56326ae460b"})
response.status_code = requests.codes.ok
# Prepare a bad response
bad_response = mock.Mock()
response.content = dumps(
{"status": 1, "request": "647d2300-702c-4b38-8b2f-d56326ae460b"})
bad_response.status_code = requests.codes.internal_server_error
# Assign our good response
mock_post.return_value = response
# prepare our attachment
attach = AppriseAttachment(os.path.join(TEST_VAR_DIR, 'apprise-test.gif'))
# Instantiate our object
obj = Apprise.instantiate(
'pover://{}@{}/'.format(user_key, api_token))
assert isinstance(obj, plugins.NotifyPushover)
# Test our attachment
assert obj.notify(body="test", attach=attach) is True
# Test our call count
assert mock_post.call_count == 1
assert mock_post.call_args_list[0][0][0] == \
'https://api.pushover.net/1/messages.json'
# Reset our mock object for multiple tests
mock_post.reset_mock()
# Test multiple attachments
assert attach.add(os.path.join(TEST_VAR_DIR, 'apprise-test.gif'))
assert obj.notify(body="test", attach=attach) is True
# Test our call count
assert mock_post.call_count == 2
assert mock_post.call_args_list[0][0][0] == \
'https://api.pushover.net/1/messages.json'
assert mock_post.call_args_list[1][0][0] == \
'https://api.pushover.net/1/messages.json'
# Reset our mock object for multiple tests
mock_post.reset_mock()
image = tmpdir.mkdir("pover_image").join("test.jpg")
image.write('a' * plugins.NotifyPushover.attach_max_size_bytes)
attach = AppriseAttachment.instantiate(str(image))
assert obj.notify(body="test", attach=attach) is True
# Test our call count
assert mock_post.call_count == 1
assert mock_post.call_args_list[0][0][0] == \
'https://api.pushover.net/1/messages.json'
# Reset our mock object for multiple tests
mock_post.reset_mock()
# Add 1 more byte to the file (putting it over the limit)
image.write('a' * (plugins.NotifyPushover.attach_max_size_bytes + 1))
attach = AppriseAttachment.instantiate(str(image))
assert obj.notify(body="test", attach=attach) is False
# Test our call count
assert mock_post.call_count == 0
# Test case when file is missing
attach = AppriseAttachment.instantiate(
'file://{}?cache=False'.format(str(image)))
os.unlink(str(image))
assert obj.notify(
body='body', title='title', attach=attach) is False
# Test our call count
assert mock_post.call_count == 0
# Test unsuported files:
image = tmpdir.mkdir("pover_unsupported").join("test.doc")
image.write('a' * 256)
attach = AppriseAttachment.instantiate(str(image))
# Content is silently ignored
assert obj.notify(body="test", attach=attach) is True
# prepare our attachment
attach = AppriseAttachment(os.path.join(TEST_VAR_DIR, 'apprise-test.gif'))
# Throw an exception on the first call to requests.post()
for side_effect in (requests.RequestException(), OSError(), bad_response):
mock_post.side_effect = [side_effect, side_effect]
# We'll fail now because of our error handling
assert obj.send(body="test", attach=attach) is False
# Same case without an attachment
assert obj.send(body="test") is False
@mock.patch('requests.get')
@mock.patch('requests.post')
def test_plugin_pushover_edge_cases(mock_post, mock_get):
"""
NotifyPushover() Edge Cases
"""
# Disable Throttling to speed testing
plugins.NotifyBase.request_rate_per_sec = 0
# No token
with pytest.raises(TypeError):
plugins.NotifyPushover(token=None)
# Initialize some generic (but valid) tokens
token = 'a' * 30
user_key = 'u' * 30
invalid_device = 'd' * 35
# Support strings
devices = 'device1,device2,,,,%s' % invalid_device
# Prepare Mock
mock_get.return_value = requests.Request()
mock_post.return_value = requests.Request()
mock_post.return_value.status_code = requests.codes.ok
mock_get.return_value.status_code = requests.codes.ok
# No webhook id specified
with pytest.raises(TypeError):
plugins.NotifyPushover(user_key=user_key, webhook_id=None)
obj = plugins.NotifyPushover(
user_key=user_key, token=token, targets=devices)
assert isinstance(obj, plugins.NotifyPushover) is True
assert len(obj.targets) == 3
# This call fails because there is 1 invalid device
assert obj.notify(
body='body', title='title',
notify_type=NotifyType.INFO) is False
obj = plugins.NotifyPushover(user_key=user_key, token=token)
assert isinstance(obj, plugins.NotifyPushover) is True
# Default is to send to all devices, so there will be a
# device defined here
assert len(obj.targets) == 1
# This call succeeds because all of the devices are valid
assert obj.notify(
body='body', title='title', notify_type=NotifyType.INFO) is True
obj = plugins.NotifyPushover(user_key=user_key, token=token, targets=set())
assert isinstance(obj, plugins.NotifyPushover) is True
# Default is to send to all devices, so there will be a
# device defined here
assert len(obj.targets) == 1
# No User Key specified
with pytest.raises(TypeError):
plugins.NotifyPushover(user_key=None, token="abcd")
# No Access Token specified
with pytest.raises(TypeError):
plugins.NotifyPushover(user_key="abcd", token=None)
with pytest.raises(TypeError):
plugins.NotifyPushover(user_key="abcd", token=" ")

View File

@ -0,0 +1,286 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2020 Chris Caron <lead2gold@gmail.com>
# All rights reserved.
#
# This code is licensed under the MIT License.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files(the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions :
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
import os
import pytest
import mock
import requests
from json import dumps
from apprise import AppriseAttachment
from apprise import NotifyType
from apprise import plugins
from helpers import AppriseURLTester
# Disable logging for a cleaner testing output
import logging
logging.disable(logging.CRITICAL)
# Attachment Directory
TEST_VAR_DIR = os.path.join(os.path.dirname(__file__), 'var')
# Our Testing URLs
apprise_url_tests = (
('psafer://:@/', {
'instance': TypeError,
}),
('psafer://', {
'instance': TypeError,
}),
('psafers://', {
'instance': TypeError,
}),
('psafer://{}'.format('a' * 20), {
'instance': plugins.NotifyPushSafer,
# This will fail because we're also expecting a server acknowledgement
'notify_response': False,
}),
('psafer://{}'.format('b' * 20), {
'instance': plugins.NotifyPushSafer,
# invalid JSON response
'requests_response_text': '{',
'notify_response': False,
}),
('psafer://{}'.format('c' * 20), {
'instance': plugins.NotifyPushSafer,
# A failure has status set to zero
# We also expect an 'error' flag to be set
'requests_response_text': {
'status': 0,
'error': 'we failed'
},
'notify_response': False,
}),
('psafers://{}'.format('d' * 20), {
'instance': plugins.NotifyPushSafer,
# A failure has status set to zero
# Test without an 'error' flag
'requests_response_text': {
'status': 0,
},
'notify_response': False,
}),
# This will notify all users ('a')
('psafer://{}'.format('e' * 20), {
'instance': plugins.NotifyPushSafer,
# A status of 1 is a success
'requests_response_text': {
'status': 1,
}
}),
# This will notify a selected set of devices
('psafer://{}/12/24/53'.format('e' * 20), {
'instance': plugins.NotifyPushSafer,
# A status of 1 is a success
'requests_response_text': {
'status': 1,
}
}),
# Same as above, but exercises the to= argument
('psafer://{}?to=12,24,53'.format('e' * 20), {
'instance': plugins.NotifyPushSafer,
# A status of 1 is a success
'requests_response_text': {
'status': 1,
}
}),
# Set priority
('psafer://{}?priority=emergency'.format('f' * 20), {
'instance': plugins.NotifyPushSafer,
'requests_response_text': {
'status': 1,
}
}),
# Support integer value too
('psafer://{}?priority=-1'.format('f' * 20), {
'instance': plugins.NotifyPushSafer,
'requests_response_text': {
'status': 1,
}
}),
# Invalid priority
('psafer://{}?priority=invalid'.format('f' * 20), {
# Invalid Priority
'instance': TypeError,
}),
# Invalid priority
('psafer://{}?priority=25'.format('f' * 20), {
# Invalid Priority
'instance': TypeError,
}),
# Set sound
('psafer://{}?sound=ok'.format('g' * 20), {
'instance': plugins.NotifyPushSafer,
'requests_response_text': {
'status': 1,
}
}),
# Support integer value too
('psafers://{}?sound=14'.format('g' * 20), {
'instance': plugins.NotifyPushSafer,
'requests_response_text': {
'status': 1,
},
'privacy_url': 'psafers://g...g',
}),
# Invalid sound
('psafer://{}?sound=invalid'.format('h' * 20), {
# Invalid Sound
'instance': TypeError,
}),
('psafer://{}?sound=94000'.format('h' * 20), {
# Invalid Sound
'instance': TypeError,
}),
# Set vibration (integer only)
('psafers://{}?vibration=1'.format('h' * 20), {
'instance': plugins.NotifyPushSafer,
'requests_response_text': {
'status': 1,
},
'privacy_url': 'psafers://h...h',
}),
# Invalid sound
('psafer://{}?vibration=invalid'.format('h' * 20), {
# Invalid Vibration
'instance': TypeError,
}),
# Invalid vibration
('psafer://{}?vibration=25000'.format('h' * 20), {
# Invalid Vibration
'instance': TypeError,
}),
('psafers://{}'.format('d' * 20), {
'instance': plugins.NotifyPushSafer,
# A failure has status set to zero
# Test without an 'error' flag
'requests_response_text': {
'status': 0,
},
# force a failure
'response': False,
'requests_response_code': requests.codes.internal_server_error,
}),
('psafer://{}'.format('d' * 20), {
'instance': plugins.NotifyPushSafer,
# A failure has status set to zero
# Test without an 'error' flag
'requests_response_text': {
'status': 0,
},
# throw a bizzare code forcing us to fail to look it up
'response': False,
'requests_response_code': 999,
}),
('psafers://{}'.format('d' * 20), {
'instance': plugins.NotifyPushSafer,
# A failure has status set to zero
# Test without an 'error' flag
'requests_response_text': {
'status': 0,
},
# Throws a series of connection and transfer exceptions when this
# flag is set and tests that we gracfully handle them
'test_requests_exceptions': True,
}),
)
def test_plugin_pushsafer_urls():
"""
NotifyPushSafer() Apprise URLs
"""
# Run our general tests
AppriseURLTester(tests=apprise_url_tests).run_all()
@mock.patch('requests.post')
def test_plugin_pushsafer_general(mock_post):
"""
NotifyPushSafer() General Tests
"""
# Disable Throttling to speed testing
plugins.NotifyPushSafer.request_rate_per_sec = 0
# Private Key
privatekey = 'abc123'
# Prepare Mock
mock_post.return_value = requests.Request()
mock_post.return_value.status_code = requests.codes.ok
mock_post.return_value.content = dumps({
'status': 1,
'success': "okay",
})
# Exception should be thrown about the fact no private key was specified
with pytest.raises(TypeError):
plugins.NotifyPushSafer(privatekey=None)
# Multiple Attachment Support
path = os.path.join(TEST_VAR_DIR, 'apprise-test.gif')
attach = AppriseAttachment()
for _ in range(0, 4):
attach.add(path)
obj = plugins.NotifyPushSafer(privatekey=privatekey)
assert obj.notify(
body='body', title='title', notify_type=NotifyType.INFO,
attach=attach) is True
# Test error reading attachment from disk
with mock.patch('io.open', side_effect=OSError):
obj.notify(
body='body', title='title', notify_type=NotifyType.INFO,
attach=attach)
# Test unsupported mime type
attach = AppriseAttachment(path)
attach[0]._mimetype = 'application/octet-stream'
# We gracefully just don't send the attachment in these cases;
# The notify itself will still be successful
mock_post.reset_mock()
assert obj.notify(
body='body', title='title', notify_type=NotifyType.INFO,
attach=attach) is True
# the 'p', 'p2', and 'p3' are the data variables used when including an
# image.
assert 'data' in mock_post.call_args[1]
assert 'p' not in mock_post.call_args[1]['data']
assert 'p2' not in mock_post.call_args[1]['data']
assert 'p3' not in mock_post.call_args[1]['data']
# Invalid file path
path = os.path.join(TEST_VAR_DIR, '/invalid/path/to/an/invalid/file.jpg')
assert obj.notify(
body='body', title='title', notify_type=NotifyType.INFO,
attach=path) is False

View File

@ -27,6 +27,7 @@ import six
import requests
import mock
from apprise import plugins
from helpers import AppriseURLTester
from json import dumps
from datetime import datetime
@ -36,11 +37,198 @@ from datetime import timedelta
import logging
logging.disable(logging.CRITICAL)
# Our Testing URLs
apprise_url_tests = (
('reddit://', {
# Missing all credentials
'instance': TypeError,
}),
('reddit://:@/', {
'instance': TypeError,
}),
('reddit://user@app_id/app_secret/', {
# No password
'instance': TypeError,
}),
('reddit://user:password@app_id/', {
# No app secret
'instance': TypeError,
}),
('reddit://user:password@app_id/appsecret/apprise', {
# No invalid app_id (has underscore)
'instance': TypeError,
}),
('reddit://user:password@app-id/app_secret/apprise', {
# No invalid app_secret (has underscore)
'instance': TypeError,
}),
('reddit://user:password@app-id/app-secret/apprise?kind=invalid', {
# An Invalid Kind
'instance': TypeError,
}),
('reddit://user:password@app-id/app-secret/apprise', {
# Login failed
'instance': plugins.NotifyReddit,
# Expected notify() response is False because internally we would
# have failed to login
'notify_response': False,
}),
('reddit://user:password@app-id/app-secret', {
# Login successful, but there was no subreddit to notify
'instance': plugins.NotifyReddit,
'requests_response_text': {
"access_token": 'abc123',
"token_type": "bearer",
"expires_in": 100000,
"scope": '*',
"refresh_token": 'def456',
# The below is used in the response:
"json": {
# No errors during post
"errors": [],
},
},
# Expected notify() response is False
'notify_response': False,
}),
('reddit://user:password@app-id/app-secret/apprise', {
'instance': plugins.NotifyReddit,
'requests_response_text': {
"access_token": 'abc123',
"token_type": "bearer",
"expires_in": 100000,
"scope": '*',
"refresh_token": 'def456',
# The below is used in the response:
"json": {
# Identify an error
"errors": [('KEY', 'DESC', 'INFO'), ],
},
},
# Expected notify() response is False because the
# reddit server provided us errors
'notify_response': False,
}),
('reddit://user:password@app-id/app-secret/apprise', {
'instance': plugins.NotifyReddit,
'requests_response_text': {
"access_token": 'abc123',
"token_type": "bearer",
# Test case where 'expires_in' entry is missing
"scope": '*',
"refresh_token": 'def456',
# The below is used in the response:
"json": {
# No errors during post
"errors": [],
},
},
# Our expected url(privacy=True) startswith() response:
'privacy_url': 'reddit://user:****@****/****/apprise',
}),
('reddit://user:password@app-id/app-secret/apprise/subreddit2', {
# password:login acceptable
'instance': plugins.NotifyReddit,
'requests_response_text': {
"access_token": 'abc123',
"token_type": "bearer",
"expires_in": 100000,
"scope": '*',
"refresh_token": 'def456',
# The below is used in the response:
"json": {
# No errors during post
"errors": [],
},
},
# Our expected url(privacy=True) startswith() response:
'privacy_url':
'reddit://user:****@****/****/apprise/subreddit2',
}),
# Pass in some arguments to over-ride defaults
('reddit://user:pass@id/secret/sub/'
'?ad=yes&nsfw=yes&replies=no&resubmit=yes&spoiler=yes&kind=self', {
'instance': plugins.NotifyReddit,
'requests_response_text': {
"access_token": 'abc123',
"token_type": "bearer",
"expires_in": 100000,
"scope": '*',
"refresh_token": 'def456',
# The below is used in the response:
"json": {
# No errors during post
"errors": [],
},
},
# Our expected url(privacy=True) startswith() response:
'privacy_url': 'reddit://user:****@****/****/sub'}),
# Pass in more arguments
('reddit://'
'?user=l2g&pass=pass&app_secret=abc123&app_id=54321&to=sub1,sub2', {
'instance': plugins.NotifyReddit,
'requests_response_text': {
"access_token": 'abc123',
"token_type": "bearer",
"expires_in": 100000,
"scope": '*',
"refresh_token": 'def456',
# The below is used in the response:
"json": {
# No errors during post
"errors": [],
},
},
# Our expected url(privacy=True) startswith() response:
'privacy_url': 'reddit://l2g:****@****/****/sub1/sub2'}),
# More arguments ...
('reddit://user:pass@id/secret/sub7/sub6/sub5/'
'?flair_id=wonder&flair_text=not%20for%20you', {
'instance': plugins.NotifyReddit,
'requests_response_text': {
"access_token": 'abc123',
"token_type": "bearer",
"expires_in": 100000,
"scope": '*',
"refresh_token": 'def456',
# The below is used in the response:
"json": {
# No errors during post
"errors": [],
},
},
# Our expected url(privacy=True) startswith() response:
'privacy_url': 'reddit://user:****@****/****/sub'}),
('reddit://user:password@app-id/app-secret/apprise', {
'instance': plugins.NotifyReddit,
# throw a bizzare code forcing us to fail to look it up
'response': False,
'requests_response_code': 999,
}),
('reddit://user:password@app-id/app-secret/apprise', {
'instance': plugins.NotifyReddit,
# Throws a series of connection and transfer exceptions when this flag
# is set and tests that we gracfully handle them
'test_requests_exceptions': True,
}),
)
def test_plugin_reddit_urls():
"""
NotifyReddit() Apprise URLs
"""
# Run our general tests
AppriseURLTester(tests=apprise_url_tests).run_all()
@mock.patch('requests.post')
def test_notify_reddit_plugin_general(mock_post):
def test_plugin_reddit_general(mock_post):
"""
API: NotifyReddit() General Tests
NotifyReddit() General Tests
"""
# Disable Throttling to speed testing

View File

@ -0,0 +1,327 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2021 Chris Caron <lead2gold@gmail.com>
# All rights reserved.
#
# This code is licensed under the MIT License.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files(the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions :
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
import mock
import pytest
import requests
from apprise import plugins
from apprise import NotifyType
from helpers import AppriseURLTester
# Disable logging for a cleaner testing output
import logging
logging.disable(logging.CRITICAL)
# Our Testing URLs
apprise_url_tests = (
('rocket://', {
'instance': None,
}),
('rockets://', {
'instance': None,
}),
('rocket://:@/', {
'instance': None,
}),
# No username or pass
('rocket://localhost', {
'instance': TypeError,
}),
# No room or channel
('rocket://user:pass@localhost', {
'instance': TypeError,
}),
# No valid rooms or channels
('rocket://user:pass@localhost/#/!/@', {
'instance': TypeError,
}),
# No user/pass combo
('rocket://user@localhost/room/', {
'instance': TypeError,
}),
# No user/pass combo
('rocket://localhost/room/', {
'instance': TypeError,
}),
# A room and port identifier
('rocket://user:pass@localhost:8080/room/', {
'instance': plugins.NotifyRocketChat,
# The response text is expected to be the following on a success
'requests_response_text': {
'status': 'success',
'data': {
'authToken': 'abcd',
'userId': 'user',
},
},
}),
# A channel (using the to=)
('rockets://user:pass@localhost?to=#channel', {
'instance': plugins.NotifyRocketChat,
# The response text is expected to be the following on a success
'requests_response_text': {
'status': 'success',
'data': {
'authToken': 'abcd',
'userId': 'user',
},
},
}),
# A channel
('rockets://user:pass@localhost/#channel', {
'instance': plugins.NotifyRocketChat,
# The response text is expected to be the following on a success
'requests_response_text': {
'status': 'success',
'data': {
'authToken': 'abcd',
'userId': 'user',
},
},
}),
# Several channels
('rocket://user:pass@localhost/#channel1/#channel2/?avatar=Yes', {
'instance': plugins.NotifyRocketChat,
# The response text is expected to be the following on a success
'requests_response_text': {
'status': 'success',
'data': {
'authToken': 'abcd',
'userId': 'user',
},
},
}),
# Several Rooms
('rocket://user:pass@localhost/room1/room2', {
'instance': plugins.NotifyRocketChat,
# The response text is expected to be the following on a success
'requests_response_text': {
'status': 'success',
'data': {
'authToken': 'abcd',
'userId': 'user',
},
},
}),
# A room and channel
('rocket://user:pass@localhost/room/#channel?mode=basic&avatar=Yes', {
'instance': plugins.NotifyRocketChat,
# The response text is expected to be the following on a success
'requests_response_text': {
'status': 'success',
'data': {
'authToken': 'abcd',
'userId': 'user',
},
},
# Our expected url(privacy=True) startswith() response:
'privacy_url': 'rocket://user:****@localhost',
}),
# A user/pass where the pass matches a webtoken
# to ensure we get the right mode, we enforce basic mode
# so that web/token gets interpreted as a password
('rockets://user:pass%2Fwithslash@localhost/#channel/?mode=basic', {
'instance': plugins.NotifyRocketChat,
# The response text is expected to be the following on a success
'requests_response_text': {
'status': 'success',
'data': {
'authToken': 'abcd',
'userId': 'user',
},
},
}),
# A room and channel
('rockets://user:pass@localhost/rooma/#channela', {
# The response text is expected to be the following on a success
'requests_response_code': requests.codes.ok,
'requests_response_text': {
# return something other then a success message type
'status': 'failure',
},
# Exception is thrown in this case
'instance': plugins.NotifyRocketChat,
# Notifications will fail in this event
'response': False,
}),
# A web token
('rockets://web/token@localhost/@user/#channel/roomid', {
'instance': plugins.NotifyRocketChat,
}),
('rockets://user:web/token@localhost/@user/?mode=webhook', {
'instance': plugins.NotifyRocketChat,
}),
('rockets://user:web/token@localhost?to=@user2,#channel2', {
'instance': plugins.NotifyRocketChat,
}),
('rockets://web/token@localhost/?avatar=No', {
# a simple webhook token with default values
'instance': plugins.NotifyRocketChat,
# Our expected url(privacy=True) startswith() response:
'privacy_url': 'rockets://w...n@localhost',
}),
('rockets://localhost/@user/?mode=webhook&webhook=web/token', {
'instance': plugins.NotifyRocketChat,
}),
('rockets://user:web/token@localhost/@user/?mode=invalid', {
# invalid mode
'instance': TypeError,
}),
('rocket://user:pass@localhost:8081/room1/room2', {
'instance': plugins.NotifyRocketChat,
# force a failure using basic mode
'response': False,
'requests_response_code': requests.codes.internal_server_error,
}),
('rockets://user:web/token@localhost?to=@user3,#channel3', {
'instance': plugins.NotifyRocketChat,
# force a failure using webhook mode
'response': False,
'requests_response_code': requests.codes.internal_server_error,
}),
('rocket://user:pass@localhost:8082/#channel', {
'instance': plugins.NotifyRocketChat,
# throw a bizzare code forcing us to fail to look it up
'response': False,
'requests_response_code': 999,
}),
('rocket://user:pass@localhost:8083/#chan1/#chan2/room', {
'instance': plugins.NotifyRocketChat,
# Throws a series of connection and transfer exceptions when this flag
# is set and tests that we gracfully handle them
'test_requests_exceptions': True,
}),
)
def test_plugin_rocket_chat_urls():
"""
NotifyRocketChat() Apprise URLs
"""
# Run our general tests
AppriseURLTester(tests=apprise_url_tests).run_all()
@mock.patch('requests.get')
@mock.patch('requests.post')
def test_plugin_rocketchat_edge_cases(mock_post, mock_get):
"""
NotifyRocketChat() Edge Cases
"""
# Disable Throttling to speed testing
plugins.NotifyBase.request_rate_per_sec = 0
# Chat ID
recipients = 'AbcD1245, @l2g, @lead2gold, #channel, #channel2'
# Authentication
user = 'myuser'
password = 'mypass'
# Prepare Mock
mock_get.return_value = requests.Request()
mock_post.return_value = requests.Request()
mock_post.return_value.status_code = requests.codes.ok
mock_get.return_value.status_code = requests.codes.ok
mock_post.return_value.content = ''
mock_get.return_value.content = ''
obj = plugins.NotifyRocketChat(
user=user, password=password, targets=recipients)
assert isinstance(obj, plugins.NotifyRocketChat) is True
assert len(obj.channels) == 2
assert len(obj.users) == 2
assert len(obj.rooms) == 1
# No Webhook specified
with pytest.raises(TypeError):
obj = plugins.NotifyRocketChat(webhook=None, mode='webhook')
#
# Logout
#
assert obj.logout() is True
# Invalid JSON during Login
mock_post.return_value.content = '{'
mock_get.return_value.content = '}'
assert obj.login() is False
# Prepare Mock to fail
mock_post.return_value.content = ''
mock_get.return_value.content = ''
mock_post.return_value.status_code = requests.codes.internal_server_error
mock_get.return_value.status_code = requests.codes.internal_server_error
#
# Send Notification
#
assert obj.notify(
body='body', title='title', notify_type=NotifyType.INFO) is False
assert obj._send(payload='test', notify_type=NotifyType.INFO) is False
#
# Logout
#
assert obj.logout() is False
# KeyError handling
mock_post.return_value.status_code = 999
mock_get.return_value.status_code = 999
#
# Send Notification
#
assert obj.notify(
body='body', title='title', notify_type=NotifyType.INFO) is False
assert obj._send(payload='test', notify_type=NotifyType.INFO) is False
#
# Logout
#
assert obj.logout() is False
# Generate exceptions
mock_get.side_effect = requests.ConnectionError(
0, 'requests.ConnectionError() not handled')
mock_post.side_effect = mock_get.side_effect
#
# Send Notification
#
assert obj._send(payload='test', notify_type=NotifyType.INFO) is False
# Attempt the check again but fake a successful login
obj.login = mock.Mock()
obj.login.return_value = True
assert obj.notify(
body='body', title='title', notify_type=NotifyType.INFO) is False
#
# Logout
#
assert obj.logout() is False

145
test/test_plugin_ryver.py Normal file
View File

@ -0,0 +1,145 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2021 Chris Caron <lead2gold@gmail.com>
# All rights reserved.
#
# This code is licensed under the MIT License.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files(the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions :
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
import pytest
import requests
from apprise import plugins
from helpers import AppriseURLTester
# Disable logging for a cleaner testing output
import logging
logging.disable(logging.CRITICAL)
# Our Testing URLs
apprise_url_tests = (
('ryver://', {
'instance': TypeError,
}),
('ryver://:@/', {
'instance': TypeError,
}),
('ryver://apprise', {
# Just org provided (no token)
'instance': TypeError,
}),
('ryver://apprise/ckhrjW8w672m6HG?mode=invalid', {
# invalid mode provided
'instance': TypeError,
}),
('ryver://x/ckhrjW8w672m6HG?mode=slack', {
# Invalid org
'instance': TypeError,
}),
('ryver://apprise/ckhrjW8w672m6HG?mode=slack', {
# No username specified; this is still okay as we use whatever
# the user told the webhook to use; set our slack mode
'instance': plugins.NotifyRyver,
}),
('ryver://apprise/ckhrjW8w672m6HG?mode=ryver', {
# No username specified; this is still okay as we use whatever
# the user told the webhook to use; set our ryver mode
'instance': plugins.NotifyRyver,
}),
# Legacy webhook mode setting:
# Legacy webhook mode setting:
('ryver://apprise/ckhrjW8w672m6HG?webhook=slack', {
# No username specified; this is still okay as we use whatever
# the user told the webhook to use; set our slack mode
'instance': plugins.NotifyRyver,
}),
('ryver://apprise/ckhrjW8w672m6HG?webhook=ryver', {
# No username specified; this is still okay as we use whatever
# the user told the webhook to use; set our ryver mode
'instance': plugins.NotifyRyver,
# Our expected url(privacy=True) startswith() response:
'privacy_url': 'ryver://apprise/c...G',
}),
# Support Native URLs
('https://apprise.ryver.com/application/webhook/ckhrjW8w672m6HG', {
'instance': plugins.NotifyRyver,
}),
# Support Native URLs with arguments
('https://apprise.ryver.com/application/webhook/ckhrjW8w672m6HG'
'?webhook=ryver',
{
'instance': plugins.NotifyRyver,
}),
('ryver://caronc@apprise/ckhrjW8w672m6HG', {
'instance': plugins.NotifyRyver,
# don't include an image by default
'include_image': False,
}),
('ryver://apprise/ckhrjW8w672m6HG', {
'instance': plugins.NotifyRyver,
# force a failure
'response': False,
'requests_response_code': requests.codes.internal_server_error,
}),
('ryver://apprise/ckhrjW8w672m6HG', {
'instance': plugins.NotifyRyver,
# throw a bizzare code forcing us to fail to look it up
'response': False,
'requests_response_code': 999,
}),
('ryver://apprise/ckhrjW8w672m6HG', {
'instance': plugins.NotifyRyver,
# Throws a series of connection and transfer exceptions when this flag
# is set and tests that we gracfully handle them
'test_requests_exceptions': True,
}),
)
def test_plugin_ryver_urls():
"""
NotifyRyver() Apprise URLs
"""
# Run our general tests
AppriseURLTester(tests=apprise_url_tests).run_all()
def test_plugin_ryver_edge_cases():
"""
NotifyRyver() Edge Cases
"""
# Disable Throttling to speed testing
plugins.NotifyBase.request_rate_per_sec = 0
# No token
with pytest.raises(TypeError):
plugins.NotifyRyver(organization="abc", token=None)
with pytest.raises(TypeError):
plugins.NotifyRyver(organization="abc", token=" ")
# No organization
with pytest.raises(TypeError):
plugins.NotifyRyver(organization=None, token="abc")
with pytest.raises(TypeError):
plugins.NotifyRyver(organization=" ", token="abc")

View File

@ -0,0 +1,159 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2021 Chris Caron <lead2gold@gmail.com>
# All rights reserved.
#
# This code is licensed under the MIT License.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files(the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions :
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
import mock
import pytest
import requests
from apprise import plugins
from helpers import AppriseURLTester
# Disable logging for a cleaner testing output
import logging
logging.disable(logging.CRITICAL)
# a test UUID we can use
UUID4 = '8b799edf-6f98-4d3a-9be7-2862fb4e5752'
# Our Testing URLs
apprise_url_tests = (
('sendgrid://', {
'instance': None,
}),
('sendgrid://:@/', {
'instance': None,
}),
('sendgrid://abcd', {
# Just an broken email (no api key or email)
'instance': None,
}),
('sendgrid://abcd@host', {
# Just an Email specified, no API Key
'instance': None,
}),
('sendgrid://invalid-api-key+*-d:user@example.com', {
# An invalid API Key
'instance': TypeError,
}),
('sendgrid://abcd:user@example.com', {
# No To/Target Address(es) specified; so we sub in the same From
# address
'instance': plugins.NotifySendGrid,
}),
('sendgrid://abcd:user@example.com/newuser@example.com', {
# A good email
'instance': plugins.NotifySendGrid,
}),
('sendgrid://abcd:user@example.com/newuser@example.com'
'?bcc=l2g@nuxref.com', {
# A good email with Blind Carbon Copy
'instance': plugins.NotifySendGrid,
}),
('sendgrid://abcd:user@example.com/newuser@example.com'
'?cc=l2g@nuxref.com', {
# A good email with Carbon Copy
'instance': plugins.NotifySendGrid,
}),
('sendgrid://abcd:user@example.com/newuser@example.com'
'?to=l2g@nuxref.com', {
# A good email with Carbon Copy
'instance': plugins.NotifySendGrid,
}),
('sendgrid://abcd:user@example.com/newuser@example.com'
'?template={}'.format(UUID4), {
# A good email with a template + no substitutions
'instance': plugins.NotifySendGrid,
}),
('sendgrid://abcd:user@example.com/newuser@example.com'
'?template={}&+sub=value&+sub2=value2'.format(UUID4), {
# A good email with a template + substitutions
'instance': plugins.NotifySendGrid,
# Our expected url(privacy=True) startswith() response:
'privacy_url': 'sendgrid://a...d:user@example.com/',
}),
('sendgrid://abcd:user@example.ca/newuser@example.ca', {
'instance': plugins.NotifySendGrid,
# force a failure
'response': False,
'requests_response_code': requests.codes.internal_server_error,
}),
('sendgrid://abcd:user@example.uk/newuser@example.uk', {
'instance': plugins.NotifySendGrid,
# throw a bizzare code forcing us to fail to look it up
'response': False,
'requests_response_code': 999,
}),
('sendgrid://abcd:user@example.au/newuser@example.au', {
'instance': plugins.NotifySendGrid,
# Throws a series of connection and transfer exceptions when this flag
# is set and tests that we gracfully handle them
'test_requests_exceptions': True,
}),
)
def test_plugin_sendgrid_urls():
"""
NotifySendGrid() Apprise URLs
"""
# Run our general tests
AppriseURLTester(tests=apprise_url_tests).run_all()
@mock.patch('requests.get')
@mock.patch('requests.post')
def test_plugin_sendgrid_edge_cases(mock_post, mock_get):
"""
NotifySendGrid() Edge Cases
"""
# Disable Throttling to speed testing
plugins.NotifyBase.request_rate_per_sec = 0
# no apikey
with pytest.raises(TypeError):
plugins.NotifySendGrid(
apikey=None, from_email='user@example.com')
# invalid from email
with pytest.raises(TypeError):
plugins.NotifySendGrid(
apikey='abcd', from_email='!invalid')
# no email
with pytest.raises(TypeError):
plugins.NotifySendGrid(apikey='abcd', from_email=None)
# Invalid To email address
plugins.NotifySendGrid(
apikey='abcd', from_email='user@example.com', targets="!invalid")
# Test invalid bcc/cc entries mixed with good ones
assert isinstance(plugins.NotifySendGrid(
apikey='abcd',
from_email='l2g@example.com',
bcc=('abc@def.com', '!invalid'),
cc=('abc@test.org', '!invalid')), plugins.NotifySendGrid)

View File

@ -0,0 +1,176 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2019 Chris Caron <lead2gold@gmail.com>
# All rights reserved.
#
# This code is licensed under the MIT License.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files(the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions :
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
import sys
import mock
import pytest
import requests
import json
from apprise import Apprise
from apprise import plugins
from helpers import AppriseURLTester
# Disable logging for a cleaner testing output
import logging
logging.disable(logging.CRITICAL)
# Our Testing URLs
apprise_url_tests = (
('spush://', {
# No api key
'instance': TypeError,
}),
('spush://{}'.format('A' * 14), {
# API Key specified however expected server response
# didn't have 'OK' in JSON response
'instance': plugins.NotifySimplePush,
# Expected notify() response
'notify_response': False,
}),
('spush://{}'.format('Y' * 14), {
# API Key valid and expected response was valid
'instance': plugins.NotifySimplePush,
# Set our response to OK
'requests_response_text': {
'status': 'OK',
},
# Our expected url(privacy=True) startswith() response:
'privacy_url': 'spush://Y...Y/',
}),
('spush://{}?event=Not%20So%20Good'.format('X' * 14), {
# API Key valid and expected response was valid
'instance': plugins.NotifySimplePush,
# Set our response to something that is not okay
'requests_response_text': {
'status': 'NOT-OK',
},
# Expected notify() response
'notify_response': False,
}),
('spush://salt:pass@{}'.format('X' * 14), {
# Now we'll test encrypted messages with new salt
'instance': plugins.NotifySimplePush,
# Set our response to OK
'requests_response_text': {
'status': 'OK',
},
# Our expected url(privacy=True) startswith() response:
'privacy_url': 'spush://****:****@X...X/',
}),
('spush://{}'.format('Y' * 14), {
'instance': plugins.NotifySimplePush,
# throw a bizzare code forcing us to fail to look it up
'response': False,
'requests_response_code': 999,
# Set a failing message too
'requests_response_text': {
'status': 'BadRequest',
'message': 'Title or message too long',
},
}),
('spush://{}'.format('Z' * 14), {
'instance': plugins.NotifySimplePush,
# Throws a series of connection and transfer exceptions when this flag
# is set and tests that we gracfully handle them
'test_requests_exceptions': True,
}),
)
@pytest.mark.skipif(
'cryptography' not in sys.modules, reason="Requires cryptography")
def test_plugin_simplepush_urls():
"""
NotifySimplePush() Apprise URLs
"""
# Run our general tests
AppriseURLTester(tests=apprise_url_tests).run_all()
@pytest.mark.skipif(
'cryptography' in sys.modules,
reason="Requires that cryptography NOT be installed")
def test_plugin_fcm_cryptography_import_error():
"""
NotifySimplePush() Cryptography loading failure
"""
# Attempt to instantiate our object
obj = Apprise.instantiate('spush://{}'.format('Y' * 14))
# It's not possible because our cryptography depedancy is missing
assert obj is None
@pytest.mark.skipif(
'cryptography' not in sys.modules, reason="Requires cryptography")
def test_plugin_simplepush_edge_cases():
"""
NotifySimplePush() Edge Cases
"""
# Disable Throttling to speed testing
plugins.NotifyBase.request_rate_per_sec = 0
# No token
with pytest.raises(TypeError):
plugins.NotifySimplePush(apikey=None)
with pytest.raises(TypeError):
plugins.NotifySimplePush(apikey=" ")
# Bad event
with pytest.raises(TypeError):
plugins.NotifySimplePush(apikey="abc", event=object)
with pytest.raises(TypeError):
plugins.NotifySimplePush(apikey="abc", event=" ")
@pytest.mark.skipif(
'cryptography' not in sys.modules, reason="Requires cryptography")
@mock.patch('requests.post')
def test_plugin_simplepush_general(mock_post):
"""
NotifySimplePush() General Tests
"""
# Disable Throttling to speed testing
plugins.NotifyBase.request_rate_per_sec = 0
# Prepare a good response
response = mock.Mock()
response.content = json.dumps({
'status': 'OK',
})
response.status_code = requests.codes.ok
mock_post.return_value = response
obj = Apprise.instantiate('spush://{}'.format('Y' * 14))
# Verify our content works as expected
assert obj.notify(title="test", body="test") is True

178
test/test_plugin_sinch.py Normal file
View File

@ -0,0 +1,178 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2021 Chris Caron <lead2gold@gmail.com>
# All rights reserved.
#
# This code is licensed under the MIT License.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files(the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions :
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
import mock
import pytest
import requests
from json import dumps
from apprise import plugins
from helpers import AppriseURLTester
# Disable logging for a cleaner testing output
import logging
logging.disable(logging.CRITICAL)
# Our Testing URLs
apprise_url_tests = (
('sinch://', {
# No Account SID specified
'instance': TypeError,
}),
('sinch://:@/', {
# invalid Auth token
'instance': TypeError,
}),
('sinch://{}@12345678'.format('a' * 32), {
# Just spi provided
'instance': TypeError,
}),
('sinch://{}:{}@_'.format('a' * 32, 'b' * 32), {
# spi and token provided but invalid from
'instance': TypeError,
}),
('sinch://{}:{}@{}'.format('a' * 32, 'b' * 32, '3' * 5), {
# using short-code (5 characters) without a target
# We can still instantiate ourselves with a valid short code
'instance': plugins.NotifySinch,
# Expected notify() response because we have no one to notify
'notify_response': False,
}),
('sinch://{}:{}@{}'.format('a' * 32, 'b' * 32, '3' * 9), {
# spi and token provided and from but invalid from no
'instance': TypeError,
}),
('sinch://{}:{}@{}/123/{}/abcd/'.format(
'a' * 32, 'b' * 32, '3' * 11, '9' * 15), {
# valid everything but target numbers
'instance': plugins.NotifySinch,
}),
('sinch://{}:{}@12345/{}'.format('a' * 32, 'b' * 32, '4' * 11), {
# using short-code (5 characters)
'instance': plugins.NotifySinch,
# Our expected url(privacy=True) startswith() response:
'privacy_url': 'sinch://...aaaa:b...b@12345',
}),
('sinch://{}:{}@123456/{}'.format('a' * 32, 'b' * 32, '4' * 11), {
# using short-code (6 characters)
'instance': plugins.NotifySinch,
}),
('sinch://{}:{}@{}'.format('a' * 32, 'b' * 32, '5' * 11), {
# using phone no with no target - we text ourselves in
# this case
'instance': plugins.NotifySinch,
}),
('sinch://{}:{}@{}?region=eu'.format('a' * 32, 'b' * 32, '5' * 11), {
# Specify a region
'instance': plugins.NotifySinch,
}),
('sinch://{}:{}@{}?region=invalid'.format('a' * 32, 'b' * 32, '5' * 11), {
# Invalid region
'instance': TypeError,
}),
('sinch://_?spi={}&token={}&from={}'.format(
'a' * 32, 'b' * 32, '5' * 11), {
# use get args to acomplish the same thing
'instance': plugins.NotifySinch,
}),
('sinch://_?spi={}&token={}&source={}'.format(
'a' * 32, 'b' * 32, '5' * 11), {
# use get args to acomplish the same thing (use source instead of from)
'instance': plugins.NotifySinch,
}),
('sinch://_?spi={}&token={}&from={}&to={}'.format(
'a' * 32, 'b' * 32, '5' * 11, '7' * 13), {
# use to=
'instance': plugins.NotifySinch,
}),
('sinch://{}:{}@{}'.format('a' * 32, 'b' * 32, '6' * 11), {
'instance': plugins.NotifySinch,
# throw a bizzare code forcing us to fail to look it up
'response': False,
'requests_response_code': 999,
}),
('sinch://{}:{}@{}'.format('a' * 32, 'b' * 32, '6' * 11), {
'instance': plugins.NotifySinch,
# Throws a series of connection and transfer exceptions when this flag
# is set and tests that we gracfully handle them
'test_requests_exceptions': True,
}),
)
def test_plugin_sinch_urls():
"""
NotifyTemplate() Apprise URLs
"""
# Run our general tests
AppriseURLTester(tests=apprise_url_tests).run_all()
@mock.patch('requests.post')
def test_plugin_sinch_edge_cases(mock_post):
"""
NotifySinch() Edge Cases
"""
# Disable Throttling to speed testing
plugins.NotifyBase.request_rate_per_sec = 0
# Prepare our response
response = requests.Request()
response.status_code = requests.codes.ok
# Prepare Mock
mock_post.return_value = response
# Initialize some generic (but valid) tokens
service_plan_id = '{}'.format('b' * 32)
api_token = '{}'.format('b' * 32)
source = '+1 (555) 123-3456'
# No service_plan_id specified
with pytest.raises(TypeError):
plugins.NotifySinch(
service_plan_id=None, api_token=api_token, source=source)
# No api_token specified
with pytest.raises(TypeError):
plugins.NotifySinch(
service_plan_id=service_plan_id, api_token=None, source=source)
# a error response
response.status_code = 400
response.content = dumps({
'code': 21211,
'message': "The 'To' number +1234567 is not a valid phone number.",
})
mock_post.return_value = response
# Initialize our object
obj = plugins.NotifySinch(
service_plan_id=service_plan_id, api_token=api_token, source=source)
# We will fail with the above error code
assert obj.notify('title', 'body', 'info') is False

View File

@ -31,6 +31,7 @@ import requests
from apprise import plugins
from apprise import NotifyType
from apprise import AppriseAttachment
from helpers import AppriseURLTester
from json import dumps
@ -41,15 +42,233 @@ logging.disable(logging.CRITICAL)
# Attachment Directory
TEST_VAR_DIR = os.path.join(os.path.dirname(__file__), 'var')
# Our Testing URLs
apprise_url_tests = (
('slack://', {
'instance': TypeError,
}),
('slack://:@/', {
'instance': TypeError,
}),
('slack://T1JJ3T3L2', {
# Just Token 1 provided
'instance': TypeError,
}),
('slack://T1JJ3T3L2/A1BRTD4JD/', {
# Just 2 tokens provided
'instance': TypeError,
}),
('slack://T1JJ3T3L2/A1BRTD4JD/TIiajkdnlazkcOXrIdevi7FQ/#hmm/#-invalid-', {
# No username specified; this is still okay as we sub in
# default; The one invalid channel is skipped when sending a message
'instance': plugins.NotifySlack,
# There is an invalid channel that we will fail to deliver to
# as a result the response type will be false
'response': False,
'requests_response_text': {
'ok': False,
'message': 'Bad Channel',
},
}),
('slack://T1JJ3T3L2/A1BRTD4JD/TIiajkdnlazkcOXrIdevi7FQ/#channel', {
# No username specified; this is still okay as we sub in
# default; The one invalid channel is skipped when sending a message
'instance': plugins.NotifySlack,
# don't include an image by default
'include_image': False,
'requests_response_text': 'ok'
}),
('slack://T1JJ3T3L2/A1BRTD4JD/TIiajkdnlazkcOXrIdevi7FQ/+id/@id/', {
# + encoded id,
# @ userid
'instance': plugins.NotifySlack,
'requests_response_text': 'ok',
}),
('slack://username@T1JJ3T3L2/A1BRTD4JD/TIiajkdnlazkcOXrIdevi7FQ/'
'?to=#nuxref', {
'instance': plugins.NotifySlack,
# Our expected url(privacy=True) startswith() response:
'privacy_url': 'slack://username@T...2/A...D/T...Q/',
'requests_response_text': 'ok',
}),
('slack://username@T1JJ3T3L2/A1BRTD4JD/TIiajkdnlazkcOXrIdevi7FQ/#nuxref', {
'instance': plugins.NotifySlack,
'requests_response_text': 'ok',
}),
# You can't send to email using webhook
('slack://T1JJ3T3L2/A1BRTD4JD/TIiajkdnl/user@gmail.com', {
'instance': plugins.NotifySlack,
'requests_response_text': 'ok',
# we'll have a notify response failure in this case
'notify_response': False,
}),
# Specify Token on argument string (with username)
('slack://bot@_/#nuxref?token=T1JJ3T3L2/A1BRTD4JD/TIiajkdnadfdajkjkfl/', {
'instance': plugins.NotifySlack,
'requests_response_text': 'ok',
}),
# Specify Token and channels on argument string (no username)
('slack://?token=T1JJ3T3L2/A1BRTD4JD/TIiajkdnlazkcOXrIdevi7FQ/&to=#chan', {
'instance': plugins.NotifySlack,
'requests_response_text': 'ok',
}),
# Test webhook that doesn't have a proper response
('slack://username@T1JJ3T3L2/A1BRTD4JD/TIiajkdnlazkcOXrIdevi7FQ/#nuxref', {
'instance': plugins.NotifySlack,
'requests_response_text': 'fail',
# we'll have a notify response failure in this case
'notify_response': False,
}),
# Test using a bot-token (also test footer set to no flag)
('slack://username@xoxb-1234-1234-abc124/#nuxref?footer=no', {
'instance': plugins.NotifySlack,
'requests_response_text': {
'ok': True,
'message': '',
# support attachments
'file': {
'url_private': 'http://localhost/',
},
},
}),
# Test blocks mode
('slack://?token=T1JJ3T3L2/A1BRTD4JD/TIiajkdnlazkcOXrIdevi7FQ/'
'&to=#chan&blocks=yes&footer=yes',
{
'instance': plugins.NotifySlack,
'requests_response_text': 'ok'}),
('slack://?token=T1JJ3T3L2/A1BRTD4JD/TIiajkdnlazkcOXrIdevi7FQ/'
'&to=#chan&blocks=yes&footer=no',
{
'instance': plugins.NotifySlack,
'requests_response_text': 'ok'}),
('slack://?token=T1JJ3T3L2/A1BRTD4JD/TIiajkdnlazkcOXrIdevi7FQ/'
'&to=#chan&blocks=yes&footer=yes&image=no',
{
'instance': plugins.NotifySlack,
'requests_response_text': 'ok'}),
('slack://?token=T1JJ3T3L2/A1BRTD4JD/TIiajkdnlazkcOXrIdevi7FQ/'
'&to=#chan&blocks=yes&format=text',
{
'instance': plugins.NotifySlack,
'requests_response_text': 'ok'}),
('slack://?token=T1JJ3T3L2/A1BRTD4JD/TIiajkdnlazkcOXrIdevi7FQ/'
'&to=#chan&blocks=no&format=text',
{
'instance': plugins.NotifySlack,
'requests_response_text': 'ok'}),
# Test using a bot-token as argument
('slack://?token=xoxb-1234-1234-abc124&to=#nuxref&footer=no&user=test', {
'instance': plugins.NotifySlack,
'requests_response_text': {
'ok': True,
'message': '',
# support attachments
'file': {
'url_private': 'http://localhost/',
},
},
# Our expected url(privacy=True) startswith() response:
'privacy_url': 'slack://test@x...4/nuxref/',
}),
# We contain 1 or more invalid channels, so we'll fail on our notify call
('slack://?token=xoxb-1234-1234-abc124&to=#nuxref,#$,#-&footer=no', {
'instance': plugins.NotifySlack,
'requests_response_text': {
'ok': True,
'message': '',
# support attachments
'file': {
'url_private': 'http://localhost/',
},
},
# We fail because of the empty channel #$ and #-
'notify_response': False,
}),
('slack://username@xoxb-1234-1234-abc124/#nuxref', {
'instance': plugins.NotifySlack,
'requests_response_text': {
'ok': True,
'message': '',
},
# we'll fail to send attachments because we had no 'file' response in
# our object
'response': False,
}),
('slack://username@T1JJ3T3L2/A1BRTD4JD/TIiajkdnlazkcOXrIdevi7FQ', {
# Missing a channel, falls back to webhook channel bindings
'instance': plugins.NotifySlack,
'requests_response_text': 'ok',
}),
# Native URL Support, take the slack URL and still build from it
('https://hooks.slack.com/services/{}/{}/{}'.format(
'A' * 9, 'B' * 9, 'c' * 24), {
'instance': plugins.NotifySlack,
'requests_response_text': 'ok',
}),
# Native URL Support with arguments
('https://hooks.slack.com/services/{}/{}/{}?format=text'.format(
'A' * 9, 'B' * 9, 'c' * 24), {
'instance': plugins.NotifySlack,
'requests_response_text': 'ok',
}),
('slack://username@-INVALID-/A1BRTD4JD/TIiajkdnlazkcOXrIdevi7FQ/#cool', {
# invalid 1st Token
'instance': TypeError,
}),
('slack://username@T1JJ3T3L2/-INVALID-/TIiajkdnlazkcOXrIdevi7FQ/#great', {
# invalid 2rd Token
'instance': TypeError,
}),
('slack://username@T1JJ3T3L2/A1BRTD4JD/-INVALID-/#channel', {
# invalid 3rd Token
'instance': TypeError,
}),
('slack://l2g@T1JJ3T3L2/A1BRTD4JD/TIiajkdnlazkcOXrIdevi7FQ/#usenet', {
'instance': plugins.NotifySlack,
# force a failure
'response': False,
'requests_response_code': requests.codes.internal_server_error,
'requests_response_text': 'ok',
}),
('slack://respect@T1JJ3T3L2/A1BRTD4JD/TIiajkdnlazkcOXrIdevi7FQ/#a', {
'instance': plugins.NotifySlack,
# throw a bizzare code forcing us to fail to look it up
'response': False,
'requests_response_code': 999,
'requests_response_text': 'ok',
}),
('slack://notify@T1JJ3T3L2/A1BRTD4JD/TIiajkdnlazkcOXrIdevi7FQ/#b', {
'instance': plugins.NotifySlack,
# Throws a series of connection and transfer exceptions when this flag
# is set and tests that we gracfully handle them
'test_requests_exceptions': True,
'requests_response_text': 'ok',
}),
)
def test_plugin_slack_urls():
"""
NotifySlack() Apprise URLs
"""
# Run our general tests
AppriseURLTester(tests=apprise_url_tests).run_all()
@mock.patch('requests.post')
def test_slack_oauth_access_token(mock_post):
def test_plugin_slack_oauth_access_token(mock_post):
"""
API: NotifySlack() OAuth Access Token Tests
NotifySlack() OAuth Access Token Tests
"""
# Disable Throttling to speed testing
plugins.NotifyBase.request_rate_per_sec = 0
plugins.NotifySlack.request_rate_per_sec = 0
# Generate an invalid bot token
token = 'xo-invalid'
@ -166,13 +385,13 @@ def test_slack_oauth_access_token(mock_post):
@mock.patch('requests.post')
def test_slack_webhook(mock_post):
def test_plugin_slack_webhook_mode(mock_post):
"""
API: NotifySlack() Webhook Tests
NotifySlack() Webhook Mode Tests
"""
# Disable Throttling to speed testing
plugins.NotifyBase.request_rate_per_sec = 0
plugins.NotifySlack.request_rate_per_sec = 0
# Prepare Mock
mock_post.return_value = requests.Request()
@ -214,13 +433,13 @@ def test_slack_webhook(mock_post):
@mock.patch('requests.post')
@mock.patch('requests.get')
def test_slack_send_by_email(mock_get, mock_post):
def test_plugin_slack_send_by_email(mock_get, mock_post):
"""
API: NotifySlack() Send by Email Tests
NotifySlack() Send by Email Tests
"""
# Disable Throttling to speed testing
plugins.NotifyBase.request_rate_per_sec = 0
plugins.NotifySlack.request_rate_per_sec = 0
# Generate a (valid) bot token
token = 'xoxb-1234-1234-abc124'

View File

@ -31,6 +31,7 @@ from apprise import plugins
from apprise import Apprise
from apprise import AppriseAttachment
from apprise import NotifyType
from helpers import AppriseURLTester
# Disable logging for a cleaner testing output
import logging
@ -39,11 +40,124 @@ logging.disable(logging.CRITICAL)
# Attachment Directory
TEST_VAR_DIR = os.path.join(os.path.dirname(__file__), 'var')
# Our Testing URLs
apprise_url_tests = (
('smtp2go://', {
'instance': TypeError,
}),
('smtp2go://:@/', {
'instance': TypeError,
}),
# No Token specified
('smtp2go://user@localhost.localdomain', {
'instance': TypeError,
}),
# Token is valid, but no user name specified
('smtp2go://localhost.localdomain/{}-{}-{}'.format(
'a' * 32, 'b' * 8, 'c' * 8), {
'instance': TypeError,
}),
# Invalid from email address
('smtp2go://!@localhost.localdomain/{}-{}-{}'.format(
'a' * 32, 'b' * 8, 'c' * 8), {
'instance': TypeError,
}),
# No To email address, but everything else is valid
('smtp2go://user@localhost.localdomain/{}-{}-{}'.format(
'a' * 32, 'b' * 8, 'c' * 8), {
'instance': plugins.NotifySMTP2Go,
}),
('smtp2go://user@localhost.localdomain/{}-{}-{}?format=markdown'.format(
'a' * 32, 'b' * 8, 'c' * 8), {
'instance': plugins.NotifySMTP2Go,
}),
('smtp2go://user@localhost.localdomain/{}-{}-{}?format=html'.format(
'a' * 32, 'b' * 8, 'c' * 8), {
'instance': plugins.NotifySMTP2Go,
}),
('smtp2go://user@localhost.localdomain/{}-{}-{}?format=text'.format(
'a' * 32, 'b' * 8, 'c' * 8), {
'instance': plugins.NotifySMTP2Go,
}),
# headers
('smtp2go://user@localhost.localdomain/{}-{}-{}'
'?+X-Customer-Campaign-ID=Apprise'.format(
'a' * 32, 'b' * 8, 'c' * 8), {
'instance': plugins.NotifySMTP2Go,
}),
# bcc and cc
('smtp2go://user@localhost.localdomain/{}-{}-{}'
'?bcc=user@example.com&cc=user2@example.com'.format(
'a' * 32, 'b' * 8, 'c' * 8), {
'instance': plugins.NotifySMTP2Go,
}),
# One To Email address
('smtp2go://user@localhost.localdomain/{}-{}-{}/test@example.com'.format(
'a' * 32, 'b' * 8, 'c' * 8), {
'instance': plugins.NotifySMTP2Go,
}),
('smtp2go://user@localhost.localdomain/'
'{}-{}-{}?to=test@example.com'.format(
'a' * 32, 'b' * 8, 'c' * 8), {
'instance': plugins.NotifySMTP2Go}),
# One To Email address, a from name specified too
('smtp2go://user@localhost.localdomain/{}-{}-{}/'
'test@example.com?name="Frodo"'.format(
'a' * 32, 'b' * 8, 'c' * 8), {
'instance': plugins.NotifySMTP2Go}),
# Invalid 'To' Email address
('smtp2go://user@localhost.localdomain/{}-{}-{}/invalid'.format(
'a' * 32, 'b' * 8, 'c' * 8), {
'instance': plugins.NotifySMTP2Go,
# Expected notify() response
'notify_response': False,
}),
# Multiple 'To', 'Cc', and 'Bcc' addresses (with invalid ones)
('smtp2go://user@example.com/{}-{}-{}/{}?bcc={}&cc={}'.format(
'a' * 32, 'b' * 8, 'c' * 8,
'/'.join(('user1@example.com', 'invalid', 'User2:user2@example.com')),
','.join(('user3@example.com', 'i@v', 'User1:user1@example.com')),
','.join(('user4@example.com', 'g@r@b', 'Da:user5@example.com'))), {
'instance': plugins.NotifySMTP2Go,
}),
('smtp2go://user@localhost.localdomain/{}-{}-{}'.format(
'a' * 32, 'b' * 8, 'c' * 8), {
'instance': plugins.NotifySMTP2Go,
# force a failure
'response': False,
'requests_response_code': requests.codes.internal_server_error,
}),
('smtp2go://user@localhost.localdomain/{}-{}-{}'.format(
'a' * 32, 'b' * 8, 'c' * 8), {
'instance': plugins.NotifySMTP2Go,
# throw a bizzare code forcing us to fail to look it up
'response': False,
'requests_response_code': 999,
}),
('smtp2go://user@localhost.localdomain/{}-{}-{}'.format(
'a' * 32, 'b' * 8, 'c' * 8), {
'instance': plugins.NotifySMTP2Go,
# Throws a series of connection and transfer exceptions when this flag
# is set and tests that we gracfully handle them
'test_requests_exceptions': True,
}),
)
def test_plugin_smtp2go_urls():
"""
NotifySMTP2Go() Apprise URLs
"""
# Run our general tests
AppriseURLTester(tests=apprise_url_tests).run_all()
@mock.patch('requests.post')
def test_notify_smtp2go_plugin_attachments(mock_post):
def test_plugin_smtp2go_attachments(mock_post):
"""
API: NotifySMTP2Go() Attachments
NotifySMTP2Go() Attachments
"""
# Disable Throttling to speed testing

View File

@ -28,6 +28,7 @@ import pytest
import requests
from apprise import plugins
from apprise import Apprise
from helpers import AppriseURLTester
# Disable logging for a cleaner testing output
import logging
@ -38,13 +39,69 @@ TEST_ACCESS_KEY_ID = 'AHIAJGNT76XIMXDBIJYA'
TEST_ACCESS_KEY_SECRET = 'bu1dHSdO22pfaaVy/wmNsdljF4C07D3bndi9PQJ9'
TEST_REGION = 'us-east-2'
# Our Testing URLs
apprise_url_tests = (
('sns://', {
'instance': TypeError,
}),
('sns://:@/', {
'instance': TypeError,
}),
('sns://T1JJ3T3L2', {
# Just Token 1 provided
'instance': TypeError,
}),
('sns://T1JJ3TD4JD/TIiajkdnlazk7FQ/', {
# Missing a region
'instance': TypeError,
}),
('sns://T1JJ3T3L2/A1BRTD4JD/TIiajkdnlazkcevi7FQ/us-west-2/12223334444', {
# we have a valid URL and one number to text
'instance': plugins.NotifySNS,
}),
('sns://T1JJ3TD4JD/TIiajkdnlazk7FQ/us-west-2/12223334444/12223334445', {
# Multi SNS Suppport
'instance': plugins.NotifySNS,
# Our expected url(privacy=True) startswith() response:
'privacy_url': 'sns://T...D/****/us-west-2',
}),
('sns://T1JJ3T3L2/A1BRTD4JD/TIiajkdnlazkcOXrIdevi7FQ/us-east-1'
'?to=12223334444', {
# Missing a topic and/or phone No
'instance': plugins.NotifySNS,
}),
('sns://T1JJ3T3L2/A1BRTD4JD/TIiajkdnlazkcevi7FQ/us-west-2/12223334444', {
'instance': plugins.NotifySNS,
# throw a bizzare code forcing us to fail to look it up
'response': False,
'requests_response_code': 999,
}),
('sns://T1JJ3T3L2/A1BRTD4JD/TIiajkdnlazkcevi7FQ/us-west-2/15556667777', {
'instance': plugins.NotifySNS,
# Throws a series of connection and transfer exceptions when this flag
# is set and tests that we gracfully handle them
'test_requests_exceptions': True,
}),
)
def test_plugin_sns_urls():
"""
NotifySNS() Apprise URLs
"""
# Run our general tests
AppriseURLTester(tests=apprise_url_tests).run_all()
# We initialize a post object just incase a test fails below
# we don't want it sending any notifications upstream
@mock.patch('requests.post')
def test_object_initialization(mock_post):
def test_plugin_sns_edge_cases(mock_post):
"""
API: NotifySNS Plugin() initialization
NotifySNS() Edge Cases
"""
@ -112,9 +169,9 @@ def test_object_initialization(mock_post):
assert obj.notify(body='test', title='test') is False
def test_url_parsing():
def test_plugin_sns_url_parsing():
"""
API: NotifySNS Plugin() URL Parsing
NotifySNS() URL Parsing
"""
@ -156,9 +213,9 @@ def test_url_parsing():
assert TEST_ACCESS_KEY_SECRET == results['secret_access_key']
def test_object_parsing():
def test_plugin_sns_object_parsing():
"""
API: NotifySNS Plugin() Object Parsing
NotifySNS() Object Parsing
"""
@ -183,9 +240,9 @@ def test_object_parsing():
assert len(a) == 3
def test_aws_response_handling():
def test_plugin_sns_aws_response_handling():
"""
API: NotifySNS Plugin() AWS Response Handling
NotifySNS() AWS Response Handling
"""
# Not a string
@ -261,9 +318,9 @@ def test_aws_response_handling():
@mock.patch('requests.post')
def test_aws_topic_handling(mock_post):
def test_plugin_sns_aws_topic_handling(mock_post):
"""
API: NotifySNS Plugin() AWS Topic Handling
NotifySNS() AWS Topic Handling
"""
# Disable Throttling to speed testing

View File

@ -0,0 +1,408 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2020 Chris Caron <lead2gold@gmail.com>
# All rights reserved.
#
# This code is licensed under the MIT License.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files(the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions :
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
import os
import pytest
import mock
import requests
from json import dumps
from apprise import plugins
from apprise import Apprise
from apprise import AppriseAttachment
from apprise import NotifyType
from helpers import AppriseURLTester
# Disable logging for a cleaner testing output
import logging
logging.disable(logging.CRITICAL)
# Attachment Directory
TEST_VAR_DIR = os.path.join(os.path.dirname(__file__), 'var')
# Our Testing URLs
apprise_url_tests = (
('sparkpost://', {
'instance': TypeError,
}),
('sparkpost://:@/', {
'instance': TypeError,
}),
# No Token specified
('sparkpost://user@localhost.localdomain', {
'instance': TypeError,
}),
# Token is valid, but no user name specified
('sparkpost://localhost.localdomain/{}'.format('a' * 32), {
'instance': TypeError,
}),
# Invalid from email address
('sparkpost://!@localhost.localdomain/{}'.format('b' * 32), {
'instance': TypeError,
}),
# No To email address, but everything else is valid
('sparkpost://user@localhost.localdomain/{}'.format('c' * 32), {
'instance': plugins.NotifySparkPost,
'requests_response_text': {
"results": {
"total_rejected_recipients": 0,
"total_accepted_recipients": 1,
"id": "11668787484950529"
}
},
}),
('sparkpost://user@localhost.localdomain/{}?format=markdown'
.format('d' * 32), {
'instance': plugins.NotifySparkPost,
'requests_response_text': {
"results": {
"total_rejected_recipients": 0,
"total_accepted_recipients": 1,
"id": "11668787484950529"
}
},
}),
('sparkpost://user@localhost.localdomain/{}?format=html'
.format('d' * 32), {
'instance': plugins.NotifySparkPost,
'requests_response_text': {
"results": {
"total_rejected_recipients": 0,
"total_accepted_recipients": 1,
"id": "11668787484950529"
}
},
}),
('sparkpost://user@localhost.localdomain/{}?format=text'
.format('d' * 32), {
'instance': plugins.NotifySparkPost,
'requests_response_text': {
"results": {
"total_rejected_recipients": 0,
"total_accepted_recipients": 1,
"id": "11668787484950529"
}
},
}),
# valid url with region specified (case insensitve)
('sparkpost://user@localhost.localdomain/{}?region=uS'.format('d' * 32), {
'instance': plugins.NotifySparkPost,
'requests_response_text': {
"results": {
"total_rejected_recipients": 0,
"total_accepted_recipients": 1,
"id": "11668787484950529"
}
},
}),
# valid url with region specified (case insensitve)
('sparkpost://user@localhost.localdomain/{}?region=EU'.format('e' * 32), {
'instance': plugins.NotifySparkPost,
'requests_response_text': {
"results": {
"total_rejected_recipients": 0,
"total_accepted_recipients": 1,
"id": "11668787484950529"
}
},
}),
# headers
('sparkpost://user@localhost.localdomain/{}'
'?+X-Customer-Campaign-ID=Apprise'.format('f' * 32), {
'instance': plugins.NotifySparkPost,
'requests_response_text': {
"results": {
"total_rejected_recipients": 0,
"total_accepted_recipients": 1,
"id": "11668787484950529"
}
},
}),
# template tokens
('sparkpost://user@localhost.localdomain/{}'
'?:name=Chris&:status=admin'.format('g' * 32), {
'instance': plugins.NotifySparkPost,
'requests_response_text': {
"results": {
"total_rejected_recipients": 0,
"total_accepted_recipients": 1,
"id": "11668787484950529"
}
},
}),
# bcc and cc
('sparkpost://user@localhost.localdomain/{}'
'?bcc=user@example.com&cc=user2@example.com'.format('h' * 32), {
'instance': plugins.NotifySparkPost,
'requests_response_text': {
"results": {
"total_rejected_recipients": 0,
"total_accepted_recipients": 1,
"id": "11668787484950529"
}
},
}),
# invalid url with region specified (case insensitve)
('sparkpost://user@localhost.localdomain/{}?region=invalid'.format(
'a' * 32), {
'instance': TypeError,
}),
# One 'To' Email address
('sparkpost://user@localhost.localdomain/{}/test@example.com'.format(
'a' * 32), {
'instance': plugins.NotifySparkPost,
'requests_response_text': {
"results": {
"total_rejected_recipients": 0,
"total_accepted_recipients": 1,
"id": "11668787484950529"
}
},
}),
# Invalid 'To' Email address
('sparkpost://user@localhost.localdomain/{}/invalid'.format(
'i' * 32), {
'instance': plugins.NotifySparkPost,
# Expected notify() response
'notify_response': False,
}),
# Multiple 'To', 'Cc', and 'Bcc' addresses (with invalid ones)
('sparkpost://user@example.com/{}/{}?bcc={}&cc={}'.format(
'j' * 32,
'/'.join(('user1@example.com', 'invalid', 'User2:user2@example.com')),
','.join(('user3@example.com', 'i@v', 'User1:user1@example.com')),
','.join(('user4@example.com', 'g@r@b', 'Da:user5@example.com'))), {
'instance': plugins.NotifySparkPost,
'requests_response_text': {
"results": {
"total_rejected_recipients": 0,
"total_accepted_recipients": 1,
"id": "11668787484950529"
}
},
}),
('sparkpost://user@localhost.localdomain/'
'{}?to=test@example.com'.format('k' * 32), {
'instance': plugins.NotifySparkPost,
'requests_response_text': {
"results": {
"total_rejected_recipients": 0,
"total_accepted_recipients": 1,
"id": "11668787484950529"
}
},
}),
# One To Email address, a from name specified too
('sparkpost://user@localhost.localdomain/{}/'
'test@example.com?name="Frodo"'.format('l' * 32), {
'instance': plugins.NotifySparkPost,
'requests_response_text': {
"results": {
"total_rejected_recipients": 0,
"total_accepted_recipients": 1,
"id": "11668787484950529"
}
},
}),
# Test invalid JSON response
('sparkpost://user@localhost.localdomain/{}'.format('m' * 32), {
'instance': plugins.NotifySparkPost,
'requests_response_text': "{",
}),
('sparkpost://user@localhost.localdomain/{}'.format('n' * 32), {
'instance': plugins.NotifySparkPost,
# force a failure
'response': False,
'requests_response_code': requests.codes.internal_server_error,
}),
('sparkpost://user@localhost.localdomain/{}'.format('o' * 32), {
'instance': plugins.NotifySparkPost,
# throw a bizzare code forcing us to fail to look it up
'response': False,
'requests_response_code': 999,
}),
('sparkpost://user@localhost.localdomain/{}'.format('p' * 32), {
'instance': plugins.NotifySparkPost,
# Throws a series of connection and transfer exceptions when this flag
# is set and tests that we gracfully handle them
'test_requests_exceptions': True,
}),
)
def test_plugin_sparkpost_urls():
"""
NotifySparkPost() Apprise URLs
"""
# Run our general tests
AppriseURLTester(tests=apprise_url_tests).run_all()
@mock.patch('requests.post')
def test_plugin_sparkpost_throttling(mock_post):
"""
NotifySparkPost() Throttling
"""
# Disable Throttling to speed testing
plugins.NotifyBase.request_rate_per_sec = 0
plugins.NotifySparkPost.sparkpost_retry_wait_sec = 0.1
plugins.NotifySparkPost.sparkpost_retry_attempts = 3
# API Key
apikey = 'abc123'
user = 'user'
host = 'example.com'
targets = '{}@{}'.format(user, host)
# Exception should be thrown about the fact no user was specified
with pytest.raises(TypeError):
plugins.NotifySparkPost(
apikey=apikey, targets=targets, host=host)
# Exception should be thrown about the fact no private key was specified
with pytest.raises(TypeError):
plugins.NotifySparkPost(
apikey=None, targets=targets, user=user, host=host)
okay_response = requests.Request()
okay_response.status_code = requests.codes.ok
okay_response.content = dumps({
"results": {
"total_rejected_recipients": 0,
"total_accepted_recipients": 1,
"id": "11668787484950529"
}
})
retry_response = requests.Request()
retry_response.status_code = requests.codes.too_many_requests
retry_response.content = dumps({
"errors": [
{
"description":
"Unconfigured or unverified sending domain.",
"code": "7001",
"message": "Invalid domain"
}
]
})
# Prepare Mock (force 2 retry responses and then one okay)
mock_post.side_effect = \
(retry_response, retry_response, okay_response)
obj = Apprise.instantiate(
'sparkpost://user@localhost.localdomain/{}'.format(apikey))
assert isinstance(obj, plugins.NotifySparkPost)
# We'll successfully perform the notification as we're within
# our retry limit
assert obj.notify('test') is True
mock_post.reset_mock()
mock_post.side_effect = \
(retry_response, retry_response, retry_response)
# Now we are less than our expected limit check so we will fail
assert obj.notify('test') is False
@mock.patch('requests.post')
def test_plugin_sparkpost_attachments(mock_post):
"""
NotifySparkPost() Attachments
"""
# Disable Throttling to speed testing
plugins.NotifyBase.request_rate_per_sec = 0
plugins.NotifySparkPost.sparkpost_retry_wait_sec = 0.1
plugins.NotifySparkPost.sparkpost_retry_attempts = 3
okay_response = requests.Request()
okay_response.status_code = requests.codes.ok
okay_response.content = dumps({
"results": {
"total_rejected_recipients": 0,
"total_accepted_recipients": 1,
"id": "11668787484950529"
}
})
# Assign our mock object our return value
mock_post.return_value = okay_response
# API Key
apikey = 'abc123'
obj = Apprise.instantiate(
'sparkpost://user@localhost.localdomain/{}'.format(apikey))
assert isinstance(obj, plugins.NotifySparkPost)
# Test Valid Attachment
path = os.path.join(TEST_VAR_DIR, 'apprise-test.gif')
attach = AppriseAttachment(path)
assert obj.notify(
body='body', title='title', notify_type=NotifyType.INFO,
attach=attach) is True
# Test invalid attachment
path = os.path.join(TEST_VAR_DIR, '/invalid/path/to/an/invalid/file.jpg')
assert obj.notify(
body='body', title='title', notify_type=NotifyType.INFO,
attach=path) is False
with mock.patch('base64.b64encode', side_effect=OSError()):
# We can't send the message if we fail to parse the data
assert obj.notify(
body='body', title='title', notify_type=NotifyType.INFO,
attach=attach) is False
obj = Apprise.instantiate(
'sparkpost://no-reply@example.com/{}/'
'user1@example.com/user2@example.com?batch=yes'.format(apikey))
assert isinstance(obj, plugins.NotifySparkPost)
# Force our batch to break into separate messages
obj.default_batch_size = 1
# We'll send 2 messages
mock_post.reset_mock()
assert obj.notify(
body='body', title='title', notify_type=NotifyType.INFO,
attach=attach) is True
assert mock_post.call_count == 2
# single batch
mock_post.reset_mock()
# We'll send 1 message
obj.default_batch_size = 2
assert obj.notify(
body='body', title='title', notify_type=NotifyType.INFO,
attach=attach) is True
assert mock_post.call_count == 1

118
test/test_plugin_spontit.py Normal file
View File

@ -0,0 +1,118 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2021 Chris Caron <lead2gold@gmail.com>
# All rights reserved.
#
# This code is licensed under the MIT License.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files(the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions :
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
import requests
from apprise import plugins
from helpers import AppriseURLTester
# Disable logging for a cleaner testing output
import logging
logging.disable(logging.CRITICAL)
# Our Testing URLs
apprise_url_tests = (
('spontit://', {
# invalid url
'instance': TypeError,
}),
# Another bad url
('spontit://:@/', {
'instance': TypeError,
}),
# No user specified
('spontit://%s' % ('a' * 100), {
'instance': TypeError,
}),
# Invalid API Key specified
('spontit://user@%%20_', {
'instance': TypeError,
}),
# Provide a valid user and API Key
('spontit://%s@%s' % ('u' * 11, 'b' * 100), {
'instance': plugins.NotifySpontit,
# Our expected url(privacy=True) startswith() response:
'privacy_url': 'spontit://{}@b...b/'.format('u' * 11),
}),
# Provide a valid user and API Key, but provide an invalid channel
('spontit://%s@%s/#!!' % ('u' * 11, 'b' * 100), {
# An instance is still created, but the channel won't be notified
'instance': plugins.NotifySpontit,
}),
# Provide a valid user, API Key and a valid channel
('spontit://%s@%s/#abcd' % ('u' * 11, 'b' * 100), {
'instance': plugins.NotifySpontit,
}),
# Provide a valid user, API Key, and a subtitle
('spontit://%s@%s/?subtitle=Test' % ('u' * 11, 'b' * 100), {
'instance': plugins.NotifySpontit,
}),
# Provide a valid user, API Key, and a lengthy subtitle
('spontit://%s@%s/?subtitle=%s' % ('u' * 11, 'b' * 100, 'c' * 300), {
'instance': plugins.NotifySpontit,
}),
# Provide a valid user and API Key, but provide a valid channel (that is
# not ours).
# Spontit uses a slash (/) to delimite the user from the channel id when
# specifying channel entries. For Apprise we need to encode this
# so we convert the slash (/) into %2F
('spontit://{}@{}/#1245%2Fabcd'.format('u' * 11, 'b' * 100), {
'instance': plugins.NotifySpontit,
}),
# Provide multipe channels
('spontit://{}@{}/#1245%2Fabcd/defg'.format('u' * 11, 'b' * 100), {
'instance': plugins.NotifySpontit,
}),
# Provide multipe channels through the use of the to= variable
('spontit://{}@{}/?to=#1245/abcd'.format('u' * 11, 'b' * 100), {
'instance': plugins.NotifySpontit,
}),
('spontit://%s@%s' % ('u' * 11, 'b' * 100), {
'instance': plugins.NotifySpontit,
# force a failure
'response': False,
'requests_response_code': requests.codes.internal_server_error,
}),
('spontit://%s@%s' % ('u' * 11, 'b' * 100), {
'instance': plugins.NotifySpontit,
# throw a bizzare code forcing us to fail to look it up
'response': False,
'requests_response_code': 999,
}),
('spontit://%s@%s' % ('u' * 11, 'b' * 100), {
'instance': plugins.NotifySpontit,
# Throws a series of connection and transfer exceptions when this flag
# is set and tests that we gracfully handle them
'test_requests_exceptions': True,
}),
)
def test_plugin_spontit_urls():
"""
NotifySpontit() Apprise URLs
"""
# Run our general tests
AppriseURLTester(tests=apprise_url_tests).run_all()

View File

@ -0,0 +1,125 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2021 Chris Caron <lead2gold@gmail.com>
# All rights reserved.
#
# This code is licensed under the MIT License.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files(the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions :
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
from apprise import plugins
from helpers import AppriseURLTester
# Disable logging for a cleaner testing output
import logging
logging.disable(logging.CRITICAL)
# Our Testing URLs
apprise_url_tests = (
('strmlabs://', {
# No Access Token specified
'instance': TypeError,
}),
('strmlabs://a_bd_/', {
# invalid Access Token
'instance': TypeError,
}),
('strmlabs://IcIcArukDQtuC1is1X1UdKZjTg118Lag2vScOmso', {
# access token
'instance': plugins.NotifyStreamlabs,
# Our expected url(privacy=True) startswith() response:
'privacy_url': 'strmlabs://I...o',
}),
# Test incorrect currency
('strmlabs://IcIcArukDQtuC1is1X1UdKZjTg118Lag2vScOmso/?currency=ABCD', {
'instance': TypeError,
}),
# Test complete params - donations
('strmlabs://IcIcArukDQtuC1is1X1UdKZjTg118Lag2vScOmso/'
'?name=tt&identifier=pyt&amount=20&currency=USD&call=donations',
{'instance': plugins.NotifyStreamlabs, }),
# Test complete params - donations
('strmlabs://IcIcArukDQtuC1is1X1UdKZjTg118Lag2vScOmso/'
'?image_href=https://example.org/rms.jpg'
'&sound_href=https://example.org/rms.mp3',
{'instance': plugins.NotifyStreamlabs, }),
# Test complete params - alerts
('strmlabs://IcIcArukDQtuC1is1X1UdKZjTg118Lag2vScOmso/'
'?duration=1000&image_href=&'
'sound_href=&alert_type=donation&special_text_color=crimson',
{'instance': plugins.NotifyStreamlabs, }),
# Test incorrect call
('strmlabs://IcIcArukDQtuC1is1X1UdKZjTg118Lag2vScOmso/'
'?name=tt&identifier=pyt&amount=20&currency=USD&call=rms',
{'instance': TypeError, }),
# Test incorrect alert_type
('strmlabs://IcIcArukDQtuC1is1X1UdKZjTg118Lag2vScOmso/'
'?name=tt&identifier=pyt&amount=20&currency=USD&alert_type=rms',
{'instance': TypeError, }),
# Test incorrect name
('strmlabs://IcIcArukDQtuC1is1X1UdKZjTg118Lag2vScOmso/?name=t', {
'instance': TypeError,
}),
('strmlabs://IcIcArukDQtuC1is1X1UdKZjTg118Lag2vScOmso/?call=donations', {
'instance': plugins.NotifyStreamlabs,
# A failure has status set to zero
# Test without an 'error' flag
'requests_response_text': {
'status': 0,
},
# throw a bizzare code forcing us to fail to look it up
'response': False,
'requests_response_code': 999,
}),
('strmlabs://IcIcArukDQtuC1is1X1UdKZjTg118Lag2vScOmso/?call=alerts', {
'instance': plugins.NotifyStreamlabs,
# A failure has status set to zero
# Test without an 'error' flag
'requests_response_text': {
'status': 0,
},
# throw a bizzare code forcing us to fail to look it up
'response': False,
'requests_response_code': 999,
}),
('strmlabs://IcIcArukDQtuC1is1X1UdKZjTg118Lag2vScOmso/?call=alerts', {
'instance': plugins.NotifyStreamlabs,
# Throws a series of connection and transfer exceptions when this flag
# is set and tests that we gracfully handle them
'test_requests_exceptions': True,
}),
('strmlabs://IcIcArukDQtuC1is1X1UdKZjTg118Lag2vScOmso/?call=donations', {
'instance': plugins.NotifyStreamlabs,
# Throws a series of connection and transfer exceptions when this flag
# is set and tests that we gracfully handle them
'test_requests_exceptions': True,
}),
)
def test_plugin_streamlabs_urls():
"""
NotifyStreamlabs() Apprise URLs
"""
# Run our general tests
AppriseURLTester(tests=apprise_url_tests).run_all()

View File

@ -36,9 +36,9 @@ logging.disable(logging.CRITICAL)
@mock.patch('syslog.syslog')
@mock.patch('syslog.openlog')
def test_notify_syslog_by_url(openlog, syslog):
def test_plugin_syslog_by_url(openlog, syslog):
"""
API: Syslog URL Testing
NotifySyslog() Apprise URLs
"""
# an invalid URL
@ -105,9 +105,9 @@ def test_notify_syslog_by_url(openlog, syslog):
@mock.patch('syslog.syslog')
@mock.patch('syslog.openlog')
def test_notify_syslog_by_class(openlog, syslog):
def test_plugin_syslog_edge_cases(openlog, syslog):
"""
API: Syslog Class Testing
NotifySyslog() Edge Cases
"""
@ -131,10 +131,10 @@ def test_notify_syslog_by_class(openlog, syslog):
@mock.patch('syslog.openlog')
@mock.patch('socket.socket')
@mock.patch('os.getpid')
def test_notify_syslog_remote(
def test_plugin_syslog_remote(
mock_getpid, mock_socket, mock_openlog, mock_syslog):
"""
API: Syslog Remote Testing
NotifySyslog() Remote Testing
"""
payload = "test"

View File

@ -0,0 +1,85 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2021 Chris Caron <lead2gold@gmail.com>
# All rights reserved.
#
# This code is licensed under the MIT License.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files(the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions :
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
import requests
from apprise import plugins
from helpers import AppriseURLTester
# Disable logging for a cleaner testing output
import logging
logging.disable(logging.CRITICAL)
# a test UUID we can use
UUID4 = '8b799edf-6f98-4d3a-9be7-2862fb4e5752'
# Our Testing URLs
apprise_url_tests = (
('push://', {
# Missing API Key
'instance': TypeError,
}),
# Invalid API Key
('push://%s' % ('+' * 24), {
'instance': TypeError,
}),
# APIkey
('push://%s' % UUID4, {
'instance': plugins.NotifyTechulusPush,
# Our expected url(privacy=True) startswith() response:
'privacy_url': 'push://8...2/',
}),
# API Key + bad url
('push://:@/', {
'instance': TypeError,
}),
('push://%s' % UUID4, {
'instance': plugins.NotifyTechulusPush,
# force a failure
'response': False,
'requests_response_code': requests.codes.internal_server_error,
}),
('push://%s' % UUID4, {
'instance': plugins.NotifyTechulusPush,
# throw a bizzare code forcing us to fail to look it up
'response': False,
'requests_response_code': 999,
}),
('push://%s' % UUID4, {
'instance': plugins.NotifyTechulusPush,
# Throws a series of connection and transfer exceptions when this flag
# is set and tests that we gracfully handle them
'test_requests_exceptions': True,
}),
)
def test_plugin_techulus_push_urls():
"""
NotifyTechulusPush() Apprise URLs
"""
# Run our general tests
AppriseURLTester(tests=apprise_url_tests).run_all()

View File

@ -35,6 +35,7 @@ from apprise import AppriseAttachment
from apprise import AppriseAsset
from apprise import NotifyType
from apprise import plugins
from helpers import AppriseURLTester
# Disable logging for a cleaner testing output
import logging
@ -43,14 +44,185 @@ logging.disable(logging.CRITICAL)
# Attachment Directory
TEST_VAR_DIR = os.path.join(os.path.dirname(__file__), 'var')
# Our Testing URLs
apprise_url_tests = (
##################################
# NotifyTelegram
##################################
('tgram://', {
'instance': None,
}),
# Simple Message
('tgram://123456789:abcdefg_hijklmnop/lead2gold/', {
'instance': plugins.NotifyTelegram,
}),
# Simple Message (no images)
('tgram://123456789:abcdefg_hijklmnop/lead2gold/', {
'instance': plugins.NotifyTelegram,
# don't include an image by default
'include_image': False,
}),
# Simple Message with multiple chat names
('tgram://123456789:abcdefg_hijklmnop/id1/id2/', {
'instance': plugins.NotifyTelegram,
}),
# Simple Message with multiple chat names
('tgram://123456789:abcdefg_hijklmnop/?to=id1,id2', {
'instance': plugins.NotifyTelegram,
}),
# Simple Message with an invalid chat ID
('tgram://123456789:abcdefg_hijklmnop/%$/', {
'instance': plugins.NotifyTelegram,
# Notify will fail
'response': False,
}),
# Simple Message with multiple chat ids
('tgram://123456789:abcdefg_hijklmnop/id1/id2/23423/-30/', {
'instance': plugins.NotifyTelegram,
}),
# Simple Message with multiple chat ids (no images)
('tgram://123456789:abcdefg_hijklmnop/id1/id2/23423/-30/', {
'instance': plugins.NotifyTelegram,
# don't include an image by default
'include_image': False,
}),
# Support bot keyword prefix
('tgram://bottest@123456789:abcdefg_hijklmnop/lead2gold/', {
'instance': plugins.NotifyTelegram,
}),
# Testing image
('tgram://123456789:abcdefg_hijklmnop/lead2gold/?image=Yes', {
'instance': plugins.NotifyTelegram,
}),
# Testing invalid format (fall's back to html)
('tgram://123456789:abcdefg_hijklmnop/lead2gold/?format=invalid', {
'instance': plugins.NotifyTelegram,
}),
# Testing empty format (falls back to html)
('tgram://123456789:abcdefg_hijklmnop/lead2gold/?format=', {
'instance': plugins.NotifyTelegram,
}),
# Testing valid formats
('tgram://123456789:abcdefg_hijklmnop/lead2gold/?format=markdown', {
'instance': plugins.NotifyTelegram,
}),
('tgram://123456789:abcdefg_hijklmnop/lead2gold/?format=html', {
'instance': plugins.NotifyTelegram,
}),
('tgram://123456789:abcdefg_hijklmnop/lead2gold/?format=text', {
'instance': plugins.NotifyTelegram,
}),
# Test Silent Settings
('tgram://123456789:abcdefg_hijklmnop/lead2gold/?silent=yes', {
'instance': plugins.NotifyTelegram,
}),
('tgram://123456789:abcdefg_hijklmnop/lead2gold/?silent=no', {
'instance': plugins.NotifyTelegram,
}),
# Test Web Page Preview Settings
('tgram://123456789:abcdefg_hijklmnop/lead2gold/?preview=yes', {
'instance': plugins.NotifyTelegram,
}),
('tgram://123456789:abcdefg_hijklmnop/lead2gold/?preview=no', {
'instance': plugins.NotifyTelegram,
}),
# Simple Message without image
('tgram://123456789:abcdefg_hijklmnop/lead2gold/', {
'instance': plugins.NotifyTelegram,
# don't include an image by default
'include_image': False,
}),
# Invalid Bot Token
('tgram://alpha:abcdefg_hijklmnop/lead2gold/', {
'instance': None,
}),
# AuthToken + bad url
('tgram://:@/', {
'instance': None,
}),
('tgram://123456789:abcdefg_hijklmnop/lead2gold/', {
'instance': plugins.NotifyTelegram,
# force a failure
'response': False,
'requests_response_code': requests.codes.internal_server_error,
}),
('tgram://123456789:abcdefg_hijklmnop/lead2gold/?image=Yes', {
'instance': plugins.NotifyTelegram,
# force a failure without an image specified
'include_image': False,
'response': False,
'requests_response_code': requests.codes.internal_server_error,
}),
('tgram://123456789:abcdefg_hijklmnop/id1/id2/', {
'instance': plugins.NotifyTelegram,
# force a failure with multiple chat_ids
'response': False,
'requests_response_code': requests.codes.internal_server_error,
}),
('tgram://123456789:abcdefg_hijklmnop/id1/id2/', {
'instance': plugins.NotifyTelegram,
# force a failure without an image specified
'include_image': False,
'response': False,
'requests_response_code': requests.codes.internal_server_error,
}),
('tgram://123456789:abcdefg_hijklmnop/lead2gold/', {
'instance': plugins.NotifyTelegram,
# throw a bizzare code forcing us to fail to look it up
'response': False,
'requests_response_code': 999,
}),
('tgram://123456789:abcdefg_hijklmnop/lead2gold/', {
'instance': plugins.NotifyTelegram,
# throw a bizzare code forcing us to fail to look it up without
# having an image included
'include_image': False,
'response': False,
'requests_response_code': 999,
}),
# Test with image set
('tgram://123456789:abcdefg_hijklmnop/lead2gold/?image=Yes', {
'instance': plugins.NotifyTelegram,
# throw a bizzare code forcing us to fail to look it up without
# having an image included
'include_image': True,
'response': False,
'requests_response_code': 999,
}),
('tgram://123456789:abcdefg_hijklmnop/lead2gold/', {
'instance': plugins.NotifyTelegram,
# Throws a series of connection and transfer exceptions when this flag
# is set and tests that we gracfully handle them
'test_requests_exceptions': True,
}),
('tgram://123456789:abcdefg_hijklmnop/lead2gold/?image=Yes', {
'instance': plugins.NotifyTelegram,
# Throws a series of connection and transfer exceptions when this flag
# is set and tests that we gracfully handle them without images set
'include_image': True,
'test_requests_exceptions': True,
}),
)
def test_plugin_telegram_urls():
"""
NotifyTelegram() Apprise URLs
"""
# Run our general tests
AppriseURLTester(tests=apprise_url_tests).run_all()
@mock.patch('requests.get')
@mock.patch('requests.post')
def test_notify_telegram_plugin(mock_post, mock_get):
def test_plugin_telegram_general(mock_post, mock_get):
"""
API: NotifyTelegram() Tests
NotifyTelegram() General Tests
"""
# Disable Throttling to speed testing
plugins.NotifyBase.request_rate_per_sec = 0

245
test/test_plugin_twilio.py Normal file
View File

@ -0,0 +1,245 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2021 Chris Caron <lead2gold@gmail.com>
# All rights reserved.
#
# This code is licensed under the MIT License.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files(the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions :
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
import six
import mock
import requests
import pytest
from json import dumps
from apprise import plugins
from apprise import Apprise
from helpers import AppriseURLTester
# Disable logging for a cleaner testing output
import logging
logging.disable(logging.CRITICAL)
# Our Testing URLs
apprise_url_tests = (
('twilio://', {
# No Account SID specified
'instance': TypeError,
}),
('twilio://:@/', {
# invalid Auth token
'instance': TypeError,
}),
('twilio://AC{}@12345678'.format('a' * 32), {
# Just sid provided
'instance': TypeError,
}),
('twilio://AC{}:{}@_'.format('a' * 32, 'b' * 32), {
# sid and token provided but invalid from
'instance': TypeError,
}),
('twilio://AC{}:{}@{}'.format('a' * 32, 'b' * 32, '3' * 5), {
# using short-code (5 characters) without a target
# We can still instantiate ourselves with a valid short code
'instance': plugins.NotifyTwilio,
# Since there are no targets specified we expect a False return on
# send()
'notify_response': False,
}),
('twilio://AC{}:{}@{}'.format('a' * 32, 'b' * 32, '3' * 9), {
# sid and token provided and from but invalid from no
'instance': TypeError,
}),
('twilio://AC{}:{}@{}/123/{}/abcd/'.format(
'a' * 32, 'b' * 32, '3' * 11, '9' * 15), {
# valid everything but target numbers
'instance': plugins.NotifyTwilio,
}),
('twilio://AC{}:{}@12345/{}'.format('a' * 32, 'b' * 32, '4' * 11), {
# using short-code (5 characters)
'instance': plugins.NotifyTwilio,
# Our expected url(privacy=True) startswith() response:
'privacy_url': 'twilio://...aaaa:b...b@12345',
}),
('twilio://AC{}:{}@123456/{}'.format('a' * 32, 'b' * 32, '4' * 11), {
# using short-code (6 characters)
'instance': plugins.NotifyTwilio,
}),
('twilio://AC{}:{}@{}'.format('a' * 32, 'b' * 32, '5' * 11), {
# using phone no with no target - we text ourselves in
# this case
'instance': plugins.NotifyTwilio,
}),
('twilio://_?sid=AC{}&token={}&from={}'.format(
'a' * 32, 'b' * 32, '5' * 11), {
# use get args to acomplish the same thing
'instance': plugins.NotifyTwilio,
}),
('twilio://_?sid=AC{}&token={}&source={}'.format(
'a' * 32, 'b' * 32, '5' * 11), {
# use get args to acomplish the same thing (use source instead of from)
'instance': plugins.NotifyTwilio,
}),
('twilio://_?sid=AC{}&token={}&from={}&to={}'.format(
'a' * 32, 'b' * 32, '5' * 11, '7' * 13), {
# use to=
'instance': plugins.NotifyTwilio,
}),
('twilio://AC{}:{}@{}'.format('a' * 32, 'b' * 32, '6' * 11), {
'instance': plugins.NotifyTwilio,
# throw a bizzare code forcing us to fail to look it up
'response': False,
'requests_response_code': 999,
}),
('twilio://AC{}:{}@{}'.format('a' * 32, 'b' * 32, '6' * 11), {
'instance': plugins.NotifyTwilio,
# Throws a series of connection and transfer exceptions when this flag
# is set and tests that we gracfully handle them
'test_requests_exceptions': True,
}),
)
def test_plugin_twilio_urls():
"""
NotifyTwilio() Apprise URLs
"""
# Run our general tests
AppriseURLTester(tests=apprise_url_tests).run_all()
@mock.patch('requests.post')
def test_plugin_twilio_auth(mock_post):
"""
NotifyTwilio() Auth
- account-wide auth token
- API key and its own auth token
"""
# Disable Throttling to speed testing
plugins.NotifyBase.request_rate_per_sec = 0
response = mock.Mock()
response.content = ''
response.status_code = requests.codes.ok
# Prepare Mock
mock_post.return_value = response
# Initialize some generic (but valid) tokens
account_sid = 'AC{}'.format('b' * 32)
apikey = 'SK{}'.format('b' * 32)
auth_token = '{}'.format('b' * 32)
source = '+1 (555) 123-3456'
dest = '+1 (555) 987-6543'
message_contents = "test"
# Variation of initialization without API key
obj = Apprise.instantiate(
'twilio://{}:{}@{}/{}'
.format(account_sid, auth_token, source, dest))
assert isinstance(obj, plugins.NotifyTwilio) is True
assert isinstance(obj.url(), six.string_types) is True
# Send Notification
assert obj.send(body=message_contents) is True
# Variation of initialization with API key
obj = Apprise.instantiate(
'twilio://{}:{}@{}/{}?apikey={}'
.format(account_sid, auth_token, source, dest, apikey))
assert isinstance(obj, plugins.NotifyTwilio) is True
assert isinstance(obj.url(), six.string_types) is True
# Send Notification
assert obj.send(body=message_contents) is True
# Validate expected call parameters
assert mock_post.call_count == 2
first_call = mock_post.call_args_list[0]
second_call = mock_post.call_args_list[1]
# URL and message parameters are the same for both calls
assert first_call[0][0] == \
second_call[0][0] == \
'https://api.twilio.com/2010-04-01/Accounts/{}/Messages.json'.format(
account_sid)
assert first_call[1]['data']['Body'] == \
second_call[1]['data']['Body'] == \
message_contents
assert first_call[1]['data']['From'] == \
second_call[1]['data']['From'] == \
'+15551233456'
assert first_call[1]['data']['To'] == \
second_call[1]['data']['To'] == \
'+15559876543'
# Auth differs depending on if API Key is used
assert first_call[1]['auth'] == (account_sid, auth_token)
assert second_call[1]['auth'] == (apikey, auth_token)
@mock.patch('requests.post')
def test_plugin_twilio_edge_cases(mock_post):
"""
NotifyTwilio() Edge Cases
"""
# Disable Throttling to speed testing
plugins.NotifyBase.request_rate_per_sec = 0
# Prepare our response
response = requests.Request()
response.status_code = requests.codes.ok
# Prepare Mock
mock_post.return_value = response
# Initialize some generic (but valid) tokens
account_sid = 'AC{}'.format('b' * 32)
auth_token = '{}'.format('b' * 32)
source = '+1 (555) 123-3456'
# No account_sid specified
with pytest.raises(TypeError):
plugins.NotifyTwilio(
account_sid=None, auth_token=auth_token, source=source)
# No auth_token specified
with pytest.raises(TypeError):
plugins.NotifyTwilio(
account_sid=account_sid, auth_token=None, source=source)
# a error response
response.status_code = 400
response.content = dumps({
'code': 21211,
'message': "The 'To' number +1234567 is not a valid phone number.",
})
mock_post.return_value = response
# Initialize our object
obj = plugins.NotifyTwilio(
account_sid=account_sid, auth_token=auth_token, source=source)
# We will fail with the above error code
assert obj.notify('title', 'body', 'info') is False

View File

@ -28,15 +28,83 @@ import requests
from json import dumps
from apprise import plugins
from apprise import Apprise
from helpers import AppriseURLTester
# Disable logging for a cleaner testing output
import logging
logging.disable(logging.CRITICAL)
# Our Testing URLs
apprise_url_tests = (
('twist://', {
# Missing Email and Login
'instance': None,
}),
('twist://:@/', {
'instance': None,
}),
('twist://user@example.com/', {
# No password
'instance': None,
}),
('twist://user@example.com/password', {
# Password acceptable as first entry in path
'instance': plugins.NotifyTwist,
# Expected notify() response is False because internally we would
# have failed to login
'notify_response': False,
}),
('twist://password:user1@example.com', {
# password:login acceptable
'instance': plugins.NotifyTwist,
# Expected notify() response is False because internally we would
# have failed to login
'notify_response': False,
def test_twist_plugin_init():
# Our expected url(privacy=True) startswith() response:
'privacy_url': 'twist://****:user1@example.com',
}),
('twist://password:user2@example.com', {
# password:login acceptable
'instance': plugins.NotifyTwist,
# Expected notify() response is False because internally we would
# have logged in, but we would have failed to look up the #General
# channel and workspace.
'requests_response_text': {
# Login expected response
'id': 1234,
'default_workspace': 9876,
},
'notify_response': False,
}),
('twist://password:user2@example.com', {
'instance': plugins.NotifyTwist,
# throw a bizzare code forcing us to fail to look it up
'response': False,
'requests_response_code': 999,
}),
('twist://password:user2@example.com', {
'instance': plugins.NotifyTwist,
# Throws a series of connection and transfer exceptions when this flag
# is set and tests that we gracfully handle them
'test_requests_exceptions': True,
}),
)
def test_plugin_twist_urls():
"""
API: NotifyTwist init()
NotifyTwist() Apprise URLs
"""
# Run our general tests
AppriseURLTester(tests=apprise_url_tests).run_all()
def test_plugin_twist_init():
"""
NotifyTwist() init()
"""
try:
@ -95,9 +163,9 @@ def test_twist_plugin_init():
@mock.patch('requests.get')
@mock.patch('requests.post')
def test_twist_plugin_auth(mock_post, mock_get):
def test_plugin_twist_auth(mock_post, mock_get):
"""
API: NotifyTwist login/logout()
NotifyTwist() login/logout()
"""
# Disable Throttling to speed testing
@ -214,11 +282,9 @@ def test_twist_plugin_auth(mock_post, mock_get):
@mock.patch('requests.get')
@mock.patch('requests.post')
def test_twist_plugin_cache(mock_post, mock_get):
def test_plugin_twist_cache(mock_post, mock_get):
"""
API: NotifyTwist cache()
Test cache handling
NotifyTwist() Cache Handling
"""
# Disable Throttling to speed testing
@ -303,9 +369,9 @@ def test_twist_plugin_cache(mock_post, mock_get):
@mock.patch('requests.get')
@mock.patch('requests.post')
def test_twist_plugin_fetch(mock_post, mock_get):
def test_plugin_twist_fetch(mock_post, mock_get):
"""
API: NotifyTwist fetch()
NotifyTwist() fetch()
fetch() is a wrapper that handles all kinds of edge cases and even
attempts to re-authenticate to the Twist server if our token

View File

@ -30,59 +30,176 @@ import requests
from json import dumps
from datetime import datetime
from apprise import plugins
from helpers import AppriseURLTester
# Disable logging for a cleaner testing output
import logging
logging.disable(logging.CRITICAL)
# Our Testing URLs
apprise_url_tests = (
##################################
# NotifyTwitter
##################################
('twitter://', {
# Missing Consumer API Key
'instance': TypeError,
}),
('twitter://:@/', {
'instance': TypeError,
}),
('twitter://consumer_key', {
# Missing Keys
'instance': TypeError,
}),
('twitter://consumer_key/consumer_secret/', {
# Missing Keys
'instance': TypeError,
}),
('twitter://consumer_key/consumer_secret/access_token/', {
# Missing Access Secret
'instance': TypeError,
}),
('twitter://consumer_key/consumer_secret/access_token/access_secret', {
# No user mean's we message ourselves
'instance': plugins.NotifyTwitter,
# Expected notify() response False (because we won't be able
# to detect our user)
'notify_response': False,
def test_twitter_plugin_init():
# Our expected url(privacy=True) startswith() response:
'privacy_url': 'twitter://c...y/****/a...n/****',
}),
('twitter://consumer_key/consumer_secret/access_token/access_secret'
'?cache=no', {
# No user mean's we message ourselves
'instance': plugins.NotifyTwitter,
# However we'll be okay if we return a proper response
'requests_response_text': {
'id': 12345,
'screen_name': 'test'
},
}),
('twitter://consumer_key/consumer_secret/access_token/access_secret', {
# No user mean's we message ourselves
'instance': plugins.NotifyTwitter,
# However we'll be okay if we return a proper response
'requests_response_text': {
'id': 12345,
'screen_name': 'test'
},
}),
# A duplicate of the entry above, this will cause cache to be referenced
('twitter://consumer_key/consumer_secret/access_token/access_secret', {
# No user mean's we message ourselves
'instance': plugins.NotifyTwitter,
# However we'll be okay if we return a proper response
'requests_response_text': {
'id': 12345,
'screen_name': 'test'
},
}),
# handle cases where the screen_name is missing from the response causing
# an exception during parsing
('twitter://consumer_key/consumer_secret2/access_token/access_secret', {
# No user mean's we message ourselves
'instance': plugins.NotifyTwitter,
# However we'll be okay if we return a proper response
'requests_response_text': {
'id': 12345,
},
# due to a mangled response_text we'll fail
'notify_response': False,
}),
('twitter://user@consumer_key/csecret2/access_token/access_secret/-/%/', {
# One Invalid User
'instance': plugins.NotifyTwitter,
# Expected notify() response False (because we won't be able
# to detect our user)
'notify_response': False,
}),
('twitter://user@consumer_key/csecret/access_token/access_secret'
'?cache=No', {
# No Cache
'instance': plugins.NotifyTwitter,
'requests_response_text': [{
'id': 12345,
'screen_name': 'user'
}],
}),
('twitter://user@consumer_key/csecret/access_token/access_secret', {
# We're good!
'instance': plugins.NotifyTwitter,
'requests_response_text': [{
'id': 12345,
'screen_name': 'user'
}],
}),
# A duplicate of the entry above, this will cause cache to be referenced
# for this reason, we don't even need to return a valid response
('twitter://user@consumer_key/csecret/access_token/access_secret', {
# We're identifying the same user we already sent to
'instance': plugins.NotifyTwitter,
}),
('twitter://ckey/csecret/access_token/access_secret?mode=tweet', {
# A Public Tweet
'instance': plugins.NotifyTwitter,
}),
('twitter://user@ckey/csecret/access_token/access_secret?mode=invalid', {
# An invalid mode
'instance': TypeError,
}),
('twitter://usera@consumer_key/consumer_secret/access_token/'
'access_secret/user/?to=userb', {
# We're good!
'instance': plugins.NotifyTwitter,
'requests_response_text': [{
'id': 12345,
'screen_name': 'usera'
}, {
'id': 12346,
'screen_name': 'userb'
}, {
# A garbage entry we can test exception handling on
'id': 123,
}],
}),
('twitter://ckey/csecret/access_token/access_secret', {
'instance': plugins.NotifyTwitter,
# throw a bizzare code forcing us to fail to look it up
'response': False,
'requests_response_code': 999,
}),
('twitter://ckey/csecret/access_token/access_secret', {
'instance': plugins.NotifyTwitter,
# Throws a series of connection and transfer exceptions when this flag
# is set and tests that we gracfully handle them
'test_requests_exceptions': True,
}),
('twitter://ckey/csecret/access_token/access_secret?mode=tweet', {
'instance': plugins.NotifyTwitter,
# Throws a series of connection and transfer exceptions when this flag
# is set and tests that we gracfully handle them
'test_requests_exceptions': True,
}),
)
def test_plugin_twitter_urls():
"""
API: NotifyTwitter Plugin() (pt2)
NotifyTwitter() Apprise URLs
"""
with pytest.raises(TypeError):
plugins.NotifyTwitter(
ckey=None, csecret=None, akey=None, asecret=None)
with pytest.raises(TypeError):
plugins.NotifyTwitter(
ckey='value', csecret=None, akey=None, asecret=None)
with pytest.raises(TypeError):
plugins.NotifyTwitter(
ckey='value', csecret='value', akey=None, asecret=None)
with pytest.raises(TypeError):
plugins.NotifyTwitter(
ckey='value', csecret='value', akey='value', asecret=None)
assert isinstance(
plugins.NotifyTwitter(
ckey='value', csecret='value', akey='value', asecret='value'),
plugins.NotifyTwitter,
)
assert isinstance(
plugins.NotifyTwitter(
ckey='value', csecret='value', akey='value', asecret='value',
user='l2gnux'),
plugins.NotifyTwitter,
)
# Invalid Target User
with pytest.raises(TypeError):
plugins.NotifyTwitter(
ckey='value', csecret='value', akey='value', asecret='value',
targets='%G@rB@g3')
# Run our general tests
AppriseURLTester(tests=apprise_url_tests).run_all()
@mock.patch('requests.get')
@mock.patch('requests.post')
def test_notify_twitter_plugin_general(mock_post, mock_get):
def test_plugin_twitter_general(mock_post, mock_get):
"""
API: NotifyTwitter() General Tests
NotifyTwitter() General Tests
"""
ckey = 'ckey'
@ -245,3 +362,45 @@ def test_notify_twitter_plugin_general(mock_post, mock_get):
del plugins.NotifyTwitter._whoami_cache
obj.ckey = 'different.again'
assert obj.send(body="test") is True
def test_plugin_twitter_edge_cases():
"""
NotifyTwitter() Edge Cases
"""
with pytest.raises(TypeError):
plugins.NotifyTwitter(
ckey=None, csecret=None, akey=None, asecret=None)
with pytest.raises(TypeError):
plugins.NotifyTwitter(
ckey='value', csecret=None, akey=None, asecret=None)
with pytest.raises(TypeError):
plugins.NotifyTwitter(
ckey='value', csecret='value', akey=None, asecret=None)
with pytest.raises(TypeError):
plugins.NotifyTwitter(
ckey='value', csecret='value', akey='value', asecret=None)
assert isinstance(
plugins.NotifyTwitter(
ckey='value', csecret='value', akey='value', asecret='value'),
plugins.NotifyTwitter,
)
assert isinstance(
plugins.NotifyTwitter(
ckey='value', csecret='value', akey='value', asecret='value',
user='l2gnux'),
plugins.NotifyTwitter,
)
# Invalid Target User
with pytest.raises(TypeError):
plugins.NotifyTwitter(
ckey='value', csecret='value', akey='value', asecret='value',
targets='%G@rB@g3')

View File

@ -0,0 +1,91 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2021 Chris Caron <lead2gold@gmail.com>
# All rights reserved.
#
# This code is licensed under the MIT License.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files(the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions :
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
import requests
from apprise import plugins
from helpers import AppriseURLTester
# Disable logging for a cleaner testing output
import logging
logging.disable(logging.CRITICAL)
# Our Testing URLs
apprise_url_tests = (
('wxteams://', {
# Teams Token missing
'instance': TypeError,
}),
('wxteams://:@/', {
# We don't have strict host checking on for wxteams, so this URL
# actually becomes parseable and :@ becomes a hostname.
# The below errors because a second token wasn't found
'instance': TypeError,
}),
('wxteams://{}'.format('a' * 80), {
# token provided - we're good
'instance': plugins.NotifyWebexTeams,
# Our expected url(privacy=True) startswith() response:
'privacy_url': 'wxteams://a...a/',
}),
# Support Native URLs
('https://api.ciscospark.com/v1/webhooks/incoming/{}'.format('a' * 80), {
# token provided - we're good
'instance': plugins.NotifyWebexTeams,
}),
# Support Native URLs with arguments
('https://api.ciscospark.com/v1/webhooks/incoming/{}?format=text'.format(
'a' * 80), {
# token provided - we're good
'instance': plugins.NotifyWebexTeams,
}),
('wxteams://{}'.format('a' * 80), {
'instance': plugins.NotifyWebexTeams,
# force a failure
'response': False,
'requests_response_code': requests.codes.internal_server_error,
}),
('wxteams://{}'.format('a' * 80), {
'instance': plugins.NotifyWebexTeams,
# throw a bizzare code forcing us to fail to look it up
'response': False,
'requests_response_code': 999,
}),
('wxteams://{}'.format('a' * 80), {
'instance': plugins.NotifyWebexTeams,
# Throws a series of connection and transfer exceptions when this flag
# is set and tests that we gracfully handle them
'test_requests_exceptions': True,
}),
)
def test_plugin_webex_teams_urls():
"""
NotifyWebexTeams() Apprise URLs
"""
# Run our general tests
AppriseURLTester(tests=apprise_url_tests).run_all()

View File

@ -23,12 +23,11 @@
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
import pytest
import mock
import sys
import six
import types
# Rebuild our Apprise environment
import apprise
try:
@ -47,9 +46,14 @@ import logging
logging.disable(logging.CRITICAL)
def test_windows_plugin():
@pytest.mark.skipif(sys.version_info.major <= 2, reason="Requires Python 3.x+")
@pytest.mark.skipif((
'win32api' in sys.modules or
'win32con' in sys.modules or
'win32gui' in sys.modules), reason="Requires non-windows platform")
def test_plugin_windows_mocked():
"""
API: NotifyWindows Plugin()
NotifyWindows() General Checks (via non-Windows platform)
"""
@ -116,7 +120,108 @@ def test_windows_plugin():
assert isinstance(obj.url(), six.string_types) is True
# Check that it found our mocked environments
assert obj._enabled is True
assert obj.enabled is True
# _on_destroy check
obj._on_destroy(0, '', 0, 0)
# test notifications
assert obj.notify(
title='title', body='body',
notify_type=apprise.NotifyType.INFO) is True
obj = apprise.Apprise.instantiate(
'windows://_/?image=True', suppress_exceptions=False)
obj.duration = 0
assert isinstance(obj.url(), six.string_types) is True
assert obj.notify(
title='title', body='body',
notify_type=apprise.NotifyType.INFO) is True
obj = apprise.Apprise.instantiate(
'windows://_/?image=False', suppress_exceptions=False)
obj.duration = 0
assert isinstance(obj.url(), six.string_types) is True
assert obj.notify(
title='title', body='body',
notify_type=apprise.NotifyType.INFO) is True
obj = apprise.Apprise.instantiate(
'windows://_/?duration=1', suppress_exceptions=False)
assert isinstance(obj.url(), six.string_types) is True
# loads okay
assert obj.duration == 1
assert obj.notify(
title='title', body='body',
notify_type=apprise.NotifyType.INFO) is True
obj = apprise.Apprise.instantiate(
'windows://_/?duration=invalid', suppress_exceptions=False)
# Falls back to default
assert obj.duration == obj.default_popup_duration_sec
obj = apprise.Apprise.instantiate(
'windows://_/?duration=-1', suppress_exceptions=False)
# Falls back to default
assert obj.duration == obj.default_popup_duration_sec
obj = apprise.Apprise.instantiate(
'windows://_/?duration=0', suppress_exceptions=False)
# Falls back to default
assert obj.duration == obj.default_popup_duration_sec
# To avoid slowdowns (for testing), turn it to zero for now
obj.duration = 0
# Test our loading of our icon exception; it will still allow the
# notification to be sent
win32gui.LoadImage.side_effect = AttributeError
assert obj.notify(
title='title', body='body',
notify_type=apprise.NotifyType.INFO) is True
# Undo our change
win32gui.LoadImage.side_effect = None
# Test our global exception handling
win32gui.UpdateWindow.side_effect = AttributeError
assert obj.notify(
title='title', body='body',
notify_type=apprise.NotifyType.INFO) is False
# Undo our change
win32gui.UpdateWindow.side_effect = None
# Toggle our testing for when we can't send notifications because the
# package has been made unavailable to us
obj.enabled = False
assert obj.notify(
title='title', body='body',
notify_type=apprise.NotifyType.INFO) is False
@pytest.mark.skipif(
'win32api' not in sys.modules and
'win32con' not in sys.modules and
'win32gui' not in sys.modules,
reason="Requires win32api, win32con, and win32gui")
@mock.patch('win32gui.UpdateWindow')
@mock.patch('win32gui.Shell_NotifyIcon')
@mock.patch('win32gui.LoadImage')
def test_plugin_windows_native(
mock_update_window, mock_loadimage, mock_notify):
"""
NotifyWindows() General Checks (via Windows platform)
"""
# Create our instance
obj = apprise.Apprise.instantiate('windows://', suppress_exceptions=False)
obj.duration = 0
# Test URL functionality
assert isinstance(obj.url(), six.string_types) is True
# Check that it found our mocked environments
assert obj.enabled is True
# _on_destroy check
obj._on_destroy(0, '', 0, 0)
@ -166,26 +271,29 @@ def test_windows_plugin():
# Falls back to default
assert obj.duration == obj.default_popup_duration_sec
# To avoid slowdowns (for testing), turn it to zero for now
obj.duration = 0
# Test our loading of our icon exception; it will still allow the
# notification to be sent
win32gui.LoadImage.side_effect = AttributeError
mock_loadimage.side_effect = AttributeError
assert obj.notify(
title='title', body='body',
notify_type=apprise.NotifyType.INFO) is True
# Undo our change
win32gui.LoadImage.side_effect = None
mock_loadimage.side_effect = None
# Test our global exception handling
win32gui.UpdateWindow.side_effect = AttributeError
mock_update_window.side_effect = AttributeError
assert obj.notify(
title='title', body='body',
notify_type=apprise.NotifyType.INFO) is False
# Undo our change
win32gui.UpdateWindow.side_effect = None
mock_update_window.side_effect = None
# Toggle our testing for when we can't send notifications because the
# package has been made unavailable to us
obj._enabled = False
obj.enabled = False
assert obj.notify(
title='title', body='body',
notify_type=apprise.NotifyType.INFO) is False

View File

@ -0,0 +1,191 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2021 Chris Caron <lead2gold@gmail.com>
# All rights reserved.
#
# This code is licensed under the MIT License.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files(the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions :
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
import requests
from apprise import plugins
from apprise import NotifyType
from helpers import AppriseURLTester
# Disable logging for a cleaner testing output
import logging
logging.disable(logging.CRITICAL)
# Our Testing URLs
apprise_url_tests = (
('kodi://', {
'instance': None,
}),
('kodis://', {
'instance': None,
}),
('kodi://localhost', {
'instance': plugins.NotifyXBMC,
}),
('kodi://192.168.4.1', {
# Support IPv4 Addresses
'instance': plugins.NotifyXBMC,
}),
('kodi://[2001:db8:002a:3256:adfe:05c0:0003:0006]', {
# Support IPv6 Addresses
'instance': plugins.NotifyXBMC,
}),
('kodi://user:pass@localhost', {
'instance': plugins.NotifyXBMC,
# Our expected url(privacy=True) startswith() response:
'privacy_url': 'kodi://user:****@localhost',
}),
('kodi://localhost:8080', {
'instance': plugins.NotifyXBMC,
}),
('kodi://user:pass@localhost:8080', {
'instance': plugins.NotifyXBMC,
}),
('kodis://localhost', {
'instance': plugins.NotifyXBMC,
}),
('kodis://user:pass@localhost', {
'instance': plugins.NotifyXBMC,
}),
('kodis://localhost:8080/path/', {
'instance': plugins.NotifyXBMC,
}),
('kodis://user:password@localhost:8080', {
'instance': plugins.NotifyXBMC,
# Our expected url(privacy=True) startswith() response:
'privacy_url': 'kodis://user:****@localhost:8080',
}),
('kodi://localhost', {
'instance': plugins.NotifyXBMC,
# Experement with different notification types
'notify_type': NotifyType.WARNING,
}),
('kodi://localhost', {
'instance': plugins.NotifyXBMC,
# Experement with different notification types
'notify_type': NotifyType.FAILURE,
}),
('kodis://localhost:443', {
'instance': plugins.NotifyXBMC,
# don't include an image by default
'include_image': False,
}),
('kodi://:@/', {
'instance': None,
}),
('kodi://user:pass@localhost:8081', {
'instance': plugins.NotifyXBMC,
# force a failure
'response': False,
'requests_response_code': requests.codes.internal_server_error,
}),
('kodi://user:pass@localhost:8082', {
'instance': plugins.NotifyXBMC,
# throw a bizzare code forcing us to fail to look it up
'response': False,
'requests_response_code': 999,
}),
('kodi://user:pass@localhost:8083', {
'instance': plugins.NotifyXBMC,
# Throws a series of connection and transfer exceptions when this flag
# is set and tests that we gracfully handle them
'test_requests_exceptions': True,
}),
#
# XMBC (Legacy Platform) Shares this same KODI Plugin
#
('xbmc://', {
'instance': None,
}),
('xbmc://localhost', {
'instance': plugins.NotifyXBMC,
}),
('xbmc://localhost?duration=14', {
'instance': plugins.NotifyXBMC,
}),
('xbmc://localhost?duration=invalid', {
'instance': plugins.NotifyXBMC,
}),
('xbmc://localhost?duration=-1', {
'instance': plugins.NotifyXBMC,
}),
('xbmc://user:pass@localhost', {
'instance': plugins.NotifyXBMC,
}),
('xbmc://localhost:8080', {
'instance': plugins.NotifyXBMC,
}),
('xbmc://user:pass@localhost:8080', {
'instance': plugins.NotifyXBMC,
}),
('xbmc://user@localhost', {
'instance': plugins.NotifyXBMC,
# don't include an image by default
'include_image': False,
}),
('xbmc://localhost', {
'instance': plugins.NotifyXBMC,
# Experement with different notification types
'notify_type': NotifyType.WARNING,
}),
('xbmc://localhost', {
'instance': plugins.NotifyXBMC,
# Experement with different notification types
'notify_type': NotifyType.FAILURE,
}),
('xbmc://:@/', {
'instance': None,
}),
('xbmc://user:pass@localhost:8081', {
'instance': plugins.NotifyXBMC,
# force a failure
'response': False,
'requests_response_code': requests.codes.internal_server_error,
}),
('xbmc://user:pass@localhost:8082', {
'instance': plugins.NotifyXBMC,
# throw a bizzare code forcing us to fail to look it up
'response': False,
'requests_response_code': 999,
}),
('xbmc://user:pass@localhost:8083', {
'instance': plugins.NotifyXBMC,
# Throws a series of connection and transfer exceptions when this flag
# is set and tests that we gracfully handle them
'test_requests_exceptions': True,
}),
)
def test_plugin_xbmc_kodi_urls():
"""
NotifyXBMC() Apprise URLs
"""
# Run our general tests
AppriseURLTester(tests=apprise_url_tests).run_all()

Some files were not shown because too many files have changed in this diff Show More