# -*- coding: utf-8 -*- # BSD 2-Clause License # # Apprise - Push Notification Library. # Copyright (c) 2024, Chris Caron # # 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.manager_plugins import NotificationManager from apprise.plugins 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) 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 import NotifyBase from apprise.common import NotifyType class NotifyTest(NotifyBase): service_name = 'Test' # The services URL service_url = 'https://github.com/caronc/apprise/' # Define our protocol 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 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 # 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)