mirror of https://github.com/caronc/apprise
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
441 lines
13 KiB
441 lines
13 KiB
# -*- coding: utf-8 -*- |
|
# BSD 2-Clause License |
|
# |
|
# Apprise - Push Notification Library. |
|
# Copyright (c) 2024, Chris Caron <lead2gold@gmail.com> |
|
# |
|
# Redistribution and use in source and binary forms, with or without |
|
# modification, are permitted provided that the following conditions are met: |
|
# |
|
# 1. Redistributions of source code must retain the above copyright notice, |
|
# this list of conditions and the following disclaimer. |
|
# |
|
# 2. Redistributions in binary form must reproduce the above copyright notice, |
|
# this list of conditions and the following disclaimer in the documentation |
|
# and/or other materials provided with the distribution. |
|
# |
|
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" |
|
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE |
|
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE |
|
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE |
|
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR |
|
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF |
|
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS |
|
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN |
|
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) |
|
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE |
|
# POSSIBILITY OF SUCH DAMAGE. |
|
|
|
import re |
|
import pytest |
|
import types |
|
import threading |
|
from inspect import cleandoc |
|
|
|
from apprise import Apprise |
|
from apprise.NotificationManager import NotificationManager |
|
from apprise.plugins.NotifyBase import NotifyBase |
|
|
|
# Disable logging for a cleaner testing output |
|
import logging |
|
logging.disable(logging.CRITICAL) |
|
|
|
# Grant access to our Notification Manager Singleton |
|
N_MGR = NotificationManager() |
|
|
|
|
|
def test_notification_manager_general(): |
|
""" |
|
N_MGR: Notification Manager General testing |
|
|
|
""" |
|
# Clear our set so we can test init calls |
|
N_MGR.unload_modules() |
|
assert isinstance(N_MGR.schemas(), list) |
|
assert len(N_MGR.schemas()) > 0 |
|
N_MGR.unload_modules(disable_native=True) |
|
assert isinstance(N_MGR.schemas(), list) |
|
assert len(N_MGR.schemas()) == 0 |
|
|
|
N_MGR.unload_modules() |
|
assert len(N_MGR) > 0 |
|
|
|
N_MGR.unload_modules() |
|
iter(N_MGR) |
|
iter(N_MGR) |
|
|
|
N_MGR.unload_modules() |
|
assert bool(N_MGR) is False |
|
assert len([x for x in iter(N_MGR)]) > 0 |
|
assert bool(N_MGR) is True |
|
|
|
N_MGR.unload_modules() |
|
assert isinstance(N_MGR.plugins(), types.GeneratorType) |
|
assert len([x for x in N_MGR.plugins()]) > 0 |
|
N_MGR.unload_modules(disable_native=True) |
|
assert isinstance(N_MGR.plugins(), types.GeneratorType) |
|
assert len([x for x in N_MGR.plugins()]) == 0 |
|
N_MGR.unload_modules() |
|
assert isinstance(N_MGR['json'](host='localhost'), NotifyBase) |
|
N_MGR.unload_modules() |
|
assert 'json' in N_MGR |
|
|
|
# Define our good:// url |
|
class DisabledNotification(NotifyBase): |
|
# Always disabled |
|
enabled = False |
|
|
|
def __init__(self, *args, **kwargs): |
|
super().__init__(*args, **kwargs) |
|
|
|
def notify(self, *args, **kwargs): |
|
# Pretend everything is okay |
|
return True |
|
|
|
def url(self, **kwargs): |
|
# Support url() function |
|
return '' |
|
|
|
# Define our good:// url |
|
class GoodNotification(NotifyBase): |
|
|
|
secure_protocol = ('good', 'goods') |
|
|
|
def __init__(self, *args, **kwargs): |
|
super().__init__(*args, **kwargs) |
|
|
|
def notify(self, *args, **kwargs): |
|
# Pretend everything is okay |
|
return True |
|
|
|
def url(self, **kwargs): |
|
# Support url() function |
|
return '' |
|
|
|
N_MGR.unload_modules() |
|
assert N_MGR.add(GoodNotification) |
|
assert 'good' in N_MGR |
|
assert 'goods' in N_MGR |
|
assert 'abcd' not in N_MGR |
|
assert 'xyz' not in N_MGR |
|
|
|
N_MGR.unload_modules() |
|
assert N_MGR.add(GoodNotification, 'abcd') |
|
assert 'good' in N_MGR |
|
assert 'goods' in N_MGR |
|
assert 'abcd' in N_MGR |
|
assert 'xyz' not in N_MGR |
|
|
|
N_MGR.unload_modules() |
|
assert N_MGR.add(GoodNotification, ['abcd', 'xYz']) |
|
assert 'good' in N_MGR |
|
assert 'goods' in N_MGR |
|
assert 'abcd' in N_MGR |
|
# Lower case |
|
assert 'xyz' in N_MGR |
|
|
|
N_MGR.unload_modules() |
|
# Not going to work; schemas must be a list of string |
|
assert N_MGR.add(GoodNotification, object) is False |
|
|
|
N_MGR.unload_modules() |
|
with pytest.raises(KeyError): |
|
del N_MGR['good'] |
|
N_MGR['good'] = GoodNotification |
|
del N_MGR['good'] |
|
|
|
N_MGR.unload_modules() |
|
N_MGR['good'] = GoodNotification |
|
assert N_MGR['good'].enabled is True |
|
N_MGR.enable_only('json', 'xml') |
|
assert N_MGR['good'].enabled is False |
|
assert N_MGR['json'].enabled is True |
|
assert N_MGR['jsons'].enabled is True |
|
assert N_MGR['xml'].enabled is True |
|
assert N_MGR['xmls'].enabled is True |
|
|
|
# Only two plugins are enabled |
|
assert len([p for p in N_MGR.plugins(include_disabled=False)]) == 2 |
|
|
|
N_MGR.enable_only('good') |
|
assert N_MGR['good'].enabled is True |
|
assert N_MGR['json'].enabled is False |
|
assert N_MGR['jsons'].enabled is False |
|
assert N_MGR['xml'].enabled is False |
|
assert N_MGR['xmls'].enabled is False |
|
|
|
assert len([p for p in N_MGR.plugins(include_disabled=False)]) == 1 |
|
|
|
N_MGR.unload_modules() |
|
N_MGR['disabled'] = DisabledNotification |
|
assert N_MGR['disabled'].enabled is False |
|
N_MGR.enable_only('disabled') |
|
# Can't enable items that aren't supposed to be: |
|
assert N_MGR['disabled'].enabled is False |
|
|
|
N_MGR['good'] = GoodNotification |
|
assert N_MGR['good'].enabled is True |
|
|
|
# You can't disable someething already disabled |
|
N_MGR.disable('disabled') |
|
assert N_MGR['disabled'].enabled is False |
|
|
|
N_MGR.unload_modules() |
|
N_MGR.enable_only('form', 'xml') |
|
for schema in N_MGR.schemas(include_disabled=False): |
|
assert re.match(r'^(form|xml)s?$', schema, re.IGNORECASE) is not None |
|
|
|
N_MGR.unload_modules() |
|
assert N_MGR['form'].enabled is True |
|
assert N_MGR['xml'].enabled is True |
|
assert N_MGR['json'].enabled is True |
|
N_MGR.enable_only('form', 'xml') |
|
assert N_MGR['form'].enabled is True |
|
assert N_MGR['xml'].enabled is True |
|
assert N_MGR['json'].enabled is False |
|
|
|
N_MGR.disable('invalid', 'xml') |
|
assert N_MGR['form'].enabled is True |
|
assert N_MGR['xml'].enabled is False |
|
assert N_MGR['json'].enabled is False |
|
|
|
# Detect that our json object is enabled |
|
with pytest.raises(KeyError): |
|
# The below can not be indexed |
|
N_MGR['invalid'] |
|
|
|
N_MGR.unload_modules() |
|
N_MGR.disable('invalid', 'xml') |
|
|
|
N_MGR.unload_modules() |
|
assert N_MGR['json'].enabled is True |
|
|
|
# Work with an empty module tree |
|
N_MGR.unload_modules(disable_native=True) |
|
with pytest.raises(KeyError): |
|
# The below can not be indexed |
|
N_MGR['good'] |
|
|
|
N_MGR.unload_modules() |
|
assert 'hello' not in N_MGR |
|
assert 'good' not in N_MGR |
|
assert 'goods' not in N_MGR |
|
|
|
N_MGR['hello'] = GoodNotification |
|
assert 'hello' in N_MGR |
|
assert 'good' in N_MGR |
|
assert 'goods' in N_MGR |
|
|
|
N_MGR.unload_modules() |
|
N_MGR['good'] = GoodNotification |
|
|
|
with pytest.raises(KeyError): |
|
# Can not assign the value again without getting a Conflict |
|
N_MGR['good'] = GoodNotification |
|
|
|
N_MGR.unload_modules() |
|
N_MGR.remove('good', 'invalid') |
|
assert 'good' not in N_MGR |
|
assert 'goods' not in N_MGR |
|
|
|
|
|
def test_notification_manager_module_loading(tmpdir): |
|
""" |
|
N_MGR: Notification Manager Module Loading |
|
|
|
""" |
|
|
|
# Handle loading modules twice (they gracefully handle not loading more in |
|
# memory then needed) |
|
N_MGR.load_modules() |
|
N_MGR.load_modules() |
|
|
|
# |
|
# Thread Testing |
|
# |
|
|
|
# This tests against a racing condition when the modules have not been |
|
# loaded. When multiple instances of Apprise are all instantiated, |
|
# the loading of the modules will occur for each instance if detected |
|
# having not been previously done, this tests that we can dynamically |
|
# support the loading of modules once whe multiple instances to apprise |
|
# are instantiated. |
|
thread_count = 10 |
|
|
|
def thread_test(result, no): |
|
""" |
|
Load our apprise object with valid URLs and store our result |
|
""" |
|
apobj = Apprise() |
|
result[no] = apobj.add('json://localhost') and \ |
|
apobj.add('form://localhost') and \ |
|
apobj.add('xml://localhost') |
|
|
|
# Unload our modules |
|
N_MGR.unload_modules() |
|
|
|
# Prepare threads to load |
|
results = [None] * thread_count |
|
threads = [ |
|
threading.Thread(target=thread_test, args=(results, no)) |
|
for no in range(thread_count) |
|
] |
|
|
|
# Verify we can safely load our modules in a thread safe environment |
|
for t in threads: |
|
t.start() |
|
|
|
for t in threads: |
|
t.join() |
|
|
|
# Verify we loaded our urls in all threads successfully |
|
for result in results: |
|
assert result is True |
|
|
|
|
|
def test_notification_manager_decorators(tmpdir): |
|
""" |
|
N_MGR: Notification Manager Decorator testing |
|
|
|
""" |
|
|
|
# Prepare ourselves a file to work with |
|
notify_hook = tmpdir.mkdir('goodmodule').join('__init__.py') |
|
notify_hook.write(cleandoc(""" |
|
from apprise.decorators import notify |
|
|
|
# We want to trigger on anyone who configures a call to clihook:// |
|
@notify(on="clihooka") |
|
def mywrapper(body, title, notify_type, *args, **kwargs): |
|
# A simple test - print to screen |
|
print("A {}: {} - {}".format(notify_type, title, body)) |
|
|
|
# No return (so a return of None) get's translated to True |
|
|
|
# Define another in the same file; uppercase goes to lower |
|
@notify(on="CLIhookb") |
|
def mywrapper(body, title, notify_type, *args, **kwargs): |
|
# A simple test - print to screen |
|
print("B {}: {} - {}".format(notify_type, title, body)) |
|
|
|
# No return (so a return of None) get's translated to True |
|
""")) |
|
|
|
N_MGR.module_detection(str(notify_hook)) |
|
|
|
assert 'clihooka' in N_MGR |
|
assert 'clihookb' in N_MGR |
|
N_MGR.unload_modules() |
|
assert 'clihooka' not in N_MGR |
|
assert 'clihookb' not in N_MGR |
|
|
|
N_MGR.module_detection(str(notify_hook)) |
|
assert 'clihooka' in N_MGR |
|
assert 'clihookb' in N_MGR |
|
del N_MGR['clihookb'] |
|
assert 'clihooka' in N_MGR |
|
assert 'clihookb' not in N_MGR |
|
del N_MGR['clihooka'] |
|
assert 'clihooka' not in N_MGR |
|
assert 'clihookb' not in N_MGR |
|
|
|
# Prepare ourselves a file to work with |
|
notify_base = tmpdir.mkdir('plugins') |
|
notify_test = notify_base.join('NotifyTest.py') |
|
notify_test.write(cleandoc(""" |
|
# |
|
# Bare Minimum Valid Object |
|
# |
|
from apprise.plugins.NotifyBase import NotifyBase |
|
from apprise.common import NotifyType |
|
|
|
class NotifyTest(NotifyBase): |
|
|
|
service_name = 'Test' |
|
|
|
# The services URL |
|
service_url = 'https://github.com/caronc/apprise/' |
|
|
|
# All boxcar notifications are secure |
|
secure_protocol = 'mytest' |
|
|
|
# A URL that takes you to the setup/help of the specific protocol |
|
setup_url = 'https://github.com/caronc/apprise/wiki/Notify_mytest' |
|
|
|
# Define object templates |
|
templates = ( |
|
'{schema}://', |
|
) |
|
|
|
def __init__(self, **kwargs): |
|
super().__init__(**kwargs) |
|
|
|
def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs): |
|
return True |
|
|
|
def url(self): |
|
return 'mytest://' |
|
""")) |
|
assert 'mytest' not in N_MGR |
|
N_MGR.load_modules(path=str(notify_base)) |
|
assert 'mytest' in N_MGR |
|
del N_MGR['mytest'] |
|
assert 'mytest' not in N_MGR |
|
|
|
# Prepare ourselves a file to work with |
|
notify_test = notify_base.join('NotifyNoAlign.py') |
|
notify_test.write(cleandoc(""" |
|
# |
|
# Bare Minimum Valid Object |
|
# |
|
from apprise.plugins.NotifyBase import NotifyBase |
|
from apprise.common import NotifyType |
|
|
|
class NotifyDifferentName(NotifyBase): |
|
|
|
service_name = 'Unloadable' |
|
|
|
# The services URL |
|
service_url = 'https://github.com/caronc/apprise/' |
|
|
|
# All boxcar notifications are secure |
|
secure_protocol = 'noload' |
|
|
|
# A URL that takes you to the setup/help of the specific protocol |
|
setup_url = 'https://github.com/caronc/apprise/wiki/Notify_mytest' |
|
|
|
# Define object templates |
|
templates = ( |
|
'{schema}://', |
|
) |
|
|
|
def __init__(self, **kwargs): |
|
super().__init__(**kwargs) |
|
|
|
def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs): |
|
return True |
|
|
|
def url(self): |
|
return 'mytest://' |
|
""")) |
|
assert 'mytest' not in N_MGR |
|
N_MGR.load_modules(path=str(notify_base)) |
|
|
|
# It's still not loaded because the path has already been scanned |
|
assert 'mytest' not in N_MGR |
|
N_MGR.load_modules(path=str(notify_base), force=True) |
|
assert 'mytest' in N_MGR |
|
|
|
# Could not be loaded because the filename did not align with the class |
|
# name. |
|
assert 'noload' not in N_MGR |
|
|
|
# Double load will test section of code that prevents a notification |
|
# From reloading if previously already loaded |
|
N_MGR.load_modules(path=str(notify_base)) |
|
# Our item is still loaded as expected |
|
assert 'mytest' in N_MGR |
|
|
|
# Simple test to make sure we can handle duplicate entries loaded |
|
N_MGR.load_modules(path=str(notify_base), force=True) |
|
N_MGR.load_modules(path=str(notify_base), force=True)
|
|
|