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)