# BSD 2-Clause License # # Apprise - Push Notification Library. # Copyright (c) 2025, 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. from inspect import cleandoc # Disable logging for a cleaner testing output import logging import re import threading import types import pytest from apprise import Apprise, NotificationManager from apprise.plugins import NotifyBase 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(list(iter(N_MGR))) > 0 assert bool(N_MGR) N_MGR.unload_modules() assert isinstance(N_MGR.plugins(), types.GeneratorType) assert len(list(N_MGR.plugins())) > 0 N_MGR.unload_modules(disable_native=True) assert isinstance(N_MGR.plugins(), types.GeneratorType) assert len(list(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(list(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(list(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)