basic tests passing

Chris Caron 2024-08-30 22:26:50 -04:00
parent e5e7ab0784
commit 5e1e4857f1
2 changed files with 183 additions and 84 deletions

View File

@ -30,7 +30,7 @@
# - https://sendpulse.com/integrations/api/smtp # - https://sendpulse.com/integrations/api/smtp
import requests import requests
from json import dumps from json import dumps, loads
import base64 import base64
from .base import NotifyBase from .base import NotifyBase
@ -69,6 +69,9 @@ class NotifySendPulse(NotifyBase):
# The default Email API URL to use # The default Email API URL to use
notify_email_url = 'https://api.sendpulse.com/smtp/emails' notify_email_url = 'https://api.sendpulse.com/smtp/emails'
# Our OAuth Query
notify_oauth_url = 'https://api.sendpulse.com/oauth/access_token'
# Support attachments # Support attachments
attachment_support = True attachment_support = True
@ -167,6 +170,15 @@ class NotifySendPulse(NotifyBase):
""" """
super().__init__(**kwargs) super().__init__(**kwargs)
# Api Key is acquired upon a sucessful login
self.access_token = None
result = is_email(from_email)
if not result:
msg = 'Invalid ~From~ email specified: {}'.format(from_email)
self.logger.warning(msg)
raise TypeError(msg)
# Client ID # Client ID
self.client_id = validate_regex( self.client_id = validate_regex(
client_id, *self.template_tokens['client_id']['regex']) client_id, *self.template_tokens['client_id']['regex'])
@ -185,12 +197,6 @@ class NotifySendPulse(NotifyBase):
self.logger.warning(msg) self.logger.warning(msg)
raise TypeError(msg) raise TypeError(msg)
result = is_email(from_email)
if not result:
msg = 'Invalid ~From~ email specified: {}'.format(from_email)
self.logger.warning(msg)
raise TypeError(msg)
# Tracks emails to name lookups (if they exist) # Tracks emails to name lookups (if they exist)
self.__email_map = {} self.__email_map = {}
@ -208,6 +214,8 @@ class NotifySendPulse(NotifyBase):
# Acquire Blind Carbon Copies # Acquire Blind Carbon Copies
self.bcc = set() self.bcc = set()
# No template
self.template = None
if template: if template:
try: try:
# Store our template # Store our template
@ -229,9 +237,9 @@ class NotifySendPulse(NotifyBase):
for recipient in parse_emails(targets): for recipient in parse_emails(targets):
result = is_email(recipient) result = is_email(recipient)
if result: if result:
self.targets.append( self.targets.append(result['full_email'])
(result['name'] if result['name'] else False, if result['name']:
result['full_email'])) self.__email_map[result['full_email']] = result['name']
continue continue
self.logger.warning( self.logger.warning(
@ -250,7 +258,7 @@ class NotifySendPulse(NotifyBase):
if result: if result:
self.cc.add(result['full_email']) self.cc.add(result['full_email'])
if result['name']: if result['name']:
self.__email_lookup[result['full_email']] = result['name'] self.__email_map[result['full_email']] = result['name']
continue continue
self.logger.warning( self.logger.warning(
@ -265,7 +273,7 @@ class NotifySendPulse(NotifyBase):
if result: if result:
self.bcc.add(result['full_email']) self.bcc.add(result['full_email'])
if result['name']: if result['name']:
self.__email_lookup[result['full_email']] = result['name'] self.__email_map[result['full_email']] = result['name']
continue continue
self.logger.warning( self.logger.warning(
@ -300,8 +308,8 @@ class NotifySendPulse(NotifyBase):
# Handle our Carbon Copy Addresses # Handle our Carbon Copy Addresses
params['cc'] = ','.join([ params['cc'] = ','.join([
formataddr( formataddr(
(self.__email_lookup[e] (self.__email_map[e]
if e in self.__email_lookup else False, e), if e in self.__email_map else False, e),
# Swap comma for it's escaped url code (if detected) since # Swap comma for it's escaped url code (if detected) since
# we're using that as a delimiter # we're using that as a delimiter
charset='utf-8').replace(',', '%2C') charset='utf-8').replace(',', '%2C')
@ -311,8 +319,8 @@ class NotifySendPulse(NotifyBase):
# Handle our Blind Carbon Copy Addresses # Handle our Blind Carbon Copy Addresses
params['bcc'] = ','.join([ params['bcc'] = ','.join([
formataddr( formataddr(
(self.__email_lookup[e] (self.__email_map[e]
if e in self.__email_lookup else False, e), if e in self.__email_map else False, e),
# Swap comma for it's escaped url code (if detected) since # Swap comma for it's escaped url code (if detected) since
# we're using that as a delimiter # we're using that as a delimiter
charset='utf-8').replace(',', '%2C') charset='utf-8').replace(',', '%2C')
@ -353,10 +361,24 @@ class NotifySendPulse(NotifyBase):
Perform SendPulse Notification Perform SendPulse Notification
""" """
headers = { if not self.access_token:
'User-Agent': self.app_id, # Attempt to acquire acquire a login
'Content-Type': 'application/json', _payload = {
'Authorization': 'Bearer {}'.format(self.apikey), 'grant_type': 'client_credentials',
'client_id': self.client_id,
'client_secret': self.client_secret,
}
success, response = self._fetch(self.notify_oauth_url, _payload)
if not success:
return False
# If we get here, we're authenticated
self.access_token = response.get('access_token')
# Store our bearer
extended_headers = {
'Authorization': 'Bearer {}'.format(self.access_token),
} }
# error tracking (used for function return) # error tracking (used for function return)
@ -451,8 +473,8 @@ class NotifySendPulse(NotifyBase):
to = { to = {
'email': target 'email': target
} }
if target in self.__email_lookup: if target in self.__email_map:
to['name'] = self.__email_lookup[target] to['name'] = self.__email_map[target]
# Set our target # Set our target
payload['email']['to'] = [to] payload['email']['to'] = [to]
@ -463,8 +485,8 @@ class NotifySendPulse(NotifyBase):
item = { item = {
'email': email, 'email': email,
} }
if email in self.__email_lookup: if email in self.__email_map:
item['name'] = self.__email_lookup[email] item['name'] = self.__email_map[email]
payload['email']['cc'].append(item) payload['email']['cc'].append(item)
@ -474,33 +496,77 @@ class NotifySendPulse(NotifyBase):
item = { item = {
'email': email, 'email': email,
} }
if email in self.__email_lookup: if email in self.__email_map:
item['name'] = self.__email_lookup[email] item['name'] = self.__email_map[email]
payload['email']['bcc'].append(item) payload['email']['bcc'].append(item)
self.logger.debug('SendPulse POST URL: %s (cert_verify=%r)' % ( # Perform our post
self.notify_email_url, self.verify_certificate, success, response = self._fetch(
)) self.notify_email_url, payload, target, extended_headers)
self.logger.debug('SendPulse Payload: %s' % str(payload)) if not success:
has_error = True
continue
return not has_error
def _fetch(self, url, payload, target=None, extended_headers={}):
"""
Wrapper to request.post() to manage it's response better and make
the send() function cleaner and easier to maintain.
This function returns True if the _post was successful and False
if it wasn't.
"""
headers = {
'User-Agent': self.app_id,
'Content-Type': 'application/json',
}
if extended_headers:
headers.update(extended_headers)
self.logger.debug('SendPulse POST URL: %s (cert_verify=%r)' % (
self.url, self.verify_certificate,
))
self.logger.debug('SendPulse Payload: %s' % str(payload))
# Prepare our default response
response = {}
# Always call throttle before any remote server i/o is made
self.throttle()
try:
r = requests.post(
self.url,
data=dumps(payload),
headers=headers,
verify=self.verify_certificate,
timeout=self.request_timeout,
)
# Always call throttle before any remote server i/o is made
self.throttle()
try: try:
r = requests.post( response = loads(r.content)
self.notify_email_url,
data=dumps(payload),
headers=headers,
verify=self.verify_certificate,
timeout=self.request_timeout,
)
if r.status_code not in (
requests.codes.ok, requests.codes.accepted):
# We had a problem
status_str = \
NotifySendPulse.http_response_code_lookup(
r.status_code)
except (AttributeError, TypeError, ValueError):
# This gets thrown if we can't parse our JSON Response
# - ValueError = r.content is Unparsable
# - TypeError = r.content is None
# - AttributeError = r is None
self.logger.warning('Invalid response from SendPulse server.')
self.logger.debug(
'Response Details:\r\n{}'.format(r.content))
return (False, {})
if r.status_code not in (
requests.codes.ok, requests.codes.accepted):
# We had a problem
status_str = \
NotifySendPulse.http_response_code_lookup(
r.status_code)
if target:
self.logger.warning( self.logger.warning(
'Failed to send SendPulse notification to {}: ' 'Failed to send SendPulse notification to {}: '
'{}{}error={}.'.format( '{}{}error={}.'.format(
@ -508,29 +574,33 @@ class NotifySendPulse(NotifyBase):
status_str, status_str,
', ' if status_str else '', ', ' if status_str else '',
r.status_code)) r.status_code))
self.logger.debug(
'Response Details:\r\n{}'.format(r.content))
# Mark our failure
has_error = True
continue
else: else:
self.logger.warning(
'SendPulse Authentication Request failed: '
'{}{}error={}.'.format(
status_str,
', ' if status_str else '',
r.status_code))
self.logger.debug(
'Response Details:\r\n{}'.format(r.content))
else:
if target:
self.logger.info( self.logger.info(
'Sent SendPulse notification to {}.'.format(target)) 'Sent SendPulse notification to {}.'.format(target))
else:
self.logger.debug('SendPulse authentication successful')
except requests.RequestException as e: return (True, response)
self.logger.warning(
'A Connection error occurred sending SendPulse '
'notification to {}.'.format(target))
self.logger.debug('Socket Exception: %s' % str(e))
# Mark our failure except requests.RequestException as e:
has_error = True self.logger.warning(
continue 'A Connection error occurred sending SendPulse '
'notification to {}.'.format(target))
self.logger.debug('Socket Exception: %s' % str(e))
return not has_error return (False, response)
@staticmethod @staticmethod
def parse_url(url): def parse_url(url):
@ -545,6 +615,12 @@ class NotifySendPulse(NotifyBase):
# We're done early as we couldn't load the results # We're done early as we couldn't load the results
return results return results
# Define our minimum requirements; defining them now saves us from
# having to if/else all kinds of branches below...
results['from_email'] = None
results['client_id'] = None
results['client_secret'] = None
# Our URL looks like this: # Our URL looks like this:
# {schema}://{from_email}:{client_id}/{client_secret}/{targets} # {schema}://{from_email}:{client_id}/{client_secret}/{targets}
# #
@ -564,7 +640,7 @@ class NotifySendPulse(NotifyBase):
results['client_id'] = \ results['client_id'] = \
NotifySendPulse.unquote(results['qsd']['id']) NotifySendPulse.unquote(results['qsd']['id'])
else: elif results['targets']:
# Store our Client ID # Store our Client ID
results['client_id'] = results['targets'].pop(0) results['client_id'] = results['targets'].pop(0)
@ -573,7 +649,7 @@ class NotifySendPulse(NotifyBase):
results['client_secret'] = \ results['client_secret'] = \
NotifySendPulse.unquote(results['qsd']['secret']) NotifySendPulse.unquote(results['qsd']['secret'])
else: elif results['targets']:
# Store our Client Secret # Store our Client Secret
results['client_secret'] = results['targets'].pop(0) results['client_secret'] = results['targets'].pop(0)

View File

@ -26,6 +26,7 @@
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE. # POSSIBILITY OF SUCH DAMAGE.
from json import dumps
from unittest import mock from unittest import mock
import os import os
@ -42,6 +43,12 @@ from helpers import AppriseURLTester
import logging import logging
logging.disable(logging.CRITICAL) logging.disable(logging.CRITICAL)
SENDPULSE_GOOD_RESPONSE = dumps({
"access_token": 'abc123'
})
SENDPULSE_BAD_RESPONSE = '{'
# Attachment Directory # Attachment Directory
TEST_VAR_DIR = os.path.join(os.path.dirname(__file__), 'var') TEST_VAR_DIR = os.path.join(os.path.dirname(__file__), 'var')
@ -54,61 +61,76 @@ apprise_url_tests = (
'instance': None, 'instance': None,
}), }),
('sendpulse://abcd', { ('sendpulse://abcd', {
# Just an broken email (no email, client_id or secret) # invalid from email
'instance': None, 'instance': TypeError,
}), }),
('sendpulse://abcd@host', { ('sendpulse://abcd@host.com', {
# Just an Email specified, no client_id or client_secret # Just an Email specified, no client_id or client_secret
'instance': None, 'instance': TypeError,
}), }),
('sendpulse://user@example.com/client_id/client_secret/', { ('sendpulse://user@example.com/client_id/cs1/', {
'instance': NotifySendPulse, 'instance': NotifySendPulse,
'requests_response_text': SENDPULSE_GOOD_RESPONSE,
}), }),
('sendpulse://user@example.com/client_id/client_secret/' ('sendpulse://user@example.com/client_id/cs1a/', {
'instance': NotifySendPulse,
'requests_response_text': SENDPULSE_BAD_RESPONSE,
# Notify will fail because auth failed
'response': False,
}),
('sendpulse://user@example.com/client_id/cs2/'
'?bcc=l2g@nuxref.com', { '?bcc=l2g@nuxref.com', {
# A good email with Blind Carbon Copy # A good email with Blind Carbon Copy
'instance': NotifySendPulse, 'instance': NotifySendPulse,
'requests_response_text': SENDPULSE_GOOD_RESPONSE,
}), }),
('sendpulse://user@example.com/client_id/client_secret/' ('sendpulse://user@example.com/client_id/cs3/'
'?cc=l2g@nuxref.com', { '?cc=l2g@nuxref.com', {
# A good email with Carbon Copy # A good email with Carbon Copy
'instance': NotifySendPulse, 'instance': NotifySendPulse,
'requests_response_text': SENDPULSE_GOOD_RESPONSE,
}), }),
('sendpulse://user@example.com/client_id/client_secret/' ('sendpulse://user@example.com/client_id/cs4/'
'?to=l2g@nuxref.com', { '?to=l2g@nuxref.com', {
# A good email with Carbon Copy # A good email with Carbon Copy
'instance': NotifySendPulse, 'instance': NotifySendPulse,
'requests_response_text': SENDPULSE_GOOD_RESPONSE,
}), }),
('sendpulse://user@example.com/client_id/client_secret/' ('sendpulse://user@example.com/client_id/cs5/'
'?template=1234', { '?template=1234', {
# A good email with a template + no substitutions # A good email with a template + no substitutions
'instance': NotifySendPulse, 'instance': NotifySendPulse,
'requests_response_text': SENDPULSE_GOOD_RESPONSE,
}), }),
('sendpulse://user@example.com/client_id/client_secret/' ('sendpulse://user@example.com/client_id/cs6/'
'?template=1234&+sub=value&+sub2=value2', { '?template=1234&+sub=value&+sub2=value2', {
# A good email with a template + substitutions # A good email with a template + substitutions
'instance': NotifySendPulse, 'instance': NotifySendPulse,
'requests_response_text': SENDPULSE_GOOD_RESPONSE,
# Our expected url(privacy=True) startswith() response: # Our expected url(privacy=True) startswith() response:
'privacy_url': 'sendpulse://a...d:user@example.com/', 'privacy_url': 'sendpulse://user@example.com/c...d/c...6/',
}), }),
('sendpulse://user@example.com/client_id/client_secret/', { ('sendpulse://user@example.com/client_id/cs7/', {
'instance': NotifySendPulse, 'instance': NotifySendPulse,
# force a failure # force a failure
'response': False, 'response': False,
'requests_response_code': requests.codes.internal_server_error, 'requests_response_code': requests.codes.internal_server_error,
'requests_response_text': SENDPULSE_GOOD_RESPONSE,
}), }),
('sendpulse://user@example.com/client_id/client_secret/', { ('sendpulse://user@example.com/client_id/cs8/', {
'instance': NotifySendPulse, 'instance': NotifySendPulse,
# throw a bizzare code forcing us to fail to look it up # throw a bizzare code forcing us to fail to look it up
'response': False, 'response': False,
'requests_response_code': 999, 'requests_response_code': 999,
'requests_response_text': SENDPULSE_GOOD_RESPONSE,
}), }),
('sendpulse://user@example.com/client_id/client_secret/', { ('sendpulse://user@example.com/client_id/cs9/', {
'instance': NotifySendPulse, 'instance': NotifySendPulse,
# Throws a series of connection and transfer exceptions when this flag # Throws a series of connection and transfer exceptions when this flag
# is set and tests that we gracfully handle them # is set and tests that we gracfully handle them
'test_requests_exceptions': True, 'test_requests_exceptions': True,
'requests_response_text': SENDPULSE_GOOD_RESPONSE,
}), }),
) )
@ -139,27 +161,27 @@ def test_plugin_sendpulse_edge_cases(mock_post, mock_get):
with pytest.raises(TypeError): with pytest.raises(TypeError):
NotifySendPulse( NotifySendPulse(
client_id=None, client_secret='abcd', client_id=None, client_secret='abcd123',
from_email='user@example.com') from_email='user@example.com')
# invalid from email # invalid from email
with pytest.raises(TypeError): with pytest.raises(TypeError):
NotifySendPulse( NotifySendPulse(
client_id='abcd', client_secret='abcd', from_email='!invalid') client_id='abcd', client_secret='abcd456', from_email='!invalid')
# no email # no email
with pytest.raises(TypeError): with pytest.raises(TypeError):
NotifySendPulse( NotifySendPulse(
client_id='abcd', client_secret='abcd', from_email=None) client_id='abcd', client_secret='abcd789', from_email=None)
# Invalid To email address # Invalid To email address
NotifySendPulse( NotifySendPulse(
client_id='abcd', client_secret='abcd', client_id='abcd', client_secret='abcd321',
from_email='user@example.com', targets="!invalid") from_email='user@example.com', targets="!invalid")
# Test invalid bcc/cc entries mixed with good ones # Test invalid bcc/cc entries mixed with good ones
assert isinstance(NotifySendPulse( assert isinstance(NotifySendPulse(
client_id='abcd', client_secret='abcd', client_id='abcd', client_secret='abcd654',
from_email='l2g@example.com', from_email='l2g@example.com',
bcc=('abc@def.com', '!invalid'), bcc=('abc@def.com', '!invalid'),
cc=('abc@test.org', '!invalid')), NotifySendPulse) cc=('abc@test.org', '!invalid')), NotifySendPulse)
@ -175,6 +197,7 @@ def test_plugin_sendpulse_attachments(mock_post, mock_get):
request = mock.Mock() request = mock.Mock()
request.status_code = requests.codes.ok request.status_code = requests.codes.ok
request.content = SENDPULSE_GOOD_RESPONSE
# Prepare Mock # Prepare Mock
mock_post.return_value = request mock_post.return_value = request
@ -182,7 +205,7 @@ def test_plugin_sendpulse_attachments(mock_post, mock_get):
path = os.path.join(TEST_VAR_DIR, 'apprise-test.gif') path = os.path.join(TEST_VAR_DIR, 'apprise-test.gif')
attach = AppriseAttachment(path) attach = AppriseAttachment(path)
obj = Apprise.instantiate('sendpulse://user@example.com/abcd/abcd') obj = Apprise.instantiate('sendpulse://user@example.com/aaaa/bbbb')
assert isinstance(obj, NotifySendPulse) assert isinstance(obj, NotifySendPulse)
assert obj.notify( assert obj.notify(
body='body', title='title', notify_type=NotifyType.INFO, body='body', title='title', notify_type=NotifyType.INFO,