# 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 json import dumps, loads # Disable logging for a cleaner testing output import logging import os from unittest import mock from helpers import AppriseURLTester import requests from apprise import Apprise, NotifyType from apprise.plugins.notificationapi import NotifyNotificationAPI logging.disable(logging.CRITICAL) NOTIFICATIONAPI_GOOD_RESPONSE = dumps({}) NOTIFICATIONAPI_BAD_RESPONSE = "{" # Attachment Directory TEST_VAR_DIR = os.path.join(os.path.dirname(__file__), "var") # Our Testing URLs apprise_url_tests = ( ("napi://", { "instance": TypeError, }), ("napi://:@/", { "instance": TypeError, }), ("napi://abcd", { # invalid from email "instance": TypeError, }), ("napi://abcd@host.com", { # Just an Email specified, no client_id or client_secret "instance": TypeError, }), ("napi://user@client_id/cs14a/user@example.ca", { # No id matched "instance": TypeError, }), ("napi://user@client_id/cs14b/+15551235553", { # No id matched "instance": TypeError, }), ("napi://user@client_id/cs14c/+15551235553/user@example.ca", { # No id matched "instance": TypeError, }), ("napi://type@client_id/client_secret/id/+15551235553/?mode=invalid", { # Invalid mode "instance": TypeError, }), ("napi://type@client_id/client_secret/id/+15551235553/?region=invalid", { # Invalid region "instance": TypeError, }), (( "napi://type@client_id/client_secret/id/user@example.ca/" "user2@example.ca" ), { # to many emails assigned to id (variation 1) "instance": TypeError, }), (( "napi://type@client_id/client_secret/user@example.ca/" "user2@example.ca" ), { # to many emails assigned to id (variation 2) "instance": TypeError, }), (( "napi://type@client_id/client_secret/id/+15551235553/" "+15551235555" ), { # to many phone no's assigned to id (variation 1) "instance": TypeError, }), (( "napi://type@client_id/client_secret/+15551235553/" "+15551235555" ), { # to many phone no's assigned to id (variation 2) "instance": TypeError, }), ("napi://type@client_id/client_secret/id/+15551235553/?mode=invalid", { # Invalid mode "instance": TypeError, }), ("napi://client_id/client_secret/id/+15551231234/?type=*(", { # Invalid type "instance": TypeError, }), ("napi://client_id/client_secret/id/+15551231234/?channels=bad", { # Invalid channel "instance": TypeError, }), ("napi://?secret=cs&to=id,user404@example.com&type=typed", { # No id found "instance": TypeError, }), ("napi://client_id/client_secret/id/g@rb@ge/+15551235553/", { # g@rb@ge enry ignored "instance": NotifyNotificationAPI, "requests_response_text": NOTIFICATIONAPI_GOOD_RESPONSE, }), ("napi://cid/secret/id/user1@example.com/?type=apprise-msg", { "instance": NotifyNotificationAPI, "requests_response_text": NOTIFICATIONAPI_GOOD_RESPONSE, }), ("notificationapi://cid/secret/id/user1@example.com", { # Support full schema:// of notificationapi:// "instance": NotifyNotificationAPI, "requests_response_text": NOTIFICATIONAPI_GOOD_RESPONSE, }), ("napi://cid/secret/id/id2/user1@example.com", { # two id's in a row "instance": NotifyNotificationAPI, "requests_response_text": NOTIFICATIONAPI_GOOD_RESPONSE, }), (("napi://type@cid/secret/id10/user2@example.com/" "id5/+15551235555/id8/+15551235534" "?reply=Chris"), { "instance": NotifyNotificationAPI, "requests_response_text": NOTIFICATIONAPI_GOOD_RESPONSE, }), (("napi://type@cid/secret/abc1/user1@example.com/" "id5/+15551235555/?from=Chris&reply=Christopher"), { "instance": NotifyNotificationAPI, "requests_response_text": NOTIFICATIONAPI_GOOD_RESPONSE, }), (("napi://type@cid/secret/id/user3@example.com/" "?from=joe@example.ca&reply=user@abc.com"), { # Set from/source "instance": NotifyNotificationAPI, "requests_response_text": NOTIFICATIONAPI_GOOD_RESPONSE, }), (("napi://type@cid/secret/id/user4@example.com/" "?from=joe@example.ca&bcc=user1@yahoo.ca&cc=user2@yahoo.ca"), { # Set from/source "instance": NotifyNotificationAPI, "requests_response_text": NOTIFICATIONAPI_GOOD_RESPONSE, # Our expected url(privacy=True) startswith() response: "privacy_url": "napi://type@c...d/s...t/", }), ("napi://?id=ci&secret=cs&to=id,user5@example.com&type=typec", { # use just kwargs "instance": NotifyNotificationAPI, "requests_response_text": NOTIFICATIONAPI_GOOD_RESPONSE, # Our expected url(privacy=True) startswith() response: "privacy_url": "napi://typec@c...i/c...s/", }), ("napi://id?secret=cs&to=id,user5@example.com&type=typeb", { # id is pull from the host "instance": NotifyNotificationAPI, "requests_response_text": NOTIFICATIONAPI_GOOD_RESPONSE, # Our expected url(privacy=True) startswith() response: "privacy_url": "napi://typeb@i...d/c...s/", }), ("napi://secret?id=ci&to=id,user5@example.com&type=typea", { # id pulled from kwargs still allows secret to be the # next parsed entry from cli "instance": NotifyNotificationAPI, "requests_response_text": NOTIFICATIONAPI_GOOD_RESPONSE, # Our expected url(privacy=True) startswith() response: "privacy_url": "napi://typea@c...i/s...t/", }), ("napi://?id=ci&secret=cs&type=test-type®ion=eu", { # No targets specified "instance": NotifyNotificationAPI, "requests_response_text": NOTIFICATIONAPI_GOOD_RESPONSE, "notify_response": False, }), ("napi://?id=ci&secret=cs&to=id,user5@example.com&type=typec", { # bad response "instance": NotifyNotificationAPI, "requests_response_text": NOTIFICATIONAPI_BAD_RESPONSE, "notify_response": False, }), ("napi://user@client_id/cs2/id/user6@example.ca" "?bcc=invalid", { # A good email with a bad Blind Carbon Copy "instance": NotifyNotificationAPI, "requests_response_text": NOTIFICATIONAPI_GOOD_RESPONSE, }), ("napi://user@client_id/cs3/id/user8@example.ca" "?cc=l2g@nuxref.com", { # A good email with Carbon Copy "instance": NotifyNotificationAPI, "requests_response_text": NOTIFICATIONAPI_GOOD_RESPONSE, }), ("napi://client_id/cs3/id/user8@example.ca" "?channels=email,sms,slack,mobile_push,web_push,inapp", { # A good email with Carbon Copy "instance": NotifyNotificationAPI, "requests_response_text": NOTIFICATIONAPI_GOOD_RESPONSE, }), ("napi://user@client_id/cs4/id/user9@example.ca" "?cc=Chris", { # A good email with Carbon Copy "instance": NotifyNotificationAPI, "requests_response_text": NOTIFICATIONAPI_GOOD_RESPONSE, }), ("napi://user@client_id/cs5/id/user10@example.ca" "?cc=invalid", { # A good email with Carbon Copy "instance": NotifyNotificationAPI, "requests_response_text": NOTIFICATIONAPI_GOOD_RESPONSE, }), ("napi://user@client_id/cs6/id/user11@example.ca" "?to=invalid", { # an invalid to email "instance": NotifyNotificationAPI, "requests_response_text": NOTIFICATIONAPI_GOOD_RESPONSE, }), ("napi://user@client_id/cs7/id/chris1@example.com", { # An email with a designated to email "instance": NotifyNotificationAPI, "requests_response_text": NOTIFICATIONAPI_GOOD_RESPONSE, }), ("napi://user@client_id/cs8/id1/user12@example.ca" "?to=id,Chris", { # An email with a full name in in To field "instance": NotifyNotificationAPI, "requests_response_text": NOTIFICATIONAPI_GOOD_RESPONSE, }), ("napi://user@client_id/cs9/id2/user13@example.ca/" "id/kris@example.com/id/chris2@example.com/id/+15552341234" "?:token=value", { # Several emails to notify "instance": NotifyNotificationAPI, "requests_response_text": NOTIFICATIONAPI_GOOD_RESPONSE, }), ("napi://user@client_id/cs10/id/user14@example.ca" "?cc=Chris", { # An email with a full name in cc "instance": NotifyNotificationAPI, "requests_response_text": NOTIFICATIONAPI_GOOD_RESPONSE, }), ("napi://user@client_id/cs11/id/user15@example.ca" "?cc=chris12@example.com", { # An email with a full name in cc "instance": NotifyNotificationAPI, "requests_response_text": NOTIFICATIONAPI_GOOD_RESPONSE, }), ("napi://user@client_id/cs12/id/user16@example.ca" "?bcc=Chris", { # An email with a full name in bcc "instance": NotifyNotificationAPI, "requests_response_text": NOTIFICATIONAPI_GOOD_RESPONSE, }), ("napi://user@client_id/cs13/id/user@example.ca" "?bcc=chris13@example.com", { # An email with a full name in bcc "instance": NotifyNotificationAPI, "requests_response_text": NOTIFICATIONAPI_GOOD_RESPONSE, }), ("napi://user@client_id/cs14/id/user@example.ca" "?to=Chris,id14", { # An email with a full name in bcc "instance": NotifyNotificationAPI, "requests_response_text": NOTIFICATIONAPI_GOOD_RESPONSE, }), ("napi://user@client_id/cs15/id" "?to=user@example.com", { # An email with a full name in bcc "instance": NotifyNotificationAPI, "requests_response_text": NOTIFICATIONAPI_GOOD_RESPONSE, }), ("napi://user@client_id/cs16/id/user@example.ca" "?template=1234&+sub=value&+sub2=value2", { # A good email with a template + substitutions "instance": NotifyNotificationAPI, "requests_response_text": NOTIFICATIONAPI_GOOD_RESPONSE, # Our expected url(privacy=True) startswith() response: "privacy_url": "napi://user@c...d/c...6/", }), ("napi://user@client_id/cs17/id/user@example.ca", { "instance": NotifyNotificationAPI, # force a failure "response": False, "requests_response_code": requests.codes.internal_server_error, "requests_response_text": NOTIFICATIONAPI_GOOD_RESPONSE, }), ("napi://user@client_id/cs18/id/user@example.ca", { "instance": NotifyNotificationAPI, # throw a bizzare code forcing us to fail to look it up "response": False, "requests_response_code": 999, "requests_response_text": NOTIFICATIONAPI_GOOD_RESPONSE, }), ("napi://user@client_id/cs19/id/user@example.ca", { "instance": NotifyNotificationAPI, # Throws a series of connection and transfer exceptions when this flag # is set and tests that we gracfully handle them "test_requests_exceptions": True, "requests_response_text": NOTIFICATIONAPI_GOOD_RESPONSE, }), ) def test_plugin_napi_urls(): """ NotifyNotificationAPI() Apprise URLs """ # Run our general tests AppriseURLTester(tests=apprise_url_tests).run_all() @mock.patch("requests.post") def test_plugin_napi_template_sms_payloads(mock_post): """NotifyNotificationAPI() Testing Template SMS Payloads.""" okay_response = requests.Request() okay_response.status_code = requests.codes.ok okay_response.content = NOTIFICATIONAPI_GOOD_RESPONSE # Assign our mock object our return value mock_post.return_value = okay_response # Details client_id = "my_id" client_secret = "my_secret" message_type = "apprise-post" targets = "userid/+1-555-123-4567" obj = Apprise.instantiate( f"napi://{message_type}@{client_id}/{client_secret}/" f"{targets}?mode=template") assert isinstance(obj, NotifyNotificationAPI) assert isinstance(obj.url(), str) # No calls made yet assert mock_post.call_count == 0 # Send our notification assert ( obj.notify(body="body", title="title", notify_type=NotifyType.INFO) is True ) # delivery of message assert mock_post.call_count == 1 assert ( mock_post.call_args_list[0][0][0] == f"https://api.notificationapi.com/{client_id}/sender" ) payload = loads(mock_post.call_args_list[0][1]["data"]) assert payload == { "type": "apprise-post", "to": { "id": "userid", "number": "+15551234567", }, "parameters": { "appBody": "body", "appTitle": "", "appType": "info", "appId": "Apprise", "appDescription": "Apprise Notifications", "appColor": "#3AA3E3", "appImageUrl": ( "https://github.com/caronc/apprise/raw/master/apprise" "/assets/themes/default/apprise-info-72x72.png"), "appUrl": "https://github.com/caronc/apprise"}, } headers = mock_post.call_args_list[0][1]["headers"] assert headers == { "User-Agent": "Apprise", "Content-Type": "application/json", "Authorization": "Basic bXlfaWQ6bXlfc2VjcmV0"} # Reset our mock object mock_post.reset_mock() @mock.patch("requests.post") def test_plugin_napi_template_email_payloads(mock_post): """NotifyNotificationAPI() Testing Template Email Payloads.""" okay_response = requests.Request() okay_response.status_code = requests.codes.ok okay_response.content = NOTIFICATIONAPI_GOOD_RESPONSE # Assign our mock object our return value mock_post.return_value = okay_response # Details client_id = "my_id_abc" client_secret = "my_secret" message_type = "apprise-post" targets = "userid/test@example.ca" obj = Apprise.instantiate( f"napi://{message_type}@{client_id}/{client_secret}/" f"{targets}?from=Chris&bcc=joe@hidden.com&" f"cc=jason@hidden.com&:customToken=customValue&mode=template") assert isinstance(obj, NotifyNotificationAPI) assert isinstance(obj.url(), str) # No calls made yet assert mock_post.call_count == 0 # Send our notification assert ( obj.notify(body="body", title="title", notify_type=NotifyType.INFO) is True ) # delivery of message assert mock_post.call_count == 1 assert ( mock_post.call_args_list[0][0][0] == f"https://api.notificationapi.com/{client_id}/sender" ) payload = loads(mock_post.call_args_list[0][1]["data"]) assert payload == { "type": "apprise-post", "to": { "id": "userid", "email": "test@example.ca", }, "options": { "email": { "fromAddress": "chris@example.eu", "fromName": "Chris", "ccAddresses": ["jason@hidden.com"], "bccAddresses": ["joe@hidden.com"]} }, "parameters": { "customToken": "customValue", "appBody": "body", "appTitle": "", "appType": "info", "appId": "Apprise", "appDescription": "Apprise Notifications", "appColor": "#3AA3E3", "appImageUrl": ( "https://github.com/caronc/apprise/raw/master/apprise/" "assets/themes/default/apprise-info-72x72.png"), "appUrl": "https://github.com/caronc/apprise" }, } headers = mock_post.call_args_list[0][1]["headers"] assert headers == { "User-Agent": "Apprise", "Content-Type": "application/json", "Authorization": "Basic bXlfaWRfYWJjOm15X3NlY3JldA=="} # Reset our mock object mock_post.reset_mock() @mock.patch("requests.post") def test_plugin_napi_message_payloads(mock_post): """NotifyNotificationAPI() Testing Message Payloads.""" okay_response = requests.Request() okay_response.status_code = requests.codes.ok okay_response.content = NOTIFICATIONAPI_GOOD_RESPONSE # Assign our mock object our return value mock_post.return_value = okay_response # Details client_id = "my_id_abc" client_secret = "my_secret" message_type = "apprise-post" targets = "userid/test@example.ca/+15551239876" obj = Apprise.instantiate( f"napi://{message_type}@{client_id}/{client_secret}/" f"{targets}?from=Chris&bcc=joe@hidden.com" f"&mode=message") assert isinstance(obj, NotifyNotificationAPI) assert isinstance(obj.url(), str) # No calls made yet assert mock_post.call_count == 0 # Send our notification assert ( obj.notify(body="body", title="title", notify_type=NotifyType.INFO) is True ) # delivery of message assert mock_post.call_count == 1 assert ( mock_post.call_args_list[0][0][0] == f"https://api.notificationapi.com/{client_id}/sender" ) payload = loads(mock_post.call_args_list[0][1]["data"]) assert payload == { "type": "apprise-post", "to": { "id": "userid", "email": "test@example.ca", "number": "+15551239876", }, "email": { "subject": "Apprise", "html": "body", "senderName": "chris@example.eu", "senderEmail": "Chris", }, "options": { "email": { "fromAddress": "chris@example.eu", "fromName": "Chris", "bccAddresses": ["joe@hidden.com"], }, }, } headers = mock_post.call_args_list[0][1]["headers"] assert headers == { "User-Agent": "Apprise", "Content-Type": "application/json", "Authorization": "Basic bXlfaWRfYWJjOm15X3NlY3JldA=="} # Reset our mock object mock_post.reset_mock() # Reversing the sms with email causes auto-detection channel to # be sms instead of email targets = "userid/+15551239876/test@example.ca" obj = Apprise.instantiate( f"napi://{client_id}/{client_secret}/" f"{targets}?from=Chris&bcc=joe@hidden.com") assert isinstance(obj, NotifyNotificationAPI) assert isinstance(obj.url(), str) # No calls made yet assert mock_post.call_count == 0 # Send our notification assert ( obj.notify(body="body", title="title", notify_type=NotifyType.INFO) is True ) # delivery of message assert mock_post.call_count == 1 assert ( mock_post.call_args_list[0][0][0] == f"https://api.notificationapi.com/{client_id}/sender" ) payload = loads(mock_post.call_args_list[0][1]["data"]) assert payload == { "type": "apprise", "to": { "id": "userid", "number": "+15551239876", "email": "test@example.ca", }, "sms": {"message": "body"}, "options": { "email": { "fromAddress": "chris@example.eu", "fromName": "Chris", "bccAddresses": ["joe@hidden.com"]}, }, } headers = mock_post.call_args_list[0][1]["headers"] assert headers == { "User-Agent": "Apprise", "Content-Type": "application/json", "Authorization": "Basic bXlfaWRfYWJjOm15X3NlY3JldA=="} # Reset our mock object mock_post.reset_mock() # Experiment with fixed channels: obj = Apprise.instantiate( f"napi://{message_type}@{client_id}/{client_secret}/" f"{targets}?from=Chris&bcc=joe@hidden.com" f"&mode=message&channels=sms,slack") assert isinstance(obj, NotifyNotificationAPI) assert isinstance(obj.url(), str) # No calls made yet assert mock_post.call_count == 0 # Send our notification assert ( obj.notify(body="body", title="title", notify_type=NotifyType.INFO) is True ) # delivery of message assert mock_post.call_count == 1 assert ( mock_post.call_args_list[0][0][0] == f"https://api.notificationapi.com/{client_id}/sender" ) payload = loads(mock_post.call_args_list[0][1]["data"]) assert payload == { "type": "apprise-post", "to": { "id": "userid", "email": "test@example.ca", "number": "+15551239876", }, "slack": {"text": "body"}, "sms": {"message": "body"}, "options": { "email": { "fromAddress": "chris@example.eu", "fromName": "Chris", "bccAddresses": ["joe@hidden.com"], }, }, } headers = mock_post.call_args_list[0][1]["headers"] assert headers == { "User-Agent": "Apprise", "Content-Type": "application/json", "Authorization": "Basic bXlfaWRfYWJjOm15X3NlY3JldA=="} def test_plugin_napi_edge_cases(): """ NotifyNotificationAPI() Edge Cases """ client_id = "my_id_abc" client_secret = "my_secret" targets = ["userid", "test@example.ca", "+15551239876"] # Tests case where tokens is == None obj = NotifyNotificationAPI(client_id, client_secret, targets=targets) assert isinstance(obj, NotifyNotificationAPI) assert isinstance(obj.url(), str)