From 3064eb06db7b1507551442b42b7a7d233c6f9cac Mon Sep 17 00:00:00 2001 From: Chris Caron Date: Sat, 6 Sep 2025 13:23:56 -0400 Subject: [PATCH] test coverage --- README.md | 1 + apprise/config/base.py | 2 +- apprise/plugins/notificationapi.py | 77 +++---- apprise/utils/parse.py | 8 +- packaging/redhat/python-apprise.spec | 6 +- pyproject.toml | 1 + tests/test_plugin_notificationapi.py | 332 +++++++++++++++++++++++++-- 7 files changed, 360 insertions(+), 67 deletions(-) diff --git a/README.md b/README.md index 4967ba49..e2bd3d4f 100644 --- a/README.md +++ b/README.md @@ -92,6 +92,7 @@ The table below identifies the services this tool supports and some example serv | [Nextcloud](https://github.com/caronc/apprise/wiki/Notify_nextcloud) | ncloud:// or nclouds:// | (TCP) 80 or 443 | ncloud://adminuser:pass@host/User
nclouds://adminuser:pass@host/User1/User2/UserN | [NextcloudTalk](https://github.com/caronc/apprise/wiki/Notify_nextcloudtalk) | nctalk:// or nctalks:// | (TCP) 80 or 443 | nctalk://user:pass@host/RoomId
nctalks://user:pass@host/RoomId1/RoomId2/RoomIdN | [Notica](https://github.com/caronc/apprise/wiki/Notify_notica) | notica:// | (TCP) 443 | notica://Token/ +| [NotificationAPI](https://github.com/caronc/apprise/wiki/Notify_notificationapi) | napi:// | (TCP) 443 | napi://ClientID/ClientSecret/Target
napi://ClientID/ClientSecret/Target1/Target2/TargetN
napi://MessageType@ClientID/ClientSecret/Target | [Notifiarr](https://github.com/caronc/apprise/wiki/Notify_notifiarr) | notifiarr:// | (TCP) 443 | notifiarr://apikey/#channel
notifiarr://apikey/#channel1/#channel2/#channeln | [Notifico](https://github.com/caronc/apprise/wiki/Notify_notifico) | notifico:// | (TCP) 443 | notifico://ProjectID/MessageHook/ | [ntfy](https://github.com/caronc/apprise/wiki/Notify_ntfy) | ntfy:// | (TCP) 80 or 443 | ntfy://topic/
ntfys://topic/ diff --git a/apprise/config/base.py b/apprise/config/base.py index 337eb3c7..effe1a70 100644 --- a/apprise/config/base.py +++ b/apprise/config/base.py @@ -666,7 +666,7 @@ class ConfigBase(URLBase): valid_line_re = re.compile( r"^\s*(?P([;#]+(?P.*))|" r"(\s*(?P[a-z0-9, \t_-]+)\s*=|=)?\s*" - r"((?P[a-z0-9]{1,12}://.*)|(?P[a-z0-9, \t_-]+))|" + r"((?P[a-z0-9]{1,32}://.*)|(?P[a-z0-9, \t_-]+))|" r"include\s+(?P.+))?\s*$", re.I, ) diff --git a/apprise/plugins/notificationapi.py b/apprise/plugins/notificationapi.py index aab08fca..defd72c1 100644 --- a/apprise/plugins/notificationapi.py +++ b/apprise/plugins/notificationapi.py @@ -32,6 +32,7 @@ from __future__ import annotations import base64 from email.utils import formataddr +from itertools import chain from json import dumps, loads import re @@ -51,7 +52,7 @@ from .base import NotifyBase # Used to detect ID IS_VALID_ID_RE = re.compile( - r"^\s*(@|%40)?(?P[\w_-]+)\s*", re.I) + r"^\s*(@|%40)?(?P[\w_-]+)\s*$", re.I) class NotificationAPIRegion: @@ -85,7 +86,7 @@ class NotificationAPIChannel: INAPP = "inapp" WEB_PUSH = "web_push" MOBILE_PUSH = "mobile_push" - SLACK = "mobile_push" + SLACK = "slack" # A List of our channels we can use for verification @@ -209,10 +210,6 @@ class NotifyNotificationAPI(NotifyBase): "type": "choice:string", "values": NOTIFICATIONAPI_MODES, }, - "base_url": { - "name": _("Base URL Override"), - "type": "string", - }, "to": { "alias_of": "targets", }, @@ -328,7 +325,6 @@ class NotifyNotificationAPI(NotifyBase): self.message_type = self.default_message_type else: - # Validate information self.message_type = validate_regex( message_type, *self.template_tokens["type"]["regex"]) if not self.message_type: @@ -382,6 +378,9 @@ class NotifyNotificationAPI(NotifyBase): raise TypeError(msg) from None self.channels.add(channel) + # Used for URL generation afterwards only + self._invalid_targets = [] + if targets: current_target = {} for entry in parse_list(targets, sort=False): @@ -417,7 +416,7 @@ class NotifyNotificationAPI(NotifyBase): if result: if "number" not in current_target: current_target["number"] = \ - ('+' if entry[0] == '+' else '') + result["full"] + ("+" if entry[0] == "+" else "") + result["full"] if not self.channels: self.channels.add(NotificationAPIChannel.SMS) self.logger.info( @@ -448,17 +447,17 @@ class NotifyNotificationAPI(NotifyBase): current_target["id"] = result.group("id") continue - elif "id" in current_target: - # Store and move on - self.targets.append(current_target) - current_target = { - "id": result.group("id") - } - continue + # Store id in next target and move on + self.targets.append(current_target) + current_target = { + "id": result.group("id") + } + continue self.logger.warning( - "Ignoring invalid NotificationAPI target " + "Dropped invalid NotificationAPI target " f"({entry}) specified") + self._invalid_targets.append(entry) continue if "id" in current_target: @@ -509,14 +508,6 @@ class NotifyNotificationAPI(NotifyBase): if isinstance(tokens, dict): self.tokens.update(tokens) - elif tokens: - msg = ( - "The specified NotificationAPI Template Tokens " - f"({tokens}) are not identified as a dictionary." - ) - self.logger.warning(msg) - raise TypeError(msg) - return @property @@ -572,7 +563,7 @@ class NotifyNotificationAPI(NotifyBase): if self.channels: # Prepare our default channel - params["channels"] = self.channels + params["channels"] = ",".join(self.channels) if self.region != self.template_args["region"]["default"]: # Prepare our default region @@ -589,8 +580,8 @@ class NotifyNotificationAPI(NotifyBase): targets = [] for target in self.targets: - if "id" in target: - targets.append(f"@{target['id']}") + # ID is always present + targets.append(f"@{target['id']}") if "number" in target: targets.append(f"{target['number']}") if "email" in target: @@ -603,7 +594,8 @@ class NotifyNotificationAPI(NotifyBase): mtype=mtype, cid=self.pprint(self.client_id, privacy, safe=""), secret=self.pprint(self.client_secret, privacy, safe=""), - targets=NotifyNotificationAPI.quote("/".join(targets), safe="/"), + targets=NotifyNotificationAPI.quote("/".join( + chain(targets, self._invalid_targets)), safe="/"), params=NotifyNotificationAPI.urlencode(params), ) @@ -649,9 +641,9 @@ class NotifyNotificationAPI(NotifyBase): if self.notify_format == NotifyFormat.HTML else body for channel in self.channels: - # Python v3.10 supports `match/case` but since Apprise aims to be - # compatible with Python v3.9+, we must use if/else for the time - # being + # Python v3.10 supports `match/case` but since Apprise aims to + # be compatible with Python v3.9+, we must use if/else for the + # time being if channel == NotificationAPIChannel.SMS: _payload.update({ NotificationAPIChannel.SMS: { @@ -703,7 +695,7 @@ class NotifyNotificationAPI(NotifyBase): }, }) - elif channel == NotificationAPIChannel.SLACK: + else: # channel == NotificationAPIChannel.SLACK _payload.update({ NotificationAPIChannel.SLACK: { "text": (title + "\n" + text_body) @@ -883,8 +875,10 @@ class NotifyNotificationAPI(NotifyBase): results["client_secret"] = None # Prepare our targets (starting with our host) - results["targets"] = [ - NotifyNotificationAPI.unquote(results["host"])] + results["targets"] = [] + if results["host"]: + results["targets"].append( + NotifyNotificationAPI.unquote(results["host"])) # For tracking email sources results["from_addr"] = None @@ -919,13 +913,9 @@ class NotifyNotificationAPI(NotifyBase): results["region"] = \ NotifyNotificationAPI.unquote(results["qsd"]["region"]) - if "channel" in results["qsd"] and len(results["qsd"]["channel"]): - results["channel"] = \ - NotifyNotificationAPI.unquote(results["qsd"]["channel"]) - - if "type" in results["qsd"] and len(results["qsd"]["type"]): - results["message_type"] = \ - NotifyNotificationAPI.unquote(results["qsd"]["type"]) + if "channels" in results["qsd"] and len(results["qsd"]["channels"]): + results["channels"] = \ + NotifyNotificationAPI.unquote(results["qsd"]["channels"]) if "mode" in results["qsd"] and len(results["qsd"]["mode"]): results["mode"] = \ @@ -935,6 +925,11 @@ class NotifyNotificationAPI(NotifyBase): results["reply_to"] = \ NotifyNotificationAPI.unquote(results["qsd"]["reply"]) + # Handling of Message Type + if "type" in results["qsd"] and len(results["qsd"]["type"]): + results["message_type"] = \ + NotifyNotificationAPI.unquote(results["qsd"]["type"]) + elif results["user"]: # Pull from user results["message_type"] = \ diff --git a/apprise/utils/parse.py b/apprise/utils/parse.py index 1400c3c9..ecceea3a 100644 --- a/apprise/utils/parse.py +++ b/apprise/utils/parse.py @@ -58,14 +58,14 @@ NOTIFY_CUSTOM_DEL_TOKENS = re.compile(r"^-(?P.*)\s*") NOTIFY_CUSTOM_COLON_TOKENS = re.compile(r"^:(?P.*)\s*") # Used for attempting to acquire the schema if the URL can't be parsed. -GET_SCHEMA_RE = re.compile(r"\s*(?P[a-z0-9]{1,12})://.*$", re.I) +GET_SCHEMA_RE = re.compile(r"\s*(?P[a-z0-9]{1,32})://.*$", re.I) # Used for validating that a provided entry is indeed a schema # this is slightly different then the GET_SCHEMA_RE above which # insists the schema is only valid with a :// entry. this one # extrapolates the individual entries URL_DETAILS_RE = re.compile( - r"\s*(?P[a-z0-9]{1,12})(://(?P.*))?$", re.I + r"\s*(?P[a-z0-9]{1,32})(://(?P.*))?$", re.I ) # Regular expression based and expanded from: @@ -124,7 +124,7 @@ CALL_SIGN_DETECTION_RE = re.compile( # Regular expression used to destinguish between multiple URLs URL_DETECTION_RE = re.compile( - r"([a-z0-9]+?:\/\/.*?)(?=$|[\s,]+[a-z0-9]{1,12}?:\/\/)", re.I + r"([a-z0-9]+?:\/\/.*?)(?=$|[\s,]+[a-z0-9]{1,32}?:\/\/)", re.I ) EMAIL_DETECTION_RE = re.compile( @@ -1100,7 +1100,7 @@ def parse_list(*args, cast=None, allow_whitespace=True, sort=True): ) ) return ( - [x for x in filter(bool, list(result))] + list(filter(bool, list(result))) if allow_whitespace else [x.strip() for x in filter(bool, list(result)) if x.strip()] ) diff --git a/packaging/redhat/python-apprise.spec b/packaging/redhat/python-apprise.spec index 2c472122..b0d82f86 100644 --- a/packaging/redhat/python-apprise.spec +++ b/packaging/redhat/python-apprise.spec @@ -64,9 +64,9 @@ notification services. It supports sending alerts to platforms such as: \ `Kumulos`, `LaMetric`, `Lark`, `Line`, `MacOSX`, `Mailgun`, `Mastodon`, \ `Mattermost`, `Matrix`, `MessageBird`, `Microsoft Windows`, \ `Microsoft Teams`, `Misskey`, `MQTT`, `MSG91`, `MyAndroid`, `Nexmo`, \ -`Nextcloud`, `NextcloudTalk`, `Notica`, `Notifiarr`, `Notifico`, `ntfy`, \ -`Office365`, `OneSignal`, `Opsgenie`, `PagerDuty`, `PagerTree`, \ -`ParsePlatform`, `Plivo`, `PopcornNotify`, `Prowl`, `Pushalot`, \ +`Nextcloud`, `NextcloudTalk`, `Notica`, `NotificationAPI`, `Notifiarr`, +`Notifico`, `ntfy`, \ `Office365`, `OneSignal`, `Opsgenie`, `PagerDuty`, \ +`PagerTree`, `ParsePlatform`, `Plivo`, `PopcornNotify`, `Prowl`, `Pushalot`, \ `PushBullet`, `Pushjet`, `PushMe`, `Pushover`, `Pushplus`, `PushSafer`, \ `Pushy`, `PushDeer`, `QQ Push`, `Revolt`, `Reddit`, `Resend`, `Rocket.Chat`, \ `RSyslog`, `SendGrid`, `SendPulse`, `ServerChan`, `Seven`, `SFR`, `Signal`, \ diff --git a/pyproject.toml b/pyproject.toml index 975c60ca..c6f98b88 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -107,6 +107,7 @@ keywords = [ "Nextcloud", "NextcloudTalk", "Notica", + "NoticationAPI", "Notifiarr", "Notifico", "Ntfy", diff --git a/tests/test_plugin_notificationapi.py b/tests/test_plugin_notificationapi.py index 0be4a342..1e42dc9e 100644 --- a/tests/test_plugin_notificationapi.py +++ b/tests/test_plugin_notificationapi.py @@ -63,25 +63,103 @@ apprise_url_tests = ( # Just an Email specified, no client_id or client_secret "instance": TypeError, }), - ("napi://user@client_id/cs14/user@example.ca", { - # No id matched + ("napi://user@client_id/cs14a/user@example.ca", { + # No id matched "instance": TypeError, - }), - ("napi://type@client_id/client_secret/id/+15551235553/", { + }), + ("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://type@cid/secret/id/user1@example.com/", { + ("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"), { + "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"), { + "?from=joe@example.ca&reply=user@abc.com"), { # Set from/source "instance": NotifyNotificationAPI, "requests_response_text": NOTIFICATIONAPI_GOOD_RESPONSE, @@ -91,18 +169,43 @@ apprise_url_tests = ( # 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=type", { + ("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=ci&secret=cs&type=test-type", { + ("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 @@ -115,6 +218,12 @@ apprise_url_tests = ( "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 @@ -145,7 +254,8 @@ apprise_url_tests = ( "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", { + "id/kris@example.com/id/chris2@example.com/id/+15552341234" + "?:token=value", { # Several emails to notify "instance": NotifyNotificationAPI, "requests_response_text": NOTIFICATIONAPI_GOOD_RESPONSE, @@ -230,8 +340,8 @@ def test_plugin_napi_urls(): @mock.patch("requests.post") -def test_plugin_napi_sms_payloads(mock_post): - """NotifyNotificationAPI() Testing SMS Payloads.""" +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 @@ -261,8 +371,7 @@ def test_plugin_napi_sms_payloads(mock_post): is True ) - # 2 calls were made, one to perform an email lookup, the second - # was the notification itself + # delivery of message assert mock_post.call_count == 1 assert ( mock_post.call_args_list[0][0][0] @@ -299,8 +408,8 @@ def test_plugin_napi_sms_payloads(mock_post): @mock.patch("requests.post") -def test_plugin_napi_email_payloads(mock_post): - """NotifyNotificationAPI() Testing Email Payloads.""" +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 @@ -331,8 +440,7 @@ def test_plugin_napi_email_payloads(mock_post): is True ) - # 2 calls were made, one to perform an email lookup, the second - # was the notification itself + # delivery of message assert mock_post.call_count == 1 assert ( mock_post.call_args_list[0][0][0] @@ -375,3 +483,191 @@ def test_plugin_napi_email_payloads(mock_post): # 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)