apprise/test/test_rest_plugins.py
2019-10-13 13:59:02 -04:00

5567 lines
186 KiB
Python

# -*- 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 six
import pytest
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 NotifyBase
from apprise import Apprise
from apprise import AppriseAsset
from apprise.common import NotifyFormat
from apprise.common import OverflowMode
# 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'
# Some exception handling we'll use
REQUEST_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'),
)
TEST_URLS = (
##################################
# NotifyBoxcar
##################################
('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,
}),
##################################
# NotifyClickSend
##################################
('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,
}),
##################################
# NotifyD7Networks
##################################
('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': TypeError,
}),
('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,
}),
##################################
# NotifyDiscord
##################################
('discord://', {
'instance': None,
}),
# 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,
}),
# Enable other options
# DEPRICATED reference to Thumbnail
('discord://%s/%s?format=markdown&footer=Yes&thumbnail=Yes' % (
'i' * 24, 't' * 64), {
'instance': plugins.NotifyDiscord,
'requests_response_code': requests.codes.no_content,
# Our expected url(privacy=True) startswith() response:
'privacy_url': 'discord://i...i/t...t/',
}),
('discord://%s/%s?format=markdown&footer=Yes&thumbnail=No' % (
'i' * 24, 't' * 64), {
'instance': plugins.NotifyDiscord,
'requests_response_code': requests.codes.no_content,
}),
# thumbnail= is depricated and image= is the proper entry
('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' % (
'i' * 24, 't' * 64), {
'instance': plugins.NotifyDiscord,
'requests_response_code': requests.codes.no_content,
# don't include an image by default
'include_image': True,
}),
('https://discordapp.com/api/webhooks/{}/{}'.format(
'0' * 10, 'B' * 40), {
# Native URL Support, take the discord URL and still build from it
'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 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,
}),
# An invalid url
('discord://:@/', {
'instance': None,
}),
('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,
}),
##################################
# NotifyEmby
##################################
# 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. It will resume in
# in test_notify_emby_plugin()
'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. It will resume in
# in test_notify_emby_plugin()
'response': False,
# Our expected url(privacy=True) startswith() response:
'privacy_url': 'embys://l2g:****@localhost',
}),
# The rest of the emby tests are in test_notify_emby_plugin()
##################################
# NotifyFaast
##################################
('faast://', {
'instance': None,
}),
# 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://:@/', {
'instance': None,
}),
('faast://%s' % ('a' * 32), {
'instance': plugins.NotifyFaast,
# force a failure
'response': False,
'requests_response_code': requests.codes.internal_server_error,
}),
('faast://%s' % ('a' * 32), {
'instance': plugins.NotifyFaast,
# throw a bizzare code forcing us to fail to look it up
'response': False,
'requests_response_code': 999,
}),
('faast://%s' % ('a' * 32), {
'instance': plugins.NotifyFaast,
# Throws a series of connection and transfer exceptions when this flag
# is set and tests that we gracfully handle them
'test_requests_exceptions': True,
}),
##################################
# NotifyFlock
##################################
# No token specified
('flock://', {
'instance': None,
}),
# 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,
}),
# An invalid url
('flock://:@/', {
'instance': None,
}),
# 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,
}),
##################################
# NotifyGitter
##################################
('gitter://', {
'instance': None,
}),
('gitter://:@/', {
'instance': None,
}),
# 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,
}),
##################################
# NotifyGotify
##################################
('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 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,
}),
##################################
# NotifyIFTTT - If This Than That
##################################
('ifttt://', {
'instance': None,
}),
# No User
('ifttt://EventID/', {
'instance': TypeError,
}),
('ifttt://:@/', {
'instance': None,
}),
# 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,
}),
##################################
# NotifyJoin
##################################
('join://', {
'instance': None,
}),
# 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 + invalid device
('join://%s/%s' % ('a' * 32, 'k' * 12), {
'instance': TypeError,
}),
# API Key + device
('join://%s/%s' % ('a' * 32, 'd' * 32), {
'instance': plugins.NotifyJoin,
# don't include an image by default
'include_image': False,
}),
# 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,
}),
# API Key + bad url
('join://:@/', {
'instance': None,
}),
('join://%s' % ('a' * 32), {
'instance': plugins.NotifyJoin,
# force a failure
'response': False,
'requests_response_code': requests.codes.internal_server_error,
}),
('join://%s' % ('a' * 32), {
'instance': plugins.NotifyJoin,
# throw a bizzare code forcing us to fail to look it up
'response': False,
'requests_response_code': 999,
}),
('join://%s' % ('a' * 32), {
'instance': plugins.NotifyJoin,
# Throws a series of connection and transfer exceptions when this flag
# is set and tests that we gracfully handle them
'test_requests_exceptions': True,
}),
##################################
# NotifyJSON
##################################
('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,
}),
('jsons://user:password@localhost:8080', {
'instance': plugins.NotifyJSON,
# Our expected url(privacy=True) startswith() response:
'privacy_url': 'jsons://user:****@localhost:8080',
}),
('json://:@/', {
'instance': None,
}),
('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,
}),
('json://localhost:8080/path?-HeaderKey=HeaderValue', {
'instance': plugins.NotifyJSON,
}),
##################################
# NotifyKODI
##################################
('kodi://', {
'instance': None,
}),
('kodis://', {
'instance': None,
}),
('kodi://localhost', {
'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,
}),
##################################
# NotifyKumulos
##################################
('kumulos://', {
# No API or Server Key specified
'instance': None,
}),
('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': None,
}),
('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,
}),
##################################
# NotifyMailgun
##################################
('mailgun://', {
'instance': None,
}),
('mailgun://:@/', {
'instance': None,
}),
# No Token specified
('mailgun://user@host', {
'instance': TypeError,
}),
# Token is valid, but no user name specified
('mailgun://host/{}-{}-{}'.format('a' * 32, 'b' * 8, 'c' * 8), {
'instance': TypeError,
}),
# Invalid from email address
('mailgun://!@host/{}-{}-{}'.format('a' * 32, 'b' * 8, 'c' * 8), {
'instance': TypeError,
}),
# No To email address, but everything else is valid
('mailgun://user@host/{}-{}-{}'.format('a' * 32, 'b' * 8, 'c' * 8), {
'instance': plugins.NotifyMailgun,
}),
# valid url with region specified (case insensitve)
('mailgun://user@host/{}-{}-{}?region=uS'.format(
'a' * 32, 'b' * 8, 'c' * 8), {
'instance': plugins.NotifyMailgun,
}),
# valid url with region specified (case insensitve)
('mailgun://user@host/{}-{}-{}?region=EU'.format(
'a' * 32, 'b' * 8, 'c' * 8), {
'instance': plugins.NotifyMailgun,
}),
# invalid url with region specified (case insensitve)
('mailgun://user@host/{}-{}-{}?region=invalid'.format(
'a' * 32, 'b' * 8, 'c' * 8), {
'instance': TypeError,
}),
# One To Email address
('mailgun://user@host/{}-{}-{}/test@example.com'.format(
'a' * 32, 'b' * 8, 'c' * 8), {
'instance': plugins.NotifyMailgun,
}),
('mailgun://user@host/{}-{}-{}?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@host/{}-{}-{}/test@example.com?name="Frodo"'.format(
'a' * 32, 'b' * 8, 'c' * 8), {
'instance': plugins.NotifyMailgun,
}),
('mailgun://user@host/{}-{}-{}'.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@host/{}-{}-{}'.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@host/{}-{}-{}'.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,
}),
##################################
# NotifyMatrix
##################################
('matrix://', {
'instance': None,
}),
('matrixs://', {
'instance': None,
}),
('matrix://localhost', {
# treats it as a anonymous user to register
'instance': plugins.NotifyMatrix,
# response is false because we have nothing to notify
'response': False,
}),
('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,
}),
# Legacy (depricated) webhook reference
('matrix://user:token@localhost?webhook=matrix&format=text', {
# user and token correctly specified with webhook
'instance': plugins.NotifyMatrix,
'response': False,
}),
('matrix://user:token@localhost?webhook=matrix&format=html', {
# user and token correctly specified with webhook
'instance': plugins.NotifyMatrix,
}),
('matrix://user:token@localhost?webhook=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,
}),
# 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,
}),
# Legacy (Depricated) image reference
('matrixs://user:token@localhost?mode=slack&thumbnail=False', {
# user and token specified; image set to True
'instance': plugins.NotifyMatrix,
}),
('matrixs://user:token@localhost?mode=slack&thumbnail=True', {
# 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,
}),
##################################
# NotifyMatterMost
##################################
('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:0/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,
}),
##################################
# 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, 'a' * 32, UUID4), {
# All tokens provided - we're good
'instance': plugins.NotifyMSTeams,
}),
# Support native URLs
('https://outlook.office.com/webhook/{}@{}/IncomingWebhook/{}/{}'
.format(UUID4, UUID4, 'a' * 32, UUID4), {
# All tokens provided - we're good
'instance': plugins.NotifyMSTeams}),
('msteams://{}@{}/{}/{}?t2'.format(UUID4, UUID4, 'a' * 32, UUID4), {
# All tokens provided - we're good
'instance': plugins.NotifyMSTeams,
# don't include an image by default
'include_image': False,
}),
('msteams://{}@{}/{}/{}?image=No'.format(UUID4, UUID4, 'a' * 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/a...a/8...2/',
}),
('msteams://{}@{}/{}/{}?tx'.format(UUID4, UUID4, 'a' * 32, UUID4), {
'instance': plugins.NotifyMSTeams,
# force a failure
'response': False,
'requests_response_code': requests.codes.internal_server_error,
}),
('msteams://{}@{}/{}/{}?ty'.format(UUID4, UUID4, 'a' * 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, 'a' * 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,
}),
##################################
# NotifyNexmo
##################################
('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,
}),
##################################
# NotifyNotifico
##################################
('notifico://', {
'instance': None,
}),
('notifico://:@/', {
'instance': None,
}),
('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,
}),
##################################
# NotifyProwl
##################################
('prowl://', {
'instance': None,
}),
# Invalid API Key
('prowl://%s' % ('a' * 20), {
'instance': TypeError,
}),
# Provider Key
('prowl://%s/%s' % ('a' * 40, 'b' * 40), {
'instance': plugins.NotifyProwl,
}),
# Invalid Provider Key
('prowl://%s/%s' % ('a' * 40, 'b' * 20), {
'instance': TypeError,
}),
# APIkey; no device
('prowl://%s' % ('a' * 40), {
'instance': plugins.NotifyProwl,
}),
# 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,
}),
# bad url
('prowl://:@/', {
'instance': None,
}),
('prowl://%s' % ('a' * 40), {
'instance': plugins.NotifyProwl,
# force a failure
'response': False,
'requests_response_code': requests.codes.internal_server_error,
}),
('prowl://%s' % ('a' * 40), {
'instance': plugins.NotifyProwl,
# throw a bizzare code forcing us to fail to look it up
'response': False,
'requests_response_code': 999,
}),
('prowl://%s' % ('a' * 40), {
'instance': plugins.NotifyProwl,
# Throws a series of connection and transfer exceptions when this flag
# is set and tests that we gracfully handle them
'test_requests_exceptions': True,
}),
##################################
# NotifyPushBullet
##################################
('pbul://', {
'instance': None,
}),
# APIkey
('pbul://%s' % ('a' * 32), {
'instance': plugins.NotifyPushBullet,
}),
# API Key + channel
('pbul://%s/#channel/' % ('a' * 32), {
'instance': plugins.NotifyPushBullet,
}),
# API Key + channel (via to=
('pbul://%s/?to=#channel' % ('a' * 32), {
'instance': plugins.NotifyPushBullet,
}),
# 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/',
}),
# API Key + device
('pbul://%s/device/' % ('a' * 32), {
'instance': plugins.NotifyPushBullet,
}),
# API Key + 2 devices
('pbul://%s/device1/device2/' % ('a' * 32), {
'instance': plugins.NotifyPushBullet,
}),
# API Key + email
('pbul://%s/user@example.com/' % ('a' * 32), {
'instance': plugins.NotifyPushBullet,
}),
# API Key + 2 emails
('pbul://%s/user@example.com/abc@def.com/' % ('a' * 32), {
'instance': plugins.NotifyPushBullet,
}),
# API Key + Combo
('pbul://%s/device/#channel/user@example.com/' % ('a' * 32), {
'instance': plugins.NotifyPushBullet,
}),
# ,
('pbul://%s' % ('a' * 32), {
'instance': plugins.NotifyPushBullet,
# force a failure
'response': False,
'requests_response_code': requests.codes.internal_server_error,
}),
('pbul://%s' % ('a' * 32), {
'instance': plugins.NotifyPushBullet,
# throw a bizzare code forcing us to fail to look it up
'response': False,
'requests_response_code': 999,
}),
('pbul://%s' % ('a' * 32), {
'instance': plugins.NotifyPushBullet,
# Throws a series of connection and transfer exceptions when this flag
# is set and tests that we gracfully handle them
'test_requests_exceptions': True,
}),
('pbul://:@/', {
'instance': None,
}),
('pbul://%s' % ('a' * 32), {
'instance': plugins.NotifyPushBullet,
# force a failure
'response': False,
'requests_response_code': requests.codes.internal_server_error,
}),
('pbul://%s' % ('a' * 32), {
'instance': plugins.NotifyPushBullet,
# throw a bizzare code forcing us to fail to look it up
'response': False,
'requests_response_code': 999,
}),
('pbul://%s' % ('a' * 32), {
'instance': plugins.NotifyPushBullet,
# Throws a series of connection and transfer exceptions when this flag
# is set and tests that we gracfully handle them
'test_requests_exceptions': True,
}),
##################################
# NotifyTechulusPush
##################################
('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,
}),
##################################
# NotifyPushed
##################################
('pushed://', {
'instance': None,
}),
# Application Key Only
('pushed://%s' % ('a' * 32), {
'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://:@/', {
'instance': None,
}),
('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,
}),
##################################
# NotifyPushjet
##################################
('pjet://', {
'instance': None,
}),
('pjets://', {
'instance': None,
}),
('pjet://:@/', {
'instance': None,
}),
# You must specify a secret key
('pjet://%s' % ('a' * 32), {
'instance': TypeError,
}),
# Legacy method of logging in (soon to be depricated)
('pjet://%s@localhost' % ('a' * 32), {
'instance': plugins.NotifyPushjet,
}),
# 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 login (no secret = fail normally)
# however this will work since we're providing depricated support
# at this time so the 'user' get's picked up as being the secret_key
('pjet://user:pass@localhost', {
'instance': plugins.NotifyPushjet,
}),
# 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,
}),
##################################
# NotifyPushover
##################################
('pover://', {
'instance': None,
}),
# 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 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 + 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,
}),
# bad url
('pover://:@/', {
'instance': None,
}),
('pover://%s@%s' % ('u' * 30, 'a' * 30), {
'instance': plugins.NotifyPushover,
# force a failure
'response': False,
'requests_response_code': requests.codes.internal_server_error,
}),
('pover://%s@%s' % ('u' * 30, 'a' * 30), {
'instance': plugins.NotifyPushover,
# throw a bizzare code forcing us to fail to look it up
'response': False,
'requests_response_code': 999,
}),
('pover://%s@%s' % ('u' * 30, 'a' * 30), {
'instance': plugins.NotifyPushover,
# Throws a series of connection and transfer exceptions when this flag
# is set and tests that we gracfully handle them
'test_requests_exceptions': True,
}),
##################################
# NotifyRocketChat
##################################
('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=No', {
'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', {
'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,
}),
##################################
# NotifyRyver
##################################
('ryver://', {
'instance': None,
}),
('ryver://:@/', {
'instance': None,
}),
('ryver://apprise', {
# Just org provided (no token)
'instance': TypeError,
}),
('ryver://apprise/ckhrjW8w672m6HG?webhook=invalid', {
# Invalid webhook provided
'instance': TypeError,
}),
('ryver://x/ckhrjW8w672m6HG?mode=slack', {
# Invalid org
'instance': TypeError,
}),
('ryver://apprise/ckhrjW8w672m6HG?mode=slack', {
# No username specified; this is still okay as we use whatever
# the user told the webhook to use; set our slack mode
'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,
}),
##################################
# NotifySendGrid
##################################
('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,
}),
##################################
# NotifySimplePush
##################################
('spush://', {
# No api key
'instance': None,
}),
('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, 'A' * 16), {
# 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,
}),
##################################
# NotifySlack
##################################
('slack://', {
'instance': None,
}),
('slack://:@/', {
'instance': None,
}),
('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,
}),
('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,
}),
('slack://T1JJ3T3L2/A1BRTD4JD/TIiajkdnlazkcOXrIdevi7FQ/+id/@id/', {
# + encoded id,
# @ userid
'instance': plugins.NotifySlack,
}),
('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/',
}),
('slack://username@T1JJ3T3L2/A1BRTD4JD/TIiajkdnlazkcOXrIdevi7FQ/#nuxref', {
'instance': plugins.NotifySlack,
}),
('slack://username@T1JJ3T3L2/A1BRTD4JD/TIiajkdnlazkcOXrIdevi7FQ', {
# Missing a channel, falls back to webhook channel bindings
'instance': plugins.NotifySlack,
}),
# 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,
}),
# Native URL Support with arguments
('https://hooks.slack.com/services/{}/{}/{}?format=text'.format(
'A' * 9, 'B' * 9, 'c' * 24), {
'instance': plugins.NotifySlack,
}),
('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,
}),
('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,
}),
('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,
}),
##################################
# NotifySNS (AWS)
##################################
('sns://', {
'instance': None,
}),
('sns://:@/', {
'instance': None,
}),
('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,
}),
##################################
# 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,
}),
# 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,
}),
##################################
# NotifyTwilio
##################################
('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': TypeError,
}),
('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,
}),
##################################
# NotifyTwist
##################################
('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,
# 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,
}),
##################################
# 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,
# 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,
}),
('tweet://consumer_key/consumer_secret/access_token/access_secret', {
# tweet:// is to be depricated; but we will support for purposes of
# generating a warning to the user; the above matches an above
# twitter:// reference so that it can use what was cached
'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,
}),
##################################
# NotifyMSG91
##################################
('msg91://', {
# No hostname/authkey specified
'instance': None,
}),
('msg91://-', {
# Invalid AuthKey
'instance': None,
}),
('msg91://{}'.format('a' * 23), {
# No number specified
'instance': TypeError,
}),
('msg91://{}/123'.format('a' * 23), {
# invalid phone number
'instance': TypeError,
}),
('msg91://{}/abcd'.format('a' * 23), {
# No number to notify
'instance': TypeError,
}),
('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,
}),
##################################
# NotifyMessageBird
##################################
('msgbird://', {
# No hostname/apikey specified
'instance': None,
}),
('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), {
# invalid target phone number; we have no one to notify
'instance': TypeError,
}),
('msgbird://{}/15551232000/123'.format('a' * 25), {
# invalid target phone number
'instance': TypeError,
}),
('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,
}),
##################################
# NotifyWebexTeams
##################################
('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,
}),
##################################
# NotifyKODI
##################################
('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,
}),
##################################
# NotifyXML
##################################
('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,
}),
('xmls://user:pass@localhost:8080', {
'instance': plugins.NotifyXML,
}),
('xml://:@/', {
'instance': None,
}),
('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,
}),
('xml://localhost:8080/path?-HeaderKey=HeaderValue', {
'instance': plugins.NotifyXML,
}),
##################################
# NotifyZulip
##################################
('zulip://', {
'instance': None,
}),
('zulip://:@/', {
'instance': None,
}),
('zulip://apprise', {
# Just org provided (no token or botname)
'instance': TypeError,
}),
('zulip://botname@apprise', {
# Just org and botname provided (no token)
'instance': TypeError,
}),
# invalid token
('zulip://botname@apprise/{}'.format('a' * 24), {
'instance': TypeError,
}),
# invalid botname
('zulip://....@apprise/{}'.format('a' * 32), {
'instance': TypeError,
}),
# Valid everything - no target so default is used
('zulip://botname@apprise/{}'.format('a' * 32), {
'instance': plugins.NotifyZulip,
# Our expected url(privacy=True) startswith() response:
'privacy_url': 'zulip://botname@apprise/a...a/',
}),
# Valid everything - organization as hostname
('zulip://botname@apprise.zulipchat.com/{}'.format('a' * 32), {
'instance': plugins.NotifyZulip,
}),
# Valid everything - 2 channels specified
('zulip://botname@apprise/{}/channel1/channel2'.format('a' * 32), {
'instance': plugins.NotifyZulip,
}),
# Valid everything - 2 channels specified (using to=)
('zulip://botname@apprise/{}/?to=channel1/channel2'.format('a' * 32), {
'instance': plugins.NotifyZulip,
}),
# Valid everything - 2 emails specified
('zulip://botname@apprise/{}/user@example.com/user2@example.com'.format(
'a' * 32), {
'instance': plugins.NotifyZulip,
}),
('zulip://botname@apprise/{}'.format('a' * 32), {
'instance': plugins.NotifyZulip,
# don't include an image by default
'include_image': False,
}),
('zulip://botname@apprise/{}'.format('a' * 32), {
'instance': plugins.NotifyZulip,
# force a failure
'response': False,
'requests_response_code': requests.codes.internal_server_error,
}),
('zulip://botname@apprise/{}'.format('a' * 32), {
'instance': plugins.NotifyZulip,
# throw a bizzare code forcing us to fail to look it up
'response': False,
'requests_response_code': 999,
}),
('zulip://botname@apprise/{}'.format('a' * 32), {
'instance': plugins.NotifyZulip,
# Throws a series of connection and transfer exceptions when this flag
# is set and tests that we gracfully handle them
'test_requests_exceptions': True,
}),
)
@mock.patch('requests.get')
@mock.patch('requests.post')
def test_rest_plugins(mock_post, mock_get):
"""
API: REST Based Plugins()
"""
# Disable Throttling to speed testing
plugins.NotifyBase.request_rate_per_sec = 0
# 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
# Create a large body and title with random data
body = ''.join(choice(str_alpha + str_num + ' ') for _ in range(body_len))
body = '\r\n'.join([body[i: i + row] for i in range(0, len(body), row)])
# Create our title using random data
title = ''.join(choice(str_alpha + str_num) for _ in range(title_len))
# iterate over our dictionary and test it out
for (url, meta) in TEST_URLS:
# 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 Notify response (True or False)
notify_response = meta.get('notify_response', response)
# Our expected privacy url
# Don't set this if don't need to check it's value
privacy_url = meta.get('privacy_url')
# 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)
# Allow notification type override, otherwise default to INFO
notify_type = meta.get('notify_type', NotifyType.INFO)
# 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)
# 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
# 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 ito
# a list of exceptions
test_requests_exceptions = REQUEST_EXCEPTIONS
try:
obj = Apprise.instantiate(
url, asset=asset, suppress_exceptions=False)
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
continue
if instance is None:
# Expected None but didn't get it
print('%s instantiated %s (but expected None)' % (
url, str(obj)))
assert False
assert isinstance(obj, instance) is True
if isinstance(obj, plugins.NotifyBase):
# We loaded okay; now lets make sure we can reverse this url
assert isinstance(obj.url(), six.string_types) is True
# 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)
# 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
#
# Stage 1: with title defined
#
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=body, title=title,
notify_type=notify_type) == notify_response
# check that this doesn't change using different overflow
# methods
assert obj.notify(
body=body, title=title,
notify_type=notify_type,
overflow=OverflowMode.UPSTREAM) == notify_response
assert obj.notify(
body=body, title=title,
notify_type=notify_type,
overflow=OverflowMode.TRUNCATE) == notify_response
assert obj.notify(
body=body, title=title,
notify_type=notify_type,
overflow=OverflowMode.SPLIT) == notify_response
else:
# Disable throttling
obj.request_rate_per_sec = 0
for _exception in REQUEST_EXCEPTIONS:
mock_post.side_effect = _exception
mock_get.side_effect = _exception
try:
assert obj.notify(
body=body, title=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
except TypeError:
print('%s Unhandled response %s' % (url, type(e)))
raise
#
# Stage 2: without 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 REQUEST_EXCEPTIONS:
mock_post.side_effect = _exception
mock_get.side_effect = _exception
try:
assert obj.notify(
body=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
# Tidy our object and allow any possible defined deconstructors to
# be executed.
del obj
except AssertionError:
# Don't mess with these entries
print('%s AssertionError' % url)
raise
except Exception as e:
# Handle our exception
if instance is None:
print('%s %s' % (url, str(e)))
raise
if not isinstance(e, instance):
print('%s %s' % (url, str(e)))
raise
@mock.patch('requests.get')
@mock.patch('requests.post')
def test_notify_boxcar_plugin(mock_post, mock_get):
"""
API: NotifyBoxcar() Extra Checks
"""
# 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
@mock.patch('requests.get')
@mock.patch('requests.post')
def test_notify_discord_plugin(mock_post, mock_get):
"""
API: NotifyDiscord() Extra Checks
"""
# Disable Throttling to speed testing
plugins.NotifyBase.request_rate_per_sec = 0
# Initialize some generic (but valid) tokens
webhook_id = 'A' * 24
webhook_token = 'B' * 64
# 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 webhook id
with pytest.raises(TypeError):
plugins.NotifyDiscord(webhook_id=None, webhook_token=webhook_token)
# Invalid webhook id (whitespace)
with pytest.raises(TypeError):
plugins.NotifyDiscord(webhook_id=" ", webhook_token=webhook_token)
# Invalid webhook token
with pytest.raises(TypeError):
plugins.NotifyDiscord(webhook_id=webhook_id, webhook_token=None)
# Invalid webhook token (whitespace)
with pytest.raises(TypeError):
plugins.NotifyDiscord(webhook_id=webhook_id, webhook_token=" ")
obj = plugins.NotifyDiscord(
webhook_id=webhook_id,
webhook_token=webhook_token,
footer=True, thumbnail=False)
# This call includes an image with it's payload:
assert obj.notify(
body='body', title='title', notify_type=NotifyType.INFO) is True
# Test our header parsing
test_markdown = "## Heading one\nbody body\n\n" + \
"# Heading 2 ##\n\nTest\n\n" + \
"more content\n" + \
"even more content \t\r\n\n\n" + \
"# Heading 3 ##\n\n\n" + \
"normal content\n" + \
"# heading 4\n" + \
"#### Heading 5"
results = obj.extract_markdown_sections(test_markdown)
assert isinstance(results, list) is True
# We should have 5 sections (since there are 5 headers identified above)
assert len(results) == 5
assert results[0]['name'] == 'Heading one'
assert results[0]['value'] == '```md\nbody body\n```'
assert results[1]['name'] == 'Heading 2'
assert results[1]['value'] == \
'```md\nTest\n\nmore content\neven more content\n```'
assert results[2]['name'] == 'Heading 3'
assert results[2]['value'] == \
'```md\nnormal content\n```'
assert results[3]['name'] == 'heading 4'
assert results[3]['value'] == '```md\n\n```'
assert results[4]['name'] == 'Heading 5'
assert results[4]['value'] == '```md\n\n```'
# Test our markdown
obj = Apprise.instantiate(
'discord://{}/{}/?format=markdown'.format(webhook_id, webhook_token))
assert isinstance(obj, plugins.NotifyDiscord)
assert obj.notify(
body=test_markdown, title='title', notify_type=NotifyType.INFO) is True
# Empty String
results = obj.extract_markdown_sections("")
assert isinstance(results, list) is True
assert len(results) == 0
# String without Heading
test_markdown = "Just a string without any header entries.\n" + \
"A second line"
results = obj.extract_markdown_sections(test_markdown)
assert isinstance(results, list) is True
assert len(results) == 0
# Use our test markdown string during a notification
assert obj.notify(
body=test_markdown, title='title', notify_type=NotifyType.INFO) is True
# Create an apprise instance
a = Apprise()
# Our processing is slightly different when we aren't using markdown
# as we do not pre-parse content during our notifications
assert a.add(
'discord://{webhook_id}/{webhook_token}/'
'?format=markdown&footer=Yes'.format(
webhook_id=webhook_id,
webhook_token=webhook_token)) is True
# This call includes an image with it's payload:
assert a.notify(body=test_markdown, title='title',
notify_type=NotifyType.INFO,
body_format=NotifyFormat.TEXT) is True
assert a.notify(body=test_markdown, title='title',
notify_type=NotifyType.INFO,
body_format=NotifyFormat.MARKDOWN) is True
# Toggle our logo availability
a.asset.image_url_logo = None
assert a.notify(
body='body', title='title', notify_type=NotifyType.INFO) is True
@mock.patch('requests.get')
@mock.patch('requests.post')
def test_notify_emby_plugin_login(mock_post, mock_get):
"""
API: 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 REQUEST_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:%d' % (
# Increment our port so it will always be something different than
# the default
plugins.NotifyEmby.emby_default_port + 1))
assert isinstance(obj, plugins.NotifyEmby)
assert obj.port == (plugins.NotifyEmby.emby_default_port + 1)
# 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 assigments
obj = Apprise.instantiate('emby://l2g:l2gpass@localhost')
assert isinstance(obj, plugins.NotifyEmby)
assert obj.port == plugins.NotifyEmby.emby_default_port
# 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_notify_emby_plugin_sessions(mock_post, mock_get, mock_logout,
mock_login):
"""
API: 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 REQUEST_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('requests.get')
@mock.patch('requests.post')
def test_notify_flock_plugin(mock_post, mock_get):
"""
API: NotifyFlock() Extra Checks
"""
# Disable Throttling to speed testing
plugins.NotifyBase.request_rate_per_sec = 0
# Initializes the plugin with an invalid token
with pytest.raises(TypeError):
plugins.NotifyFlock(token=None)
# Whitespace also acts as an invalid token value
with pytest.raises(TypeError):
plugins.NotifyFlock(token=" ")
def test_notify_gitter_plugin():
"""
API: NotifyGitter() Extra Checks
"""
# Define our channels
targets = ['apprise']
# Initializes the plugin with an invalid token
with pytest.raises(TypeError):
plugins.NotifyGitter(token=None, targets=targets)
# Whitespace also acts as an invalid token value
with pytest.raises(TypeError):
plugins.NotifyGitter(token=" ", targets=targets)
def test_notify_gotify_plugin():
"""
API: NotifyGotify() Extra Checks
"""
# Initializes the plugin with an invalid token
with pytest.raises(TypeError):
plugins.NotifyGotify(token=None)
# Whitespace also acts as an invalid token value
with pytest.raises(TypeError):
plugins.NotifyGotify(token=" ")
@mock.patch('requests.post')
def test_notify_msg91_plugin(mock_post):
"""
API: NotifyMSG91() Extra Checks
"""
# Disable Throttling to speed testing
plugins.NotifyBase.request_rate_per_sec = 0
# Prepare our response
response = requests.Request()
response.status_code = requests.codes.ok
# Prepare Mock
mock_post.return_value = response
# Initialize some generic (but valid) tokens
# authkey = '{}'.format('a' * 24)
target = '+1 (555) 123-3456'
# No authkey specified
with pytest.raises(TypeError):
plugins.NotifyMSG91(authkey=None, targets=target)
with pytest.raises(TypeError):
plugins.NotifyMSG91(authkey=" ", targets=target)
def test_notify_msteams_plugin():
"""
API: NotifyMSTeams() Extra Checks
"""
# Initializes the plugin with an invalid token
with pytest.raises(TypeError):
plugins.NotifyMSTeams(token_a=None, token_b='abcd', token_c='abcd')
# Whitespace also acts as an invalid token value
with pytest.raises(TypeError):
plugins.NotifyMSTeams(token_a=' ', token_b='abcd', token_c='abcd')
with pytest.raises(TypeError):
plugins.NotifyMSTeams(token_a='abcd', token_b=None, token_c='abcd')
# Whitespace also acts as an invalid token value
with pytest.raises(TypeError):
plugins.NotifyMSTeams(token_a='abcd', token_b=' ', token_c='abcd')
with pytest.raises(TypeError):
plugins.NotifyMSTeams(token_a='abcd', token_b='abcd', token_c=None)
# Whitespace also acts as an invalid token value
with pytest.raises(TypeError):
plugins.NotifyMSTeams(token_a='abcd', token_b='abcd', token_c=' ')
def test_notify_prowl_plugin():
"""
API: NotifyProwl() Extra Checks
"""
# Initializes the plugin with an invalid apikey
with pytest.raises(TypeError):
plugins.NotifyProwl(apikey=None)
# Whitespace also acts as an invalid apikey value
with pytest.raises(TypeError):
plugins.NotifyProwl(apikey=' ')
# Whitespace also acts as an invalid provider key
with pytest.raises(TypeError):
plugins.NotifyProwl(apikey='abcd', providerkey=object())
with pytest.raises(TypeError):
plugins.NotifyProwl(apikey='abcd', providerkey=' ')
@mock.patch('requests.post')
def test_notify_twilio_plugin(mock_post):
"""
API: NotifyTwilio() Extra Checks
"""
# Disable Throttling to speed testing
plugins.NotifyBase.request_rate_per_sec = 0
# Prepare our response
response = requests.Request()
response.status_code = requests.codes.ok
# Prepare Mock
mock_post.return_value = response
# Initialize some generic (but valid) tokens
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
@mock.patch('requests.post')
def test_notify_nexmo_plugin(mock_post):
"""
API: NotifyNexmo() Extra Checks
"""
# Disable Throttling to speed testing
plugins.NotifyBase.request_rate_per_sec = 0
# Prepare our response
response = requests.Request()
response.status_code = requests.codes.ok
# Prepare Mock
mock_post.return_value = response
# Initialize some generic (but valid) tokens
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
@mock.patch('apprise.plugins.NotifyEmby.login')
@mock.patch('requests.get')
@mock.patch('requests.post')
def test_notify_emby_plugin_logout(mock_post, mock_get, mock_login):
"""
API: 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
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 REQUEST_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
@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_notify_emby_plugin_notify(mock_post, mock_get, mock_logout,
mock_login, mock_sessions):
"""
API: NotifyEmby.notify()
"""
# 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 REQUEST_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_notify_ifttt_plugin(mock_post, mock_get):
"""
API: NotifyIFTTT() Extra Checks
"""
# 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
@mock.patch('requests.get')
@mock.patch('requests.post')
def test_notify_join_plugin(mock_post, mock_get):
"""
API: NotifyJoin() Extra Checks
"""
# 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
def test_notify_kumulos_plugin():
"""
API: NotifyKumulos() Extra Checks
"""
# Disable Throttling to speed testing
plugins.NotifyBase.request_rate_per_sec = 0
# Invalid API Key
with pytest.raises(TypeError):
plugins.NotifyKumulos(None, None)
with pytest.raises(TypeError):
plugins.NotifyKumulos(" ", None)
# Invalid Server Key
with pytest.raises(TypeError):
plugins.NotifyKumulos("abcd", None)
with pytest.raises(TypeError):
plugins.NotifyKumulos("abcd", " ")
def test_notify_mattermost_plugin():
"""
API: NotifyMatterMost() Extra Checks
"""
# Disable Throttling to speed testing
plugins.NotifyBase.request_rate_per_sec = 0
# Invalid Authorization Token
with pytest.raises(TypeError):
plugins.NotifyMatterMost(None)
with pytest.raises(TypeError):
plugins.NotifyMatterMost(" ")
@mock.patch('requests.post')
def test_notify_messagebird_plugin(mock_post):
"""
API: NotifyMessageBird() Extra Checks
"""
# Disable Throttling to speed testing
plugins.NotifyBase.request_rate_per_sec = 0
# Prepare our response
response = requests.Request()
response.status_code = requests.codes.ok
# Prepare Mock
mock_post.return_value = response
# Initialize some generic (but valid) tokens
# authkey = '{}'.format('a' * 24)
source = '+1 (555) 123-3456'
# No apikey specified
with pytest.raises(TypeError):
plugins.NotifyMessageBird(apikey=None, source=source)
with pytest.raises(TypeError):
plugins.NotifyMessageBird(apikey=" ", source=source)
def test_notify_pover_plugin():
"""
API: NotifyPushover() Extra Checks
"""
# Disable Throttling to speed testing
plugins.NotifyBase.request_rate_per_sec = 0
# No token
with pytest.raises(TypeError):
plugins.NotifyPushover(token=None)
def test_notify_ryver_plugin():
"""
API: NotifyRyver() Extra Checks
"""
# 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")
def test_notify_simplepush_plugin():
"""
API: NotifySimplePush() Extra Checks
"""
# Disable Throttling to speed testing
plugins.NotifyBase.request_rate_per_sec = 0
# No token
with pytest.raises(TypeError):
plugins.NotifySimplePush(apikey=None)
with pytest.raises(TypeError):
plugins.NotifySimplePush(apikey=" ")
# Bad event
with pytest.raises(TypeError):
plugins.NotifySimplePush(apikey="abc", event=object)
with pytest.raises(TypeError):
plugins.NotifySimplePush(apikey="abc", event=" ")
def test_notify_zulip_plugin():
"""
API: NotifyZulip() Extra Checks
"""
# Disable Throttling to speed testing
plugins.NotifyBase.request_rate_per_sec = 0
# must be 32 characters long
token = 'a' * 32
# Invalid organization
with pytest.raises(TypeError):
plugins.NotifyZulip(
botname='test', organization='#', token=token)
@mock.patch('requests.get')
@mock.patch('requests.post')
def test_notify_sendgrid_plugin(mock_post, mock_get):
"""
API: NotifySendGrid() Extra Checks
"""
# 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)
@mock.patch('requests.get')
@mock.patch('requests.post')
def test_notify_slack_plugin(mock_post, mock_get):
"""
API: NotifySlack() Extra Checks
"""
# Disable Throttling to speed testing
plugins.NotifyBase.request_rate_per_sec = 0
# Initialize some generic (but valid) tokens
token_a = 'A' * 9
token_b = 'B' * 9
token_c = 'c' * 24
# Support strings
channels = 'chan1,#chan2,+id,@user,,,'
obj = plugins.NotifySlack(
token_a=token_a, token_b=token_b, token_c=token_c, targets=channels)
assert len(obj.channels) == 4
# 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
# Missing first Token
with pytest.raises(TypeError):
plugins.NotifySlack(
token_a=None, token_b=token_b, token_c=token_c,
targets=channels)
# Test include_image
obj = plugins.NotifySlack(
token_a=token_a, token_b=token_b, token_c=token_c, targets=channels,
include_image=True)
# This call includes an image with it's payload:
assert obj.notify(
body='body', title='title', notify_type=NotifyType.INFO) is True
@mock.patch('requests.get')
@mock.patch('requests.post')
def test_notify_pushbullet_plugin(mock_post, mock_get):
"""
API: NotifyPushBullet() Extra Checks
"""
# 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
@mock.patch('requests.get')
@mock.patch('requests.post')
def test_notify_pushed_plugin(mock_post, mock_get):
"""
API: NotifyPushed() Extra Checks
"""
# 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
def test_notify_pushjet_plugin():
"""
API: NotifyPushjet() Extra Checks
"""
# Disable Throttling to speed testing
plugins.NotifyBase.request_rate_per_sec = 0
# No application Key specified
with pytest.raises(TypeError):
plugins.NotifyPushjet(secret_key=None)
with pytest.raises(TypeError):
plugins.NotifyPushjet(secret_key=" ")
@mock.patch('requests.get')
@mock.patch('requests.post')
def test_notify_pushover_plugin(mock_post, mock_get):
"""
API: NotifyPushover() Extra Checks
"""
# Disable Throttling to speed testing
plugins.NotifyBase.request_rate_per_sec = 0
# 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=" ")
@mock.patch('requests.get')
@mock.patch('requests.post')
def test_notify_rocketchat_plugin(mock_post, mock_get):
"""
API: NotifyRocketChat() Extra Checks
"""
# 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
# 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
#
# 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
@mock.patch('requests.get')
@mock.patch('requests.post')
def test_notify_telegram_plugin(mock_post, mock_get):
"""
API: NotifyTelegram() Extra Checks
"""
# Disable Throttling to speed testing
plugins.NotifyBase.request_rate_per_sec = 0
# Bot Token
bot_token = '123456789:abcdefg_hijklmnop'
invalid_bot_token = 'abcd:123'
# Chat ID
chat_ids = 'l2g, lead2gold'
# Prepare Mock
mock_get.return_value = requests.Request()
mock_post.return_value = requests.Request()
mock_post.return_value.status_code = requests.codes.ok
mock_get.return_value.status_code = requests.codes.ok
mock_get.return_value.content = '{}'
mock_post.return_value.content = '{}'
# Exception should be thrown about the fact no bot token was specified
with pytest.raises(TypeError):
plugins.NotifyTelegram(bot_token=None, targets=chat_ids)
# Exception should be thrown about the fact an invalid bot token was
# specifed
with pytest.raises(TypeError):
plugins.NotifyTelegram(bot_token=invalid_bot_token, targets=chat_ids)
obj = plugins.NotifyTelegram(
bot_token=bot_token, targets=chat_ids, include_image=True)
assert isinstance(obj, plugins.NotifyTelegram) is True
assert len(obj.targets) == 2
# Test Image Sending Exceptions
mock_get.side_effect = IOError()
mock_post.side_effect = IOError()
obj.send_image(obj.targets[0], NotifyType.INFO)
# Restore their entries
mock_get.side_effect = None
mock_post.side_effect = None
mock_get.return_value.content = '{}'
mock_post.return_value.content = '{}'
# test url call
assert isinstance(obj.url(), six.string_types) is True
# test privacy version of url
assert isinstance(obj.url(privacy=True), six.string_types) is True
assert obj.url(privacy=True).startswith('tgram://1...p/') is True
# Test that we can load the string we generate back:
obj = plugins.NotifyTelegram(**plugins.NotifyTelegram.parse_url(obj.url()))
assert isinstance(obj, plugins.NotifyTelegram) is True
# Prepare Mock to fail
response = mock.Mock()
response.status_code = requests.codes.internal_server_error
# a error response
response.content = dumps({
'description': 'test',
})
mock_get.return_value = response
mock_post.return_value = response
# No image asset
nimg_obj = plugins.NotifyTelegram(bot_token=bot_token, targets=chat_ids)
nimg_obj.asset = AppriseAsset(image_path_mask=False, image_url_mask=False)
# Test that our default settings over-ride base settings since they are
# not the same as the one specified in the base; this check merely
# ensures our plugin inheritance is working properly
assert obj.body_maxlen == plugins.NotifyTelegram.body_maxlen
# We don't override the title maxlen so we should be set to the same
# as our parent class in this case
assert obj.title_maxlen == plugins.NotifyBase.title_maxlen
# This tests erroneous messages involving multiple chat ids
assert obj.notify(
body='body', title='title', notify_type=NotifyType.INFO) is False
assert obj.notify(
body='body', title='title', notify_type=NotifyType.INFO) is False
assert nimg_obj.notify(
body='body', title='title', notify_type=NotifyType.INFO) is False
# This tests erroneous messages involving a single chat id
obj = plugins.NotifyTelegram(bot_token=bot_token, targets='l2g')
nimg_obj = plugins.NotifyTelegram(bot_token=bot_token, targets='l2g')
nimg_obj.asset = AppriseAsset(image_path_mask=False, image_url_mask=False)
assert obj.notify(
body='body', title='title', notify_type=NotifyType.INFO) is False
assert nimg_obj.notify(
body='body', title='title', notify_type=NotifyType.INFO) is False
# Bot Token Detection
# Just to make it clear to people reading this code and trying to learn
# what is going on. Apprise tries to detect the bot owner if you don't
# specify a user to message. The idea is to just default to messaging
# the bot owner himself (it makes it easier for people). So we're testing
# the creating of a Telegram Notification without providing a chat ID.
# We're testing the error handling of this bot detection section of the
# code
mock_post.return_value.content = dumps({
"ok": True,
"result": [{
"update_id": 645421321,
"message": {
"message_id": 1,
"from": {
"id": 532389719,
"is_bot": False,
"first_name": "Chris",
"language_code": "en-US"
},
"chat": {
"id": 532389719,
"first_name": "Chris",
"type": "private"
},
"date": 1519694394,
"text": "/start",
"entities": [{
"offset": 0,
"length": 6,
"type": "bot_command",
}],
}},
],
})
mock_post.return_value.status_code = requests.codes.ok
obj = plugins.NotifyTelegram(bot_token=bot_token, targets=None)
assert len(obj.targets) == 1
assert obj.targets[0] == '532389719'
# Do the test again, but without the expected (parsed response)
mock_post.return_value.content = dumps({
"ok": True,
"result": [{
"message": {
"text": "/ignored.entry",
}},
],
})
# Exception should be thrown about the fact no bot token was specified
with pytest.raises(TypeError):
plugins.NotifyTelegram(bot_token=bot_token, targets=None)
# Detect the bot with a bad response
mock_post.return_value.content = dumps({})
obj.detect_bot_owner()
# Test our bot detection with a internal server error
mock_post.return_value.status_code = requests.codes.internal_server_error
# Exception should be thrown over internal server error caused
with pytest.raises(TypeError):
plugins.NotifyTelegram(bot_token=bot_token, targets=None)
# Test our bot detection with an unmappable html error
mock_post.return_value.status_code = 999
# Exception should be thrown over invali internal error no
with pytest.raises(TypeError):
plugins.NotifyTelegram(bot_token=bot_token, targets=None)
# Do it again but this time provide a failure message
mock_post.return_value.content = dumps({'description': 'Failure Message'})
# Exception should be thrown about the fact no bot token was specified
with pytest.raises(TypeError):
plugins.NotifyTelegram(bot_token=bot_token, targets=None)
# Do it again but this time provide a failure message and perform a
# notification without a bot detection by providing at least 1 chat id
obj = plugins.NotifyTelegram(bot_token=bot_token, targets=['@abcd'])
assert nimg_obj.notify(
body='body', title='title', notify_type=NotifyType.INFO) is False
# iterate over our exceptions and test them
for _exception in REQUEST_EXCEPTIONS:
mock_post.side_effect = _exception
# No chat_ids specified
with pytest.raises(TypeError):
obj = plugins.NotifyTelegram(bot_token=bot_token, targets=None)
def test_notify_overflow_truncate():
"""
API: Overflow Truncate Functionality Testing
"""
#
# A little preparation
#
# Disable Throttling to speed testing
plugins.NotifyBase.request_rate_per_sec = 0
# Number of characters per line
row = 24
# Some variables we use to control the data we work with
body_len = 1024
title_len = 1024
# Create a large body and title with random data
body = ''.join(choice(str_alpha + str_num + ' ') for _ in range(body_len))
body = '\r\n'.join([body[i: i + row] for i in range(0, len(body), row)])
# the new lines add a large amount to our body; lets force the content
# back to being 1024 characters.
body = body[0:1024]
# Create our title using random data
title = ''.join(choice(str_alpha + str_num) for _ in range(title_len))
#
# First Test: Truncated Title
#
class TestNotification(NotifyBase):
# Test title max length
title_maxlen = 10
def __init__(self, *args, **kwargs):
super(TestNotification, self).__init__(**kwargs)
def notify(self, *args, **kwargs):
# Pretend everything is okay
return True
# We should throw an exception because our specified overflow is wrong.
with pytest.raises(TypeError):
# Load our object
obj = TestNotification(overflow='invalid')
# Load our object
obj = TestNotification(overflow=OverflowMode.TRUNCATE)
assert obj is not None
# Verify that we break the title to a max length of our title_max
# and that the body remains untouched
chunks = obj._apply_overflow(body=body, title=title, overflow=None)
chunks = obj._apply_overflow(
body=body, title=title, overflow=OverflowMode.SPLIT)
assert len(chunks) == 1
assert body.rstrip() == chunks[0].get('body')
assert title[0:TestNotification.title_maxlen] == chunks[0].get('title')
#
# Next Test: Line Count Control
#
class TestNotification(NotifyBase):
# Test title max length
title_maxlen = 5
# Maximum number of lines
body_max_line_count = 5
def __init__(self, *args, **kwargs):
super(TestNotification, self).__init__(**kwargs)
def notify(self, *args, **kwargs):
# Pretend everything is okay
return True
# Load our object
obj = TestNotification(overflow=OverflowMode.TRUNCATE)
assert obj is not None
# Verify that we break the title to a max length of our title_max
# and that the body remains untouched
chunks = obj._apply_overflow(body=body, title=title)
assert len(chunks) == 1
assert len(chunks[0].get('body').split('\n')) == \
TestNotification.body_max_line_count
assert title[0:TestNotification.title_maxlen] == chunks[0].get('title')
#
# Next Test: Truncated body
#
class TestNotification(NotifyBase):
# Test title max length
title_maxlen = title_len
# Enforce a body length of just 10
body_maxlen = 10
def __init__(self, *args, **kwargs):
super(TestNotification, self).__init__(**kwargs)
def notify(self, *args, **kwargs):
# Pretend everything is okay
return True
# Load our object
obj = TestNotification(overflow=OverflowMode.TRUNCATE)
assert obj is not None
# Verify that we break the title to a max length of our title_max
# and that the body remains untouched
chunks = obj._apply_overflow(body=body, title=title)
assert len(chunks) == 1
assert body[0:TestNotification.body_maxlen] == chunks[0].get('body')
assert title == chunks[0].get('title')
#
# Next Test: Append title to body + Truncated body
#
class TestNotification(NotifyBase):
# Enforce no title
title_maxlen = 0
# Enforce a body length of just 100
body_maxlen = 100
def __init__(self, *args, **kwargs):
super(TestNotification, self).__init__(**kwargs)
def notify(self, *args, **kwargs):
# Pretend everything is okay
return True
# Load our object
obj = TestNotification(overflow=OverflowMode.TRUNCATE)
assert obj is not None
# Verify that we break the title to a max length of our title_max
# and that the body remains untouched
obj.notify_format = NotifyFormat.HTML
chunks = obj._apply_overflow(body=body, title=title)
assert len(chunks) == 1
obj.notify_format = NotifyFormat.MARKDOWN
chunks = obj._apply_overflow(body=body, title=title)
assert len(chunks) == 1
obj.notify_format = NotifyFormat.TEXT
chunks = obj._apply_overflow(body=body, title=title)
assert len(chunks) == 1
# The below line should be read carefully... We're actually testing to see
# that our title is matched against our body. Behind the scenes, the title
# was appended to the body. The body was then truncated to the maxlen.
# The thing is, since the title is so large, all of the body was lost
# and a good chunk of the title was too. The message sent will just be a
# small portion of the title
assert len(chunks[0].get('body')) == TestNotification.body_maxlen
assert title[0:TestNotification.body_maxlen] == chunks[0].get('body')
def test_notify_overflow_split():
"""
API: Overflow Split Functionality Testing
"""
#
# A little preparation
#
# Disable Throttling to speed testing
plugins.NotifyBase.request_rate_per_sec = 0
# Number of characters per line
row = 24
# Some variables we use to control the data we work with
body_len = 1024
title_len = 1024
# Create a large body and title with random data
body = ''.join(choice(str_alpha + str_num) for _ in range(body_len))
body = '\r\n'.join([body[i: i + row] for i in range(0, len(body), row)])
# the new lines add a large amount to our body; lets force the content
# back to being 1024 characters.
body = body[0:1024]
# Create our title using random data
title = ''.join(choice(str_alpha + str_num) for _ in range(title_len))
#
# First Test: Truncated Title
#
class TestNotification(NotifyBase):
# Test title max length
title_maxlen = 10
def __init__(self, *args, **kwargs):
super(TestNotification, self).__init__(**kwargs)
def notify(self, *args, **kwargs):
# Pretend everything is okay
return True
# Load our object
obj = TestNotification(overflow=OverflowMode.SPLIT)
assert obj is not None
# Verify that we break the title to a max length of our title_max
# and that the body remains untouched
chunks = obj._apply_overflow(body=body, title=title)
assert len(chunks) == 1
assert body == chunks[0].get('body')
assert title[0:TestNotification.title_maxlen] == chunks[0].get('title')
#
# Next Test: Line Count Control
#
class TestNotification(NotifyBase):
# Test title max length
title_maxlen = 5
# Maximum number of lines
body_max_line_count = 5
def __init__(self, *args, **kwargs):
super(TestNotification, self).__init__(**kwargs)
def notify(self, *args, **kwargs):
# Pretend everything is okay
return True
# Load our object
obj = TestNotification(overflow=OverflowMode.SPLIT)
assert obj is not None
# Verify that we break the title to a max length of our title_max
# and that the body remains untouched
chunks = obj._apply_overflow(body=body, title=title)
assert len(chunks) == 1
assert len(chunks[0].get('body').split('\n')) == \
TestNotification.body_max_line_count
assert title[0:TestNotification.title_maxlen] == chunks[0].get('title')
#
# Next Test: Split body
#
class TestNotification(NotifyBase):
# Test title max length
title_maxlen = title_len
# Enforce a body length
# Wrap in int() so Python v3 doesn't convert the response into a float
body_maxlen = int(body_len / 4)
def __init__(self, *args, **kwargs):
super(TestNotification, self).__init__(**kwargs)
def notify(self, *args, **kwargs):
# Pretend everything is okay
return True
# Load our object
obj = TestNotification(overflow=OverflowMode.SPLIT)
assert obj is not None
# Verify that we break the title to a max length of our title_max
# and that the body remains untouched
chunks = obj._apply_overflow(body=body, title=title)
offset = 0
assert len(chunks) == 4
for chunk in chunks:
# Our title never changes
assert title == chunk.get('title')
# Our body is only broken up; not lost
_body = chunk.get('body')
assert body[offset: len(_body) + offset].rstrip() == _body
offset += len(_body)
#
# Next Test: Append title to body + split body
#
class TestNotification(NotifyBase):
# Enforce no title
title_maxlen = 0
# Enforce a body length based on the title
# Wrap in int() so Python v3 doesn't convert the response into a float
body_maxlen = int(title_len / 4)
def __init__(self, *args, **kwargs):
super(TestNotification, self).__init__(**kwargs)
def notify(self, *args, **kwargs):
# Pretend everything is okay
return True
# Load our object
obj = TestNotification(overflow=OverflowMode.SPLIT)
assert obj is not None
# Verify that we break the title to a max length of our title_max
# and that the body remains untouched
chunks = obj._apply_overflow(body=body, title=title)
# Our final product is that our title has been appended to our body to
# create one great big body. As a result we'll get quite a few lines back
# now.
offset = 0
# Our body will look like this in small chunks at the end of the day
bulk = title + '\r\n' + body
# Due to the new line added to the end
assert len(chunks) == (
# wrap division in int() so Python 3 doesn't convert it to a float on
# us
int(len(bulk) / TestNotification.body_maxlen) +
(1 if len(bulk) % TestNotification.body_maxlen else 0))
for chunk in chunks:
# Our title is empty every time
assert chunk.get('title') == ''
_body = chunk.get('body')
assert bulk[offset: len(_body) + offset] == _body
offset += len(_body)