def run_server(apprise: Apprise): apprise.notify("Starting notification service") public_ip = "" while True: current_ip = get_public_ip() if public_ip != current_ip: apprise.notify(f"Public IP changed to '{current_ip}'") public_ip = current_ip sleep(CHECK_INTERVAL_IN_SECONDS)
def test_plugin_signal_edge_cases(mock_post): """ NotifySignalAPI() Edge Cases """ # Disable Throttling to speed testing plugins.NotifyBase.request_rate_per_sec = 0 # Prepare our response response = requests.Request() response.status_code = requests.codes.ok # Prepare Mock mock_post.return_value = response # Initialize some generic (but valid) tokens source = '+1 (555) 123-3456' target = '+1 (555) 987-5432' body = "test body" title = "My Title" # No apikey specified with pytest.raises(TypeError): plugins.NotifySignalAPI(source=None) aobj = Apprise() assert aobj.add("signals://*****:*****@localhost:231/{}/{}?status=True".format( source, target)) assert aobj.notify(title=title, body=body) assert mock_post.call_count == 1 details = mock_post.call_args_list[0] assert details[0][0] == 'https://localhost:231/v2/send' payload = loads(details[1]['data']) # Status flag is set assert payload['message'] == '[i] My Title\r\ntest body'
def update_services(client: DockerClient, apprise: Apprise): """ Update all the services found on the Docker Swarm. :param client: Docker Client that is connected to a Docker Swarm Manager :param apprise: Apprise notification service """ services = client.services.list() logger.info(f'Checking for updates on {len(services)} service(s).') for service in services: name = service.name outdated, tag, digest = is_service_outdated(client, service) if outdated: update_message = f'Found update for `{tag}`, updating.' mode = service.attrs['Spec']['Mode'] replicated = 'Replicated' in mode if replicated: replicas = mode['Replicated']['Replicas'] plural = 's' if replicas > 1 else '' update_message = f"Found update for `{tag}`, updating {replicas} replica{plural}." apprise.notify(title=f'Service: `{name}`', body=update_message, notify_type=NotifyType.INFO) logger.info( f'Found update for service \'{name}\', updating using image {tag}' ) start = time.time() full_image = f"{tag}@{digest}" service.update(image=full_image, force_update=True) # Update the service end = time.time() elapsed = str(( end - start))[:4] # Calculate the time it took to update the service logger.info( f'Update for service \'{name}\' successful, took {elapsed} seconds ({full_image})' ) success_message = f'Update successful. Took {elapsed} seconds.' apprise.notify(title=f'Service: `{name}`', body=success_message, notify_type=NotifyType.SUCCESS) if getenv('PARALLEL_UPDATES', '').lower() in ['false', 'no', 'off', '0']: # When there are more images to update in one pass then update them one by one return else: logger.debug(f'No update found for service \'{name}\'')
def send_notification(message, subject): apprise_client = Apprise() if APPRISE_CONFIG_STRING: apprise_client.add(APPRISE_CONFIG_STRING) if APPRISE_CONFIG_URL: config = AppriseConfig() config.add(APPRISE_CONFIG_URL) apprise_client.add(config) res = apprise_client.notify(body=message, title=subject) print(res) if not res: print('Failed to send notification') else: print('Successfully sent notification') return res
def test_smtplib_send_okay(mock_smtplib): """ API: Test a successfully sent email """ # Disable Throttling to speed testing plugins.NotifyBase.request_rate_per_sec = 0 # Defaults to HTML obj = Apprise.instantiate('mailto://*****:*****@gmail.com', suppress_exceptions=False) assert isinstance(obj, plugins.NotifyEmail) # Support an email simulation where we can correctly quit mock_smtplib.starttls.return_value = True mock_smtplib.login.return_value = True mock_smtplib.sendmail.return_value = True mock_smtplib.quit.return_value = True assert obj.notify(body='body', title='test', notify_type=NotifyType.INFO) is True # Set Text obj = Apprise.instantiate('mailto://*****:*****@gmail.com?format=text', suppress_exceptions=False) assert isinstance(obj, plugins.NotifyEmail) assert obj.notify(body='body', title='test', notify_type=NotifyType.INFO) is True # Create an apprise object to work with as well a = Apprise() assert a.add('mailto://*****:*****@gmail.com?format=text') # Send Attachment with success attach = os.path.join(TEST_VAR_DIR, 'apprise-test.gif') assert obj.notify( body='body', title='test', notify_type=NotifyType.INFO, attach=attach) is True # same results happen from our Apprise object assert a.notify(body='body', title='test', attach=attach) is True # test using an Apprise Attachment object assert obj.notify(body='body', title='test', notify_type=NotifyType.INFO, attach=AppriseAttachment(attach)) is True # same results happen from our Apprise object assert a.notify( body='body', title='test', attach=AppriseAttachment(attach)) is True max_file_size = AttachBase.max_file_size # Now do a case where the file can't be sent AttachBase.max_file_size = 1 assert obj.notify( body='body', title='test', notify_type=NotifyType.INFO, attach=attach) is False # same results happen from our Apprise object assert a.notify(body='body', title='test', attach=attach) is False # Restore value AttachBase.max_file_size = max_file_size
def test_apprise_asyncio_runtime_error(): """ API: Apprise() AsyncIO RuntimeError handling """ class GoodNotification(NotifyBase): def __init__(self, **kwargs): super(GoodNotification, self).__init__(notify_format=NotifyFormat.HTML, **kwargs) def url(self): # Support URL return '' def send(self, **kwargs): # Pretend everything is okay return True @staticmethod def parse_url(url, *args, **kwargs): # always parseable return NotifyBase.parse_url(url, verify_host=False) # Store our good notification in our schema map SCHEMA_MAP['good'] = GoodNotification # Create ourselves an Apprise object a = Apprise() # Add a few entries for _ in range(25): a.add('good://') # Python v3.6 and lower can't handle situations gracefully when an # event_loop isn't already established(). Test that Apprise can handle # these situations import asyncio # Get our event loop loop = asyncio.get_event_loop() # Adjust out event loop to not point at anything asyncio.set_event_loop(None) # With the event loop inactive, we'll fail trying to get the active loop with pytest.raises(RuntimeError): asyncio.get_event_loop() try: # Below, we internally will throw a RuntimeError() since there will # be no active event_loop in place. However internally it will be smart # enough to create a new event loop and continue... assert a.notify(title="title", body="body") is True # Verify we have an active event loop new_loop = asyncio.get_event_loop() # We didn't throw an exception above; thus we have an event loop at # this point assert new_loop # Close off the internal loop created inside a.notify() new_loop.close() finally: # Restore our event loop (in the event the above test failed) asyncio.set_event_loop(loop)
def notify(self, servers, body, title, notify_type=NotifyType.INFO, body_format=NotifyFormat.MARKDOWN): """ processes list of servers specified """ # Decode our data body = decode(body) title = decode(title) # Apprise Asset Object asset = AppriseAsset(theme=self.default_theme) asset.app_id = 'NZB-Notify' asset.app_desc = 'NZB Notification' asset.app_url = 'https://github.com/caronc/nzb-notify' # Source Theme from GitHub Page asset.image_url_mask = 'https://raw.githubusercontent.com' \ '/caronc/nzb-notify/master/Notify' \ '/apprise-theme/{THEME}/apprise-{TYPE}-{XY}.png' asset.image_path_mask = join( dirname(__file__), 'Notify', 'apprise-theme', '{THEME}', 'apprise-{TYPE}-{XY}.png') # Include Image Flag _url = self.parse_url(self.get('IncludeImage')) # Define some globals to use in this function image_path = None image_url = None if _url: # Toggle our include image flag right away to True include_image = True # Get some more details if not re.match('^(https?|file)$', _url['schema'], re.IGNORECASE): self.logger.error( 'An invalid image url protocol (%s://) was specified.' % _url['schema'], ) return False if _url['schema'] == 'file': if not isfile(_url['fullpath']): self.logger.error( 'The specified file %s was not found.' % _url['fullpath'], ) return False image_path = _url['fullpath'] else: # We're dealing with a web request image_url = _url['url'] else: # Dealing with the old way of doing things; just toggling a # true/false flag include_image = self.parse_bool(self.get('IncludeImage'), False) if isinstance(servers, basestring): # servers can be a list of URLs, or it can be # a string which will be parsed into this list # we wanted. servers = self.parse_list(self.get('Servers', '')) # Create our apprise object a = Apprise(asset=asset) for server in servers: # Add our URL if not a.add(server): # Validation Failure self.logger.error( 'Could not initialize %s instance.' % server, ) continue # Notify our servers a.notify(body=body, title=title, notify_type=notify_type, body_format=body_format) # Always return true return True
def test_apprise_tagging(mock_post, mock_get): """ API: Apprise() object tagging functionality """ # A request robj = mock.Mock() setattr(robj, 'raw', mock.Mock()) # Allow raw.read() calls robj.raw.read.return_value = '' robj.text = '' robj.content = '' mock_get.return_value = robj mock_post.return_value = robj # Simulate a successful notification mock_get.return_value.status_code = requests.codes.ok mock_post.return_value.status_code = requests.codes.ok # Create our object a = Apprise() # An invalid addition can't add the tag assert a.add('averyinvalidschema://localhost', tag='uhoh') is False assert a.add({ 'schema': 'averyinvalidschema', 'host': 'localhost' }, tag='uhoh') is False # Add entry and assign it to a tag called 'awesome' assert a.add('json://localhost/path1/', tag='awesome') is True assert a.add({ 'schema': 'json', 'host': 'localhost', 'fullpath': '/path1/' }, tag='awesome') is True # Add another notification and assign it to a tag called 'awesome' # and another tag called 'local' assert a.add('json://localhost/path2/', tag=['mmost', 'awesome']) is True # notify the awesome tag; this would notify both services behind the # scenes assert a.notify(title="my title", body="my body", tag='awesome') is True # notify all of the tags assert a.notify(title="my title", body="my body", tag=['awesome', 'mmost' ]) is True # When we query against our loaded notifications for a tag that simply # isn't assigned to anything, we return None. None (different then False) # tells us that we litterally had nothing to query. We didn't fail... # but we also didn't do anything... assert a.notify(title="my title", body="my body", tag='missing') is None # Now to test the ability to and and/or notifications a = Apprise() # Add a tag by tuple assert a.add('json://localhost/tagA/', tag=("TagA", )) is True # Add 2 tags by string assert a.add('json://localhost/tagAB/', tag="TagA, TagB") is True # Add a tag using a set assert a.add('json://localhost/tagB/', tag=set(["TagB"])) is True # Add a tag by string (again) assert a.add('json://localhost/tagC/', tag="TagC") is True # Add 2 tags using a list assert a.add('json://localhost/tagCD/', tag=["TagC", "TagD"]) is True # Add a tag by string (again) assert a.add('json://localhost/tagD/', tag="TagD") is True # add a tag set by set (again) assert a.add('json://localhost/tagCDE/', tag=set(["TagC", "TagD", "TagE" ])) is True # Expression: TagC and TagD # Matches the following only: # - json://localhost/tagCD/ # - json://localhost/tagCDE/ assert a.notify(title="my title", body="my body", tag=[('TagC', 'TagD') ]) is True # Expression: (TagY and TagZ) or TagX # Matches nothing, None is returned in this case assert a.notify(title="my title", body="my body", tag=[('TagY', 'TagZ'), 'TagX']) is None # Expression: (TagY and TagZ) or TagA # Matches the following only: # - json://localhost/tagAB/ assert a.notify(title="my title", body="my body", tag=[('TagY', 'TagZ'), 'TagA']) is True # Expression: (TagE and TagD) or TagB # Matches the following only: # - json://localhost/tagCDE/ # - json://localhost/tagAB/ # - json://localhost/tagB/ assert a.notify(title="my title", body="my body", tag=[('TagE', 'TagD'), 'TagB']) is True # Garbage Entries in tag field just get stripped out. the below # is the same as notifying no tags at all. Since we have not added # any entries that do not have tags (that we can match against) # we fail. None is returned as a way of letting us know that we # had Notifications to notify, but since none of them matched our tag # none were notified. assert a.notify(title="my title", body="my body", tag=[ (object, ), ]) is None
def test_apprise(): """ API: Apprise() object """ # Caling load matix a second time which is an internal function causes it # to skip over content already loaded into our matrix and thefore accesses # other if/else parts of the code that aren't otherwise called __load_matrix() a = Apprise() # no items assert len(a) == 0 # Apprise object can also be directly tested with 'if' keyword # No entries results in a False response assert not a # Create an Asset object asset = AppriseAsset(theme='default') # We can load the device using our asset a = Apprise(asset=asset) # We can load our servers up front as well servers = [ 'faast://abcdefghijklmnop-abcdefg', 'kodi://kodi.server.local', ] a = Apprise(servers=servers) # 2 servers loaded assert len(a) == 2 # Apprise object can also be directly tested with 'if' keyword # At least one entry results in a True response assert a # We can retrieve our URLs this way: assert len(a.urls()) == 2 # We can add another server assert a.add('mmosts://mattermost.server.local/' '3ccdd113474722377935511fc85d3dd4') is True assert len(a) == 3 # Try adding nothing but delimiters assert a.add(',, ,, , , , ,') is False # The number of servers added doesn't change assert len(a) == 3 # We can pop an object off of our stack by it's indexed value: obj = a.pop(0) assert isinstance(obj, NotifyBase) is True assert len(a) == 2 # We can retrieve elements from our list too by reference: assert isinstance(a[0].url(), six.string_types) is True # We can iterate over our list too: count = 0 for o in a: assert isinstance(o.url(), six.string_types) is True count += 1 # verify that we did indeed iterate over each element assert len(a) == count # We can empty our set a.clear() assert len(a) == 0 # An invalid schema assert a.add('this is not a parseable url at all') is False assert len(a) == 0 # An unsupported schema assert a.add( 'invalid://we.just.do.not.support.this.plugin.type') is False assert len(a) == 0 # A poorly formatted URL assert a.add('json://user:@@@:bad?no.good') is False assert len(a) == 0 # Add a server with our asset we created earlier assert a.add('mmosts://mattermost.server.local/' '3ccdd113474722377935511fc85d3dd4', asset=asset) is True # Clear our server listings again a.clear() # No servers to notify assert a.notify(title="my title", body="my body") is False class BadNotification(NotifyBase): def __init__(self, **kwargs): super(BadNotification, self).__init__(**kwargs) # We fail whenever we're initialized raise TypeError() def url(self): # Support URL return '' @staticmethod def parse_url(url, *args, **kwargs): # always parseable return NotifyBase.parse_url(url, verify_host=False) class GoodNotification(NotifyBase): def __init__(self, **kwargs): super(GoodNotification, self).__init__( notify_format=NotifyFormat.HTML, **kwargs) def url(self): # Support URL return '' def send(self, **kwargs): # Pretend everything is okay return True @staticmethod def parse_url(url, *args, **kwargs): # always parseable return NotifyBase.parse_url(url, verify_host=False) # Store our bad notification in our schema map SCHEMA_MAP['bad'] = BadNotification # Store our good notification in our schema map SCHEMA_MAP['good'] = GoodNotification # Just to explain what is happening here, we would have parsed the # url properly but failed when we went to go and create an instance # of it. assert a.add('bad://localhost') is False assert len(a) == 0 assert a.add('good://localhost') is True assert len(a) == 1 # Bad Notification Type is still allowed as it is presumed the user # know's what their doing assert a.notify( title="my title", body="my body", notify_type='bad') is True # No Title/Body combo's assert a.notify(title=None, body=None) is False assert a.notify(title='', body=None) is False assert a.notify(title=None, body='') is False # As long as one is present, we're good assert a.notify(title=None, body='present') is True assert a.notify(title='present', body=None) is True assert a.notify(title="present", body="present") is True # Send Attachment with success attach = join(TEST_VAR_DIR, 'apprise-test.gif') assert a.notify( body='body', title='test', notify_type=NotifyType.INFO, attach=attach) is True # Send the attachment as an AppriseAttachment object assert a.notify( body='body', title='test', notify_type=NotifyType.INFO, attach=AppriseAttachment(attach)) is True # test a invalid attachment assert a.notify( body='body', title='test', notify_type=NotifyType.INFO, attach='invalid://') is False # Repeat the same tests above... # however do it by directly accessing the object; this grants the similar # results: assert a[0].notify( body='body', title='test', notify_type=NotifyType.INFO, attach=attach) is True # Send the attachment as an AppriseAttachment object assert a[0].notify( body='body', title='test', notify_type=NotifyType.INFO, attach=AppriseAttachment(attach)) is True # test a invalid attachment assert a[0].notify( body='body', title='test', notify_type=NotifyType.INFO, attach='invalid://') is False # Clear our server listings again a.clear() class ThrowNotification(NotifyBase): def notify(self, **kwargs): # Pretend everything is okay raise TypeError() def url(self): # Support URL return '' class RuntimeNotification(NotifyBase): def notify(self, **kwargs): # Pretend everything is okay raise RuntimeError() def url(self): # Support URL return '' class FailNotification(NotifyBase): def notify(self, **kwargs): # Pretend everything is okay return False def url(self): # Support URL return '' # Store our bad notification in our schema map SCHEMA_MAP['throw'] = ThrowNotification # Store our good notification in our schema map SCHEMA_MAP['fail'] = FailNotification # Store our good notification in our schema map SCHEMA_MAP['runtime'] = RuntimeNotification assert a.add('runtime://localhost') is True assert a.add('throw://localhost') is True assert a.add('fail://localhost') is True assert len(a) == 3 # Test when our notify both throws an exception and or just # simply returns False assert a.notify(title="present", body="present") is False # Create a Notification that throws an unexected exception class ThrowInstantiateNotification(NotifyBase): def __init__(self, **kwargs): # Pretend everything is okay raise TypeError() def url(self): # Support URL return '' SCHEMA_MAP['throw'] = ThrowInstantiateNotification # Reset our object a.clear() assert len(a) == 0 # Test our socket details # rto = Socket Read Timeout # cto = Socket Connect Timeout plugin = a.instantiate('good://localhost?rto=5.1&cto=10') assert isinstance(plugin, NotifyBase) assert plugin.socket_connect_timeout == 10.0 assert plugin.socket_read_timeout == 5.1 plugin = a.instantiate('good://localhost?rto=invalid&cto=invalid') assert isinstance(plugin, NotifyBase) assert plugin.socket_connect_timeout == URLBase.socket_connect_timeout assert plugin.socket_read_timeout == URLBase.socket_read_timeout # Reset our object a.clear() assert len(a) == 0 # Instantiate a bad object plugin = a.instantiate(object, tag="bad_object") assert plugin is None # Instantiate a good object plugin = a.instantiate('good://localhost', tag="good") assert isinstance(plugin, NotifyBase) # Test simple tagging inside of the object assert "good" in plugin assert "bad" not in plugin # the in (__contains__ override) is based on or'ed content; so although # 'bad' isn't tagged as being in the plugin, 'good' is, so the return # value of this is True assert ["bad", "good"] in plugin assert set(["bad", "good"]) in plugin assert ("bad", "good") in plugin # We an add already substatiated instances into our Apprise object a.add(plugin) assert len(a) == 1 # We can add entries as a list too (to add more then one) a.add([plugin, plugin, plugin]) assert len(a) == 4 # Reset our object again a.clear() with pytest.raises(TypeError): a.instantiate('throw://localhost', suppress_exceptions=False) assert len(a) == 0 assert a.instantiate( 'throw://localhost', suppress_exceptions=True) is None assert len(a) == 0 # # We rince and repeat the same tests as above, however we do them # using the dict version # # Reset our object a.clear() assert len(a) == 0 # Instantiate a good object plugin = a.instantiate({ 'schema': 'good', 'host': 'localhost'}, tag="good") assert isinstance(plugin, NotifyBase) # Test simple tagging inside of the object assert "good" in plugin assert "bad" not in plugin # the in (__contains__ override) is based on or'ed content; so although # 'bad' isn't tagged as being in the plugin, 'good' is, so the return # value of this is True assert ["bad", "good"] in plugin assert set(["bad", "good"]) in plugin assert ("bad", "good") in plugin # We an add already substatiated instances into our Apprise object a.add(plugin) assert len(a) == 1 # We can add entries as a list too (to add more then one) a.add([plugin, plugin, plugin]) assert len(a) == 4 # Reset our object again a.clear() with pytest.raises(TypeError): a.instantiate({ 'schema': 'throw', 'host': 'localhost'}, suppress_exceptions=False) assert len(a) == 0 assert a.instantiate({ 'schema': 'throw', 'host': 'localhost'}, suppress_exceptions=True) is None assert len(a) == 0
def test_plugin_discord_general(mock_post): """ NotifyDiscord() General Checks """ # Disable Throttling to speed testing plugins.NotifyBase.request_rate_per_sec = 0 # Initialize some generic (but valid) tokens webhook_id = 'A' * 24 webhook_token = 'B' * 64 # Prepare Mock mock_post.return_value = requests.Request() mock_post.return_value.status_code = requests.codes.ok # Invalid webhook id with pytest.raises(TypeError): plugins.NotifyDiscord(webhook_id=None, webhook_token=webhook_token) # Invalid webhook id (whitespace) with pytest.raises(TypeError): plugins.NotifyDiscord(webhook_id=" ", webhook_token=webhook_token) # Invalid webhook token with pytest.raises(TypeError): plugins.NotifyDiscord(webhook_id=webhook_id, webhook_token=None) # Invalid webhook token (whitespace) with pytest.raises(TypeError): plugins.NotifyDiscord(webhook_id=webhook_id, webhook_token=" ") obj = plugins.NotifyDiscord(webhook_id=webhook_id, webhook_token=webhook_token, footer=True, thumbnail=False) # Test that we get a string response assert isinstance(obj.url(), six.string_types) is True # This call includes an image with it's payload: assert obj.notify(body='body', title='title', notify_type=NotifyType.INFO) is True # Simple Markdown Single line of text test_markdown = "body" desc, results = obj.extract_markdown_sections(test_markdown) assert isinstance(results, list) is True assert len(results) == 0 # Test our header parsing when not lead with a header test_markdown = """ A section of text that has no header at the top. It also has a hash tag # <- in the middle of a string. ## Heading 1 body # Heading 2 more content on multi-lines """ desc, results = obj.extract_markdown_sections(test_markdown) # we have a description assert isinstance(desc, six.string_types) is True assert desc.startswith('A section of text that has no header at the top.') assert desc.endswith('string.') assert isinstance(results, list) is True assert len(results) == 2 assert results[0]['name'] == 'Heading 1' assert results[0]['value'] == '```md\nbody\n```' assert results[1]['name'] == 'Heading 2' assert results[1]['value'] == \ '```md\nmore content\n on multi-lines\n```' # Test our header parsing test_markdown = "## Heading one\nbody body\n\n" + \ "# Heading 2 ##\n\nTest\n\n" + \ "more content\n" + \ "even more content \t\r\n\n\n" + \ "# Heading 3 ##\n\n\n" + \ "normal content\n" + \ "# heading 4\n" + \ "#### Heading 5" desc, results = obj.extract_markdown_sections(test_markdown) assert isinstance(results, list) is True # No desc details filled out assert isinstance(desc, six.string_types) is True assert not desc # We should have 5 sections (since there are 5 headers identified above) assert len(results) == 5 assert results[0]['name'] == 'Heading one' assert results[0]['value'] == '```md\nbody body\n```' assert results[1]['name'] == 'Heading 2' assert results[1]['value'] == \ '```md\nTest\n\nmore content\neven more content\n```' assert results[2]['name'] == 'Heading 3' assert results[2]['value'] == \ '```md\nnormal content\n```' assert results[3]['name'] == 'heading 4' assert results[3]['value'] == '```\n```' assert results[4]['name'] == 'Heading 5' assert results[4]['value'] == '```\n```' # Create an apprise instance a = Apprise() # Our processing is slightly different when we aren't using markdown # as we do not pre-parse content during our notifications assert a.add( 'discord://{webhook_id}/{webhook_token}/' '?format=markdown&footer=Yes'.format( webhook_id=webhook_id, webhook_token=webhook_token)) is True # This call includes an image with it's payload: plugins.NotifyDiscord.discord_max_fields = 1 assert a.notify(body=test_markdown, title='title', notify_type=NotifyType.INFO, body_format=NotifyFormat.TEXT) is True # Throw an exception on the forth call to requests.post() # This allows to test our batch field processing response = mock.Mock() response.content = '' response.status_code = requests.codes.ok mock_post.return_value = response mock_post.side_effect = [ response, response, response, requests.RequestException() ] # Test our markdown obj = Apprise.instantiate('discord://{}/{}/?format=markdown'.format( webhook_id, webhook_token)) assert isinstance(obj, plugins.NotifyDiscord) assert obj.notify(body=test_markdown, title='title', notify_type=NotifyType.INFO) is False mock_post.side_effect = None # Empty String desc, results = obj.extract_markdown_sections("") assert isinstance(results, list) is True assert len(results) == 0 # No desc details filled out assert isinstance(desc, six.string_types) is True assert not desc # String without Heading test_markdown = "Just a string without any header entries.\n" + \ "A second line" desc, results = obj.extract_markdown_sections(test_markdown) assert isinstance(results, list) is True assert len(results) == 0 # No desc details filled out assert isinstance(desc, six.string_types) is True assert desc == 'Just a string without any header entries.\n' + \ 'A second line' # Use our test markdown string during a notification assert obj.notify( body=test_markdown, title='title', notify_type=NotifyType.INFO) is True # Create an apprise instance a = Apprise() # Our processing is slightly different when we aren't using markdown # as we do not pre-parse content during our notifications assert a.add( 'discord://{webhook_id}/{webhook_token}/' '?format=markdown&footer=Yes'.format( webhook_id=webhook_id, webhook_token=webhook_token)) is True # This call includes an image with it's payload: assert a.notify(body=test_markdown, title='title', notify_type=NotifyType.INFO, body_format=NotifyFormat.TEXT) is True assert a.notify(body=test_markdown, title='title', notify_type=NotifyType.INFO, body_format=NotifyFormat.MARKDOWN) is True # Toggle our logo availability a.asset.image_url_logo = None assert a.notify(body='body', title='title', notify_type=NotifyType.INFO) is True
def test_plugin_signal_test_based_on_feedback(mock_post): """ NotifySignalAPI() User Feedback Test """ # Disable Throttling to speed testing plugins.NotifyBase.request_rate_per_sec = 0 # Prepare our response response = requests.Request() response.status_code = requests.codes.ok # Prepare Mock mock_post.return_value = response body = "test body" title = "My Title" aobj = Apprise() aobj.add( 'signal://10.0.0.112:8080/+12512222222/+12513333333/' '12514444444?batch=yes') assert aobj.notify(title=title, body=body) # If a batch, there is only 1 post assert mock_post.call_count == 1 details = mock_post.call_args_list[0] assert details[0][0] == 'http://10.0.0.112:8080/v2/send' payload = loads(details[1]['data']) assert payload['message'] == 'My Title\r\ntest body' assert payload['number'] == "+12512222222" assert len(payload['recipients']) == 2 assert "+12513333333" in payload['recipients'] # The + is appended assert "+12514444444" in payload['recipients'] # Reset our test and turn batch mode off mock_post.reset_mock() aobj = Apprise() aobj.add( 'signal://10.0.0.112:8080/+12512222222/+12513333333/' '12514444444?batch=no') assert aobj.notify(title=title, body=body) # If a batch, there is only 1 post assert mock_post.call_count == 2 details = mock_post.call_args_list[0] assert details[0][0] == 'http://10.0.0.112:8080/v2/send' payload = loads(details[1]['data']) assert payload['message'] == 'My Title\r\ntest body' assert payload['number'] == "+12512222222" assert len(payload['recipients']) == 1 assert "+12513333333" in payload['recipients'] details = mock_post.call_args_list[1] assert details[0][0] == 'http://10.0.0.112:8080/v2/send' payload = loads(details[1]['data']) assert payload['message'] == 'My Title\r\ntest body' assert payload['number'] == "+12512222222" assert len(payload['recipients']) == 1 # The + is appended assert "+12514444444" in payload['recipients'] mock_post.reset_mock() # Test group names aobj = Apprise() aobj.add( 'signal://10.0.0.112:8080/+12513333333/@group1/@group2/' '12514444444?batch=yes') assert aobj.notify(title=title, body=body) # If a batch, there is only 1 post assert mock_post.call_count == 1 details = mock_post.call_args_list[0] assert details[0][0] == 'http://10.0.0.112:8080/v2/send' payload = loads(details[1]['data']) assert payload['message'] == 'My Title\r\ntest body' assert payload['number'] == "+12513333333" assert len(payload['recipients']) == 3 assert "+12514444444" in payload['recipients'] # our groups assert "group.group1" in payload['recipients'] assert "group.group2" in payload['recipients'] # Groups are stored properly assert '/@group1' in aobj[0].url() assert '/@group2' in aobj[0].url() # Our target phone number is also in the path assert '/+12514444444' in aobj[0].url()
def test_apprise_notify_formats(tmpdir): """ API: Apprise() TextFormat tests """ # Caling load matix a second time which is an internal function causes it # to skip over content already loaded into our matrix and thefore accesses # other if/else parts of the code that aren't otherwise called __load_matrix() a = Apprise() # no items assert (len(a) == 0) class TextNotification(NotifyBase): # set our default notification format notify_format = NotifyFormat.TEXT def __init__(self, **kwargs): super(TextNotification, self).__init__() def notify(self, **kwargs): # Pretend everything is okay return True def url(self): # Support URL return '' class HtmlNotification(NotifyBase): # set our default notification format notify_format = NotifyFormat.HTML def __init__(self, **kwargs): super(HtmlNotification, self).__init__() def notify(self, **kwargs): # Pretend everything is okay return True def url(self): # Support URL return '' class MarkDownNotification(NotifyBase): # set our default notification format notify_format = NotifyFormat.MARKDOWN def __init__(self, **kwargs): super(MarkDownNotification, self).__init__() def notify(self, **kwargs): # Pretend everything is okay return True def url(self): # Support URL return '' # Store our notifications into our schema map SCHEMA_MAP['text'] = TextNotification SCHEMA_MAP['html'] = HtmlNotification SCHEMA_MAP['markdown'] = MarkDownNotification # Test Markdown; the above calls the markdown because our good:// # defined plugin above was defined to default to HTML which triggers # a markdown to take place if the body_format specified on the notify # call assert (a.add('html://localhost') is True) assert (a.add('html://another.server') is True) assert (a.add('html://and.another') is True) assert (a.add('text://localhost') is True) assert (a.add('text://another.server') is True) assert (a.add('text://and.another') is True) assert (a.add('markdown://localhost') is True) assert (a.add('markdown://another.server') is True) assert (a.add('markdown://and.another') is True) assert (len(a) == 9) assert (a.notify(title="markdown", body="## Testing Markdown", body_format=NotifyFormat.MARKDOWN) is True) assert (a.notify(title="text", body="Testing Text", body_format=NotifyFormat.TEXT) is True) assert (a.notify(title="html", body="<b>HTML</b>", body_format=NotifyFormat.HTML) is True)
def notify(self, servers, body, title, notify_type=NotifyType.INFO, body_format=NotifyFormat.MARKDOWN): """ processes list of servers specified """ # Apprise Asset Object asset = AppriseAsset(theme=self.default_theme) asset.app_id = 'NZB-Notify' asset.app_desc = 'NZB Notification' asset.app_url = 'https://github.com/caronc/nzb-notify' # Source Theme from GitHub Page asset.image_url_mask = 'https://raw.githubusercontent.com' \ '/caronc/nzb-notify/master/Notify' \ '/apprise-theme/{THEME}/apprise-{TYPE}-{XY}.png' asset.image_path_mask = join( dirname(__file__), 'Notify', 'apprise-theme', '{THEME}', 'apprise-{TYPE}-{XY}.png') # Include Image Flag _url = self.parse_url(self.get('IncludeImage')) # Define some globals to use in this function image_path = None image_url = None if _url: # Toggle our include image flag right away to True include_image = True # Get some more details if not re.match('^(https?|file)$', _url['schema'] ,re.IGNORECASE): self.logger.error( 'An invalid image url protocol (%s://) was specified.' % \ _url['schema'], ) return False if _url['schema'] == 'file': if not isile(_url['fullpath']): self.logger.error( 'The specified file %s was not found.' % \ _url['fullpath'], ) return False image_path = _url['fullpath'] else: # We're dealing with a web request image_url = _url['url'] else: # Dealing with the old way of doing things; just toggling a true/false # flag include_image = self.parse_bool(self.get('IncludeImage'), False) if isinstance(servers, basestring): # servers can be a list of URLs, or it can be # a string which will be parsed into this list # we wanted. servers = self.parse_list(self.get('Servers', '')) # Create our apprise object a = Apprise(asset=asset) for server in servers: # Add our URL if not a.add(server): # Validation Failure self.logger.error( 'Could not initialize %s instance.' % server, ) continue # Notify our servers a.notify(body=body, title=title, notify_type=notify_type, body_format=body_format) # Always return true return True
def test_discord_plugin(mock_post): """ API: NotifyDiscord() General Checks """ # Disable Throttling to speed testing plugins.NotifyBase.request_rate_per_sec = 0 # Initialize some generic (but valid) tokens webhook_id = 'A' * 24 webhook_token = 'B' * 64 # Prepare Mock mock_post.return_value = requests.Request() mock_post.return_value.status_code = requests.codes.ok # Invalid webhook id with pytest.raises(TypeError): plugins.NotifyDiscord(webhook_id=None, webhook_token=webhook_token) # Invalid webhook id (whitespace) with pytest.raises(TypeError): plugins.NotifyDiscord(webhook_id=" ", webhook_token=webhook_token) # Invalid webhook token with pytest.raises(TypeError): plugins.NotifyDiscord(webhook_id=webhook_id, webhook_token=None) # Invalid webhook token (whitespace) with pytest.raises(TypeError): plugins.NotifyDiscord(webhook_id=webhook_id, webhook_token=" ") obj = plugins.NotifyDiscord(webhook_id=webhook_id, webhook_token=webhook_token, footer=True, thumbnail=False) # Test that we get a string response assert isinstance(obj.url(), six.string_types) is True # This call includes an image with it's payload: assert obj.notify(body='body', title='title', notify_type=NotifyType.INFO) is True # Test our header parsing test_markdown = "## Heading one\nbody body\n\n" + \ "# Heading 2 ##\n\nTest\n\n" + \ "more content\n" + \ "even more content \t\r\n\n\n" + \ "# Heading 3 ##\n\n\n" + \ "normal content\n" + \ "# heading 4\n" + \ "#### Heading 5" results = obj.extract_markdown_sections(test_markdown) assert isinstance(results, list) is True # We should have 5 sections (since there are 5 headers identified above) assert len(results) == 5 assert results[0]['name'] == 'Heading one' assert results[0]['value'] == '```md\nbody body\n```' assert results[1]['name'] == 'Heading 2' assert results[1]['value'] == \ '```md\nTest\n\nmore content\neven more content\n```' assert results[2]['name'] == 'Heading 3' assert results[2]['value'] == \ '```md\nnormal content\n```' assert results[3]['name'] == 'heading 4' assert results[3]['value'] == '```md\n\n```' assert results[4]['name'] == 'Heading 5' assert results[4]['value'] == '```md\n\n```' # Test our markdown obj = Apprise.instantiate('discord://{}/{}/?format=markdown'.format( webhook_id, webhook_token)) assert isinstance(obj, plugins.NotifyDiscord) assert obj.notify( body=test_markdown, title='title', notify_type=NotifyType.INFO) is True # Empty String results = obj.extract_markdown_sections("") assert isinstance(results, list) is True assert len(results) == 0 # String without Heading test_markdown = "Just a string without any header entries.\n" + \ "A second line" results = obj.extract_markdown_sections(test_markdown) assert isinstance(results, list) is True assert len(results) == 0 # Use our test markdown string during a notification assert obj.notify( body=test_markdown, title='title', notify_type=NotifyType.INFO) is True # Create an apprise instance a = Apprise() # Our processing is slightly different when we aren't using markdown # as we do not pre-parse content during our notifications assert a.add( 'discord://{webhook_id}/{webhook_token}/' '?format=markdown&footer=Yes'.format( webhook_id=webhook_id, webhook_token=webhook_token)) is True # This call includes an image with it's payload: assert a.notify(body=test_markdown, title='title', notify_type=NotifyType.INFO, body_format=NotifyFormat.TEXT) is True assert a.notify(body=test_markdown, title='title', notify_type=NotifyType.INFO, body_format=NotifyFormat.MARKDOWN) is True # Toggle our logo availability a.asset.image_url_logo = None assert a.notify(body='body', title='title', notify_type=NotifyType.INFO) is True
def test_plugin_telegram_formating_py2(mock_post): """ NotifyTelegram() Python v2 Formatting Tests """ # Disable Throttling to speed testing plugins.NotifyTelegram.request_rate_per_sec = 0 # Prepare Mock mock_post.return_value = requests.Request() mock_post.return_value.status_code = requests.codes.ok mock_post.return_value.content = '{}' # Simple success response mock_post.return_value.content = dumps({ "ok": True, "result": [ { "update_id": 645421321, "message": { "message_id": 2, "from": { "id": 532389719, "is_bot": False, "first_name": "Chris", "language_code": "en-US" }, "chat": { "id": 532389719, "first_name": "Chris", "type": "private" }, "date": 1519694394, "text": "/start", "entities": [{ "offset": 0, "length": 6, "type": "bot_command", }], } }, ], }) mock_post.return_value.status_code = requests.codes.ok results = plugins.NotifyTelegram.parse_url( 'tgram://123456789:abcdefg_hijklmnop/') instance = plugins.NotifyTelegram(**results) assert isinstance(instance, plugins.NotifyTelegram) response = instance.send(title='title', body='body') assert response is True # 1 call to look up bot owner, and second for notification assert mock_post.call_count == 2 assert mock_post.call_args_list[0][0][0] == \ 'https://api.telegram.org/bot123456789:abcdefg_hijklmnop/getUpdates' assert mock_post.call_args_list[1][0][0] == \ 'https://api.telegram.org/bot123456789:abcdefg_hijklmnop/sendMessage' # Reset our values mock_post.reset_mock() # Now test our HTML Conversion as TEXT) aobj = Apprise() aobj.add('tgram://123456789:abcdefg_hijklmnop/') assert len(aobj) == 1 title = '馃毃 Change detected for <i>Apprise Test Title</i>' body = '<a href="http://localhost"><i>Apprise Body Title</i></a>' \ ' had <a href="http://127.0.0.1">a change</a>' assert aobj.notify(title=title, body=body, body_format=NotifyFormat.TEXT) # Test our calls assert mock_post.call_count == 2 assert mock_post.call_args_list[0][0][0] == \ 'https://api.telegram.org/bot123456789:abcdefg_hijklmnop/getUpdates' assert mock_post.call_args_list[1][0][0] == \ 'https://api.telegram.org/bot123456789:abcdefg_hijklmnop/sendMessage' payload = loads(mock_post.call_args_list[1][1]['data']) # Test that everything is escaped properly in a TEXT mode assert payload['text'].encode('utf-8') == \ '<b>\xf0\x9f\x9a\xa8 Change detected for <i>' \ 'Apprise Test Title</i></b>\r\n<' \ 'a href="http://localhost"><i>Apprise Body Title' \ '</i></a> had <a href="http://127.0.0.1"' \ '>a change</a>' # Reset our values mock_post.reset_mock() # Now test our HTML Conversion as TEXT) aobj = Apprise() aobj.add('tgram://123456789:abcdefg_hijklmnop/?format=html') assert len(aobj) == 1 assert aobj.notify(title=title, body=body, body_format=NotifyFormat.HTML) # Test our calls assert mock_post.call_count == 2 assert mock_post.call_args_list[0][0][0] == \ 'https://api.telegram.org/bot123456789:abcdefg_hijklmnop/getUpdates' assert mock_post.call_args_list[1][0][0] == \ 'https://api.telegram.org/bot123456789:abcdefg_hijklmnop/sendMessage' payload = loads(mock_post.call_args_list[1][1]['data']) # Test that everything is escaped properly in a HTML mode assert payload['text'].encode('utf-8') == \ '<b>\xf0\x9f\x9a\xa8 Change detected for <i>Apprise Test Title</i>' \ '</b>\r\n<a href="http://localhost"><i>Apprise Body Title</i></a> ' \ 'had <a href="http://127.0.0.1">a change</a>' # Reset our values mock_post.reset_mock() # Now test our MARKDOWN Handling title = '# 馃毃 Change detected for _Apprise Test Title_' body = '_[Apprise Body Title](http://localhost)_' \ ' had [a change](http://127.0.0.1)' aobj = Apprise() aobj.add('tgram://123456789:abcdefg_hijklmnop/?format=markdown') assert len(aobj) == 1 assert aobj.notify(title=title, body=body, body_format=NotifyFormat.MARKDOWN) # Test our calls assert mock_post.call_count == 2 assert mock_post.call_args_list[0][0][0] == \ 'https://api.telegram.org/bot123456789:abcdefg_hijklmnop/getUpdates' assert mock_post.call_args_list[1][0][0] == \ 'https://api.telegram.org/bot123456789:abcdefg_hijklmnop/sendMessage' payload = loads(mock_post.call_args_list[1][1]['data']) # Test that everything is escaped properly in a HTML mode assert payload['text'].encode('utf-8') == \ '# \xf0\x9f\x9a\xa8 Change detected for _Apprise Test Title_\r\n_' \ '[Apprise Body Title](http://localhost)_ had ' \ '[a change](http://127.0.0.1)' # Reset our values mock_post.reset_mock() # Upstream to use HTML but input specified as Markdown aobj = Apprise() aobj.add('tgram://987654321:abcdefg_hijklmnop/?format=html') assert len(aobj) == 1 # HTML forced by the command line, but MARKDOWN specified as # upstream mode assert aobj.notify(title=title, body=body, body_format=NotifyFormat.MARKDOWN) # Test our calls assert mock_post.call_count == 2 assert mock_post.call_args_list[0][0][0] == \ 'https://api.telegram.org/bot987654321:abcdefg_hijklmnop/getUpdates' assert mock_post.call_args_list[1][0][0] == \ 'https://api.telegram.org/bot987654321:abcdefg_hijklmnop/sendMessage' payload = loads(mock_post.call_args_list[1][1]['data']) # Test that everything is escaped properly in a HTML mode assert payload['text'].encode('utf-8') == \ '<b>\r\n<b>\xf0\x9f\x9a\xa8 Change detected for ' \ '<i>Apprise Test Title</i></b>\r\n</b>\r\n<i>' \ '<a href="http://localhost">Apprise Body Title</a>' \ '</i> had <a href="http://127.0.0.1">a change</a>\r\n' # Reset our values mock_post.reset_mock() # Now test hebrew types (outside of default utf-8) # 讻讜转专转 谞驻诇讗讛 translates to 'A wonderful title' # 讝讜 讛讜讚注讛 translates to 'This is a notification' title = '讻讜转专转 谞驻诇讗讛' \ .decode('utf-8').encode('ISO-8859-8') body = '[_[讝讜 讛讜讚注讛](http://localhost)_' \ .decode('utf-8').encode('ISO-8859-8') asset = AppriseAsset(encoding='utf-8') # Now test default entries aobj = Apprise(asset=asset) aobj.add('tgram://123456789:abcdefg_hijklmnop/') assert len(aobj) == 1 # Our notification will fail because we'll have an encoding error assert not aobj.notify(title=title, body=body) # Nothing was even attempted to be notified assert mock_post.call_count == 0 # Let's use the expected input asset = AppriseAsset(encoding='ISO-8859-8') # Now test default entries aobj = Apprise(asset=asset) aobj.add('tgram://123456789:abcdefg_hijklmnop/') assert len(aobj) == 1 # Our notification will work now assert aobj.notify(title=title, body=body) # Test our calls assert mock_post.call_count == 2 assert mock_post.call_args_list[0][0][0] == \ 'https://api.telegram.org/bot123456789:abcdefg_hijklmnop/getUpdates' assert mock_post.call_args_list[1][0][0] == \ 'https://api.telegram.org/bot123456789:abcdefg_hijklmnop/sendMessage' payload = loads(mock_post.call_args_list[1][1]['data']) # Test that everything is escaped properly in a HTML mode assert payload['text'].encode('utf-8') == \ '<b>\xd7\x9b\xd7\x95\xd7\xaa\xd7\xa8\xd7\xaa '\ '\xd7\xa0\xd7\xa4\xd7\x9c\xd7\x90\xd7\x94</b>\r\n[_[\xd7\x96\xd7\x95 '\ '\xd7\x94\xd7\x95\xd7\x93\xd7\xa2\xd7\x94](http://localhost)_' # Now we'll test an edge case where a title was defined, but after # processing it, it was determiend there really wasn't anything there # at all at the end of the day. # Reset our values mock_post.reset_mock() # Upstream to use HTML but input specified as Markdown aobj = Apprise() aobj.add('tgram://987654321:abcdefg_hijklmnop/?format=markdown') assert len(aobj) == 1 # Now test our MARKDOWN Handling (no title defined... not really anyway) title = '# ' body = '_[Apprise Body Title](http://localhost)_' \ ' had [a change](http://127.0.0.2)' # MARKDOWN forced by the command line, but TEXT specified as # upstream mode assert aobj.notify(title=title, body=body, body_format=NotifyFormat.TEXT) # Test our calls assert mock_post.call_count == 2 assert mock_post.call_args_list[0][0][0] == \ 'https://api.telegram.org/bot987654321:abcdefg_hijklmnop/getUpdates' assert mock_post.call_args_list[1][0][0] == \ 'https://api.telegram.org/bot987654321:abcdefg_hijklmnop/sendMessage' payload = loads(mock_post.call_args_list[1][1]['data']) # Test that everything is escaped properly in a HTML mode assert payload['text'] == \ '_[Apprise Body Title](http://localhost)_ had ' \ '[a change](http://127.0.0.2)' # Reset our values mock_post.reset_mock() # Upstream to use HTML but input specified as Markdown aobj = Apprise() aobj.add('tgram://987654321:abcdefg_hijklmnop/?format=markdown') assert len(aobj) == 1 # Set an actual title this time title = '# A Great Title' body = '_[Apprise Body Title](http://localhost)_' \ ' had [a change](http://127.0.0.2)' # MARKDOWN forced by the command line, but TEXT specified as # upstream mode assert aobj.notify(title=title, body=body, body_format=NotifyFormat.TEXT) # Test our calls assert mock_post.call_count == 2 assert mock_post.call_args_list[0][0][0] == \ 'https://api.telegram.org/bot987654321:abcdefg_hijklmnop/getUpdates' assert mock_post.call_args_list[1][0][0] == \ 'https://api.telegram.org/bot987654321:abcdefg_hijklmnop/sendMessage' payload = loads(mock_post.call_args_list[1][1]['data']) # Test that everything is escaped properly in a HTML mode assert payload['text'] == \ '# A Great Title\r\n_[Apprise Body Title](http://localhost)_ had ' \ '[a change](http://127.0.0.2)'
def test_plugin_telegram_html_formatting(mock_post): """ NotifyTelegram() HTML Formatting """ # on't send anything other than <b>, <i>, <a>,<code> and <pre> # Disable Throttling to speed testing plugins.NotifyTelegram.request_rate_per_sec = 0 # Prepare Mock mock_post.return_value = requests.Request() mock_post.return_value.status_code = requests.codes.ok mock_post.return_value.content = '{}' # Simple success response mock_post.return_value.content = dumps({ "ok": True, "result": [ { "update_id": 645421321, "message": { "message_id": 2, "from": { "id": 532389719, "is_bot": False, "first_name": "Chris", "language_code": "en-US" }, "chat": { "id": 532389719, "first_name": "Chris", "type": "private" }, "date": 1519694394, "text": "/start", "entities": [{ "offset": 0, "length": 6, "type": "bot_command", }], } }, ], }) mock_post.return_value.status_code = requests.codes.ok aobj = Apprise() aobj.add('tgram://123456789:abcdefg_hijklmnop/') assert len(aobj) == 1 assert isinstance(aobj[0], plugins.NotifyTelegram) # Test our HTML Conversion title = '<title>'information&apos</title>' body = '<em>"This is in Italic"</em><br/>' \ '<h5> &emspHeadings are dropped and' \ ' converted to bold</h5>' assert aobj.notify(title=title, body=body, body_format=NotifyFormat.HTML) # 1 call to look up bot owner, and second for notification assert mock_post.call_count == 2 payload = loads(mock_post.call_args_list[1][1]['data']) # Test that everything is escaped properly in a HTML mode assert payload['text'] == \ '<b>\r\n<b>\'information\'</b>\r\n</b>\r\n<i>"This is in Italic"' \ '</i>\r\n<b> Headings are dropped and converted to bold</b>\r\n' mock_post.reset_mock() assert aobj.notify(title=title, body=body, body_format=NotifyFormat.TEXT) # owner has already been looked up, so only one call is made assert mock_post.call_count == 1 payload = loads(mock_post.call_args_list[0][1]['data']) assert payload['text'] == \ '<b><title>&apos;information&apos</title></b>' \ '\r\n<em>&quot;This is in Italic&quot</em><' \ 'br/><h5>&emsp;&emspHeadings&nbsp;are ' \ 'dropped and&nbspconverted to bold</h5>' # Lest test more complex HTML examples now mock_post.reset_mock() test_file_01 = os.path.join(TEST_VAR_DIR, '01_test_example.html') with open(test_file_01) as html_file: assert aobj.notify(body=html_file.read(), body_format=NotifyFormat.HTML) # owner has already been looked up, so only one call is made assert mock_post.call_count == 1 payload = loads(mock_post.call_args_list[0][1]['data']) assert payload['text'] == \ '\r\n<b>Bootstrap 101 Template</b>\r\n<b>My Title</b>\r\n' \ '<b>Heading 1</b>\r\n-Bullet 1\r\n-Bullet 2\r\n-Bullet 3\r\n' \ '-Bullet 1\r\n-Bullet 2\r\n-Bullet 3\r\n<b>Heading 2</b>\r\n' \ 'A div entry\r\nA div entry\r\n<code>A pre entry</code>\r\n' \ '<b>Heading 3</b>\r\n<b>Heading 4</b>\r\n<b>Heading 5</b>\r\n' \ '<b>Heading 6</b>\r\nA set of text\r\n' \ 'Another line after the set of text\r\nMore text\r\nlabel'
def test_apprise_config_with_apprise_obj(tmpdir): """ API: ConfigBase - parse valid config """ # temporary file to work with t = tmpdir.mkdir("apprise-obj").join("apprise") buf = """ good://hostname localhost=good://localhost """ t.write(buf) # Define our good:// url class GoodNotification(NotifyBase): def __init__(self, **kwargs): super(GoodNotification, self).__init__( notify_format=NotifyFormat.HTML, **kwargs) def notify(self, **kwargs): # Pretend everything is okay return True def url(self, **kwargs): # support url() return '' # Store our good notification in our schema map NOTIFY_SCHEMA_MAP['good'] = GoodNotification # Create ourselves a config object ac = AppriseConfig(cache=False) # Nothing loaded yet assert len(ac) == 0 # Add an item associated with tag a assert ac.add(configs=str(t), asset=AppriseAsset(), tag='a') is True # One configuration file assert len(ac) == 1 # 2 services found in it assert len(ac.servers()) == 2 # Pop one of them (at index 0) ac.server_pop(0) # Verify that it no longer listed assert len(ac.servers()) == 1 # Test our ability to add Config objects to our apprise object a = Apprise() # Add our configuration object assert a.add(servers=ac) is True # Detect our 1 entry (originally there were 2 but we deleted one) assert len(a) == 1 # Notify our service assert a.notify(body='apprise configuration power!') is True # Add our configuration object assert a.add( servers=[AppriseConfig(str(t)), AppriseConfig(str(t))]) is True # Detect our 5 loaded entries now; 1 from first config, and another # 2x2 based on adding our list above assert len(a) == 5 # We can't add garbage assert a.add(servers=object()) is False assert a.add(servers=[object(), object()]) is False # Our length is unchanged assert len(a) == 5 # reference index 0 of our list ref = a[0] assert isinstance(ref, NotifyBase) is True # Our length is unchanged assert len(a) == 5 # pop the index ref_popped = a.pop(0) # Verify our response assert isinstance(ref_popped, NotifyBase) is True # Our length drops by 1 assert len(a) == 4 # Content popped is the same as one referenced by index # earlier assert ref == ref_popped # pop an index out of range try: a.pop(len(a)) # We'll thrown an IndexError and not make it this far assert False except IndexError: # As expected assert True # Our length remains unchanged assert len(a) == 4 # Reference content out of range try: a[len(a)] # We'll thrown an IndexError and not make it this far assert False except IndexError: # As expected assert True # reference index at the end of our list ref = a[len(a) - 1] # Verify our response assert isinstance(ref, NotifyBase) is True # Our length stays the same assert len(a) == 4 # We can pop from the back of the list without a problem too ref_popped = a.pop(len(a) - 1) # Verify our response assert isinstance(ref_popped, NotifyBase) is True # Content popped is the same as one referenced by index # earlier assert ref == ref_popped # Our length drops by 1 assert len(a) == 3 # Now we'll test adding another element to the list so that it mixes up # our response object. # Below we add 3 different types, a ConfigBase, NotifyBase, and URL assert a.add( servers=[ ConfigFile(path=(str(t))), 'good://another.host', GoodNotification(**{'host': 'nuxref.com'})]) is True # Our length increases by 4 (2 entries in the config file, + 2 others) assert len(a) == 7 # reference index at the end of our list ref = a[len(a) - 1] # Verify our response assert isinstance(ref, NotifyBase) is True # We can pop from the back of the list without a problem too ref_popped = a.pop(len(a) - 1) # Verify our response assert isinstance(ref_popped, NotifyBase) is True # Content popped is the same as one referenced by index # earlier assert ref == ref_popped # Our length drops by 1 assert len(a) == 6 # pop our list while len(a) > 0: assert isinstance(a.pop(len(a) - 1), NotifyBase) is True
def test_aws_topic_handling(mock_post): """ API: NotifySNS Plugin() AWS Topic Handling """ # Disable Throttling to speed testing plugins.NotifySNS.request_rate_per_sec = 0 arn_response = \ """ <CreateTopicResponse xmlns="http://sns.amazonaws.com/doc/2010-03-31/"> <CreateTopicResult> <TopicArn>arn:aws:sns:us-east-1:000000000000:abcd</TopicArn> </CreateTopicResult> <ResponseMetadata> <RequestId>604bef0f-369c-50c5-a7a4-bbd474c83d6a</RequestId> </ResponseMetadata> </CreateTopicResponse> """ def post(url, data, **kwargs): """ Since Publishing a token requires 2 posts, we need to return our response depending on what step we're on """ # A request robj = mock.Mock() robj.text = '' robj.status_code = requests.codes.ok if data.find('=CreateTopic') >= 0: # Topic Post Failure robj.status_code = requests.codes.bad_request return robj # Assign ourselves a new function mock_post.side_effect = post # Create our object a = Apprise() a.add([ # Single Topic 'sns://T1JJ3T3L2/A1BRTD4JD/TIiajkdnl/us-west-2/TopicA', # Multi-Topic 'sns://T1JJ3T3L2/A1BRTD4JD/TIiajkdnl/us-east-1/TopicA/TopicB/' # Topic-Mix 'sns://T1JJ3T3L2/A1BRTD4JD/TIiajkdnlazkce/us-west-2/' \ '12223334444/TopicA']) # CreateTopic fails assert(a.notify(title='', body='test') is False) def post(url, data, **kwargs): """ Since Publishing a token requires 2 posts, we need to return our response depending on what step we're on """ # A request robj = mock.Mock() robj.text = '' robj.status_code = requests.codes.ok if data.find('=CreateTopic') >= 0: robj.text = arn_response # Manipulate Topic Publishing only (not phone) elif data.find('=Publish') >= 0 and data.find('TopicArn=') >= 0: # Topic Post Failure robj.status_code = requests.codes.bad_request return robj # Assign ourselves a new function mock_post.side_effect = post # Publish fails assert(a.notify(title='', body='test') is False) # Disable our side effect mock_post.side_effect = None # Handle case where TopicArn is missing: robj = mock.Mock() robj.text = "<CreateTopicResponse></CreateTopicResponse>" robj.status_code = requests.codes.ok # Assign ourselves a new function mock_post.return_value = robj assert(a.notify(title='', body='test') is False) # Handle case where we fails get a bad response robj = mock.Mock() robj.text = '' robj.status_code = requests.codes.bad_request mock_post.return_value = robj assert(a.notify(title='', body='test') is False) # Handle case where we get a valid response and TopicARN robj = mock.Mock() robj.text = arn_response robj.status_code = requests.codes.ok mock_post.return_value = robj # We would have failed to make Post assert(a.notify(title='', body='test') is True)
def test_apprise(): """ API: Apprise() object """ # Caling load matix a second time which is an internal function causes it # to skip over content already loaded into our matrix and thefore accesses # other if/else parts of the code that aren't otherwise called __load_matrix() a = Apprise() # no items assert (len(a) == 0) # Create an Asset object asset = AppriseAsset(theme='default') # We can load the device using our asset a = Apprise(asset=asset) # We can load our servers up front as well servers = [ 'faast://abcdefghijklmnop-abcdefg', 'kodi://kodi.server.local', 'palot://1f418df7577e32b89ac6511f2eb9aa68', ] a = Apprise(servers=servers) # 3 servers loaded assert (len(a) == 3) # We can add another server assert (a.add('mmosts://mattermost.server.local/' '3ccdd113474722377935511fc85d3dd4') is True) assert (len(a) == 4) # We can empty our set a.clear() assert (len(a) == 0) # An invalid schema assert (a.add('this is not a parseable url at all') is False) assert (len(a) == 0) # An unsupported schema assert (a.add('invalid://we.just.do.not.support.this.plugin.type') is False) assert (len(a) == 0) # A poorly formatted URL assert (a.add('json://user:@@@:bad?no.good') is False) assert (len(a) == 0) # Add a server with our asset we created earlier assert (a.add( 'mmosts://mattermost.server.local/' '3ccdd113474722377935511fc85d3dd4', asset=asset) is True) # Clear our server listings again a.clear() # No servers to notify assert (a.notify(title="my title", body="my body") is False) class BadNotification(NotifyBase): def __init__(self, **kwargs): super(BadNotification, self).__init__() # We fail whenever we're initialized raise TypeError() class GoodNotification(NotifyBase): def __init__(self, **kwargs): super(GoodNotification, self).__init__(notify_format=NotifyFormat.HTML) def notify(self, **kwargs): # Pretend everything is okay return True # Store our bad notification in our schema map SCHEMA_MAP['bad'] = BadNotification # Store our good notification in our schema map SCHEMA_MAP['good'] = GoodNotification # Just to explain what is happening here, we would have parsed the # url properly but failed when we went to go and create an instance # of it. assert (a.add('bad://localhost') is False) assert (len(a) == 0) assert (a.add('good://localhost') is True) assert (len(a) == 1) # Bad Notification Type is still allowed as it is presumed the user # know's what their doing assert (a.notify(title="my title", body="my body", notify_type='bad') is True) # No Title/Body combo's assert (a.notify(title=None, body=None) is False) assert (a.notify(title='', body=None) is False) assert (a.notify(title=None, body='') is False) # As long as one is present, we're good assert (a.notify(title=None, body='present') is True) assert (a.notify(title='present', body=None) is True) assert (a.notify(title="present", body="present") is True) # Clear our server listings again a.clear() class ThrowNotification(NotifyBase): def notify(self, **kwargs): # Pretend everything is okay raise TypeError() class RuntimeNotification(NotifyBase): def notify(self, **kwargs): # Pretend everything is okay raise RuntimeError() class FailNotification(NotifyBase): def notify(self, **kwargs): # Pretend everything is okay return False # Store our bad notification in our schema map SCHEMA_MAP['throw'] = ThrowNotification # Store our good notification in our schema map SCHEMA_MAP['fail'] = FailNotification # Store our good notification in our schema map SCHEMA_MAP['runtime'] = RuntimeNotification assert (a.add('runtime://localhost') is True) assert (a.add('throw://localhost') is True) assert (a.add('fail://localhost') is True) assert (len(a) == 3) # Test when our notify both throws an exception and or just # simply returns False assert (a.notify(title="present", body="present") is False) # Create a Notification that throws an unexected exception class ThrowInstantiateNotification(NotifyBase): def __init__(self, **kwargs): # Pretend everything is okay raise TypeError() SCHEMA_MAP['throw'] = ThrowInstantiateNotification # Reset our object a.clear() assert (len(a) == 0) # Instantiate a good object plugin = a.instantiate('good://localhost') assert (isinstance(plugin, NotifyBase)) # We an add already substatiated instances into our Apprise object a.add(plugin) assert (len(a) == 1) # Reset our object again a.clear() try: a.instantiate('throw://localhost', suppress_exceptions=False) assert (False) except TypeError: assert (True) assert (len(a) == 0) assert (a.instantiate('throw://localhost', suppress_exceptions=True) is None) assert (len(a) == 0)
def test_apprise_tagging(mock_post, mock_get): """ API: Apprise() object tagging functionality """ # A request robj = mock.Mock() setattr(robj, 'raw', mock.Mock()) # Allow raw.read() calls robj.raw.read.return_value = '' robj.text = '' robj.content = '' mock_get.return_value = robj mock_post.return_value = robj # Simulate a successful notification mock_get.return_value.status_code = requests.codes.ok mock_post.return_value.status_code = requests.codes.ok # Create our object a = Apprise() # An invalid addition can't add the tag assert (a.add('averyinvalidschema://localhost', tag='uhoh') is False) assert (a.add({ 'schema': 'averyinvalidschema', 'host': 'localhost' }, tag='uhoh') is False) # Add entry and assign it to a tag called 'awesome' assert (a.add('json://localhost/path1/', tag='awesome') is True) assert (a.add( { 'schema': 'json', 'host': 'localhost', 'fullpath': '/path1/' }, tag='awesome') is True) # Add another notification and assign it to a tag called 'awesome' # and another tag called 'local' assert (a.add('json://localhost/path2/', tag=['mmost', 'awesome']) is True) # notify the awesome tag; this would notify both services behind the # scenes assert (a.notify(title="my title", body="my body", tag='awesome') is True) # notify all of the tags assert (a.notify( title="my title", body="my body", tag=['awesome', 'mmost']) is True) # there is nothing to notify using tags 'missing'. However we intentionally # don't fail as there is value in identifying a tag that simply have # nothing to notify from while the object itself contains items assert (a.notify(title="my title", body="my body", tag='missing') is True) # Now to test the ability to and and/or notifications a = Apprise() # Add a tag by tuple assert (a.add('json://localhost/tagA/', tag=("TagA", )) is True) # Add 2 tags by string assert (a.add('json://localhost/tagAB/', tag="TagA, TagB") is True) # Add a tag using a set assert (a.add('json://localhost/tagB/', tag=set(["TagB"])) is True) # Add a tag by string (again) assert (a.add('json://localhost/tagC/', tag="TagC") is True) # Add 2 tags using a list assert (a.add('json://localhost/tagCD/', tag=["TagC", "TagD"]) is True) # Add a tag by string (again) assert (a.add('json://localhost/tagD/', tag="TagD") is True) # add a tag set by set (again) assert (a.add('json://localhost/tagCDE/', tag=set(["TagC", "TagD", "TagE"])) is True) # Expression: TagC and TagD # Matches the following only: # - json://localhost/tagCD/ # - json://localhost/tagCDE/ assert (a.notify(title="my title", body="my body", tag=[('TagC', 'TagD')]) is True) # Expression: (TagY and TagZ) or TagX # Matches nothing assert (a.notify(title="my title", body="my body", tag=[('TagY', 'TagZ'), 'TagX']) is True) # Expression: (TagY and TagZ) or TagA # Matches the following only: # - json://localhost/tagAB/ assert (a.notify(title="my title", body="my body", tag=[('TagY', 'TagZ'), 'TagA']) is True) # Expression: (TagE and TagD) or TagB # Matches the following only: # - json://localhost/tagCDE/ # - json://localhost/tagAB/ # - json://localhost/tagB/ assert (a.notify(title="my title", body="my body", tag=[('TagE', 'TagD'), 'TagB']) is True) # Garbage Entries assert (a.notify(title="my title", body="my body", tag=[ (object, ), ]) is True)
with open(twitch_category_id_file) as json_file: twitch_category_ids = load(json_file) else: twitch_category_ids = {} # get data on which channels are currently streaming live_data = liveStreams(config_data["channels"]) # find streams that have started within the threshold. time_offset = datetime.utcnow() - timedelta(minutes=config_data["offset"]) new_stream = namedtuple("data", "channel category") new_streams = [] for x in (x for x in live_data if time_offset < twitchTime(live_data[x]["started_at"])): new_streams.append( new_stream(live_data[x]["user_name"], live_data[x]["category_name"])) # send notifications if new streams have started if new_streams: apobj = Apprise() for notification in config_data["notifications"]: apobj.add(notification) message = buildMessage(new_streams) apobj.notify(title="Twitch", body=message) # save twitch_category_id_file only if new game has been added if rewrite_twitch_category_ids is True: with open(twitch_category_id_file, "w") as f: dump(twitch_category_ids, f, ensure_ascii=False)
def test_apprise(): """ API: Apprise() object """ # Caling load matix a second time which is an internal function causes it # to skip over content already loaded into our matrix and thefore accesses # other if/else parts of the code that aren't otherwise called __load_matrix() a = Apprise() # no items assert (len(a) == 0) # Create an Asset object asset = AppriseAsset(theme='default') # We can load the device using our asset a = Apprise(asset=asset) # We can load our servers up front as well servers = [ 'faast://abcdefghijklmnop-abcdefg', 'kodi://kodi.server.local', ] a = Apprise(servers=servers) # 2 servers loaded assert (len(a) == 2) # We can retrieve our URLs this way: assert (len(a.urls()) == 2) # We can add another server assert (a.add('mmosts://mattermost.server.local/' '3ccdd113474722377935511fc85d3dd4') is True) assert (len(a) == 3) # We can pop an object off of our stack by it's indexed value: obj = a.pop(0) assert (isinstance(obj, NotifyBase) is True) assert (len(a) == 2) # We can retrieve elements from our list too by reference: assert (isinstance(a[0].url(), six.string_types) is True) # We can iterate over our list too: count = 0 for o in a: assert (isinstance(o.url(), six.string_types) is True) count += 1 # verify that we did indeed iterate over each element assert (len(a) == count) # We can empty our set a.clear() assert (len(a) == 0) # An invalid schema assert (a.add('this is not a parseable url at all') is False) assert (len(a) == 0) # An unsupported schema assert (a.add('invalid://we.just.do.not.support.this.plugin.type') is False) assert (len(a) == 0) # A poorly formatted URL assert (a.add('json://user:@@@:bad?no.good') is False) assert (len(a) == 0) # Add a server with our asset we created earlier assert (a.add( 'mmosts://mattermost.server.local/' '3ccdd113474722377935511fc85d3dd4', asset=asset) is True) # Clear our server listings again a.clear() # No servers to notify assert (a.notify(title="my title", body="my body") is False) class BadNotification(NotifyBase): def __init__(self, **kwargs): super(BadNotification, self).__init__(**kwargs) # We fail whenever we're initialized raise TypeError() def url(self): # Support URL return '' class GoodNotification(NotifyBase): def __init__(self, **kwargs): super(GoodNotification, self).__init__(notify_format=NotifyFormat.HTML, **kwargs) def url(self): # Support URL return '' def notify(self, **kwargs): # Pretend everything is okay return True # Store our bad notification in our schema map SCHEMA_MAP['bad'] = BadNotification # Store our good notification in our schema map SCHEMA_MAP['good'] = GoodNotification # Just to explain what is happening here, we would have parsed the # url properly but failed when we went to go and create an instance # of it. assert (a.add('bad://localhost') is False) assert (len(a) == 0) assert (a.add('good://localhost') is True) assert (len(a) == 1) # Bad Notification Type is still allowed as it is presumed the user # know's what their doing assert (a.notify(title="my title", body="my body", notify_type='bad') is True) # No Title/Body combo's assert (a.notify(title=None, body=None) is False) assert (a.notify(title='', body=None) is False) assert (a.notify(title=None, body='') is False) # As long as one is present, we're good assert (a.notify(title=None, body='present') is True) assert (a.notify(title='present', body=None) is True) assert (a.notify(title="present", body="present") is True) # Clear our server listings again a.clear() class ThrowNotification(NotifyBase): def notify(self, **kwargs): # Pretend everything is okay raise TypeError() def url(self): # Support URL return '' class RuntimeNotification(NotifyBase): def notify(self, **kwargs): # Pretend everything is okay raise RuntimeError() def url(self): # Support URL return '' class FailNotification(NotifyBase): def notify(self, **kwargs): # Pretend everything is okay return False def url(self): # Support URL return '' # Store our bad notification in our schema map SCHEMA_MAP['throw'] = ThrowNotification # Store our good notification in our schema map SCHEMA_MAP['fail'] = FailNotification # Store our good notification in our schema map SCHEMA_MAP['runtime'] = RuntimeNotification assert (a.add('runtime://localhost') is True) assert (a.add('throw://localhost') is True) assert (a.add('fail://localhost') is True) assert (len(a) == 3) # Test when our notify both throws an exception and or just # simply returns False assert (a.notify(title="present", body="present") is False) # Create a Notification that throws an unexected exception class ThrowInstantiateNotification(NotifyBase): def __init__(self, **kwargs): # Pretend everything is okay raise TypeError() def url(self): # Support URL return '' SCHEMA_MAP['throw'] = ThrowInstantiateNotification # Reset our object a.clear() assert (len(a) == 0) # Instantiate a bad object plugin = a.instantiate(object, tag="bad_object") assert plugin is None # Instantiate a good object plugin = a.instantiate('good://localhost', tag="good") assert (isinstance(plugin, NotifyBase)) # Test simple tagging inside of the object assert ("good" in plugin) assert ("bad" not in plugin) # the in (__contains__ override) is based on or'ed content; so although # 'bad' isn't tagged as being in the plugin, 'good' is, so the return # value of this is True assert (["bad", "good"] in plugin) assert (set(["bad", "good"]) in plugin) assert (("bad", "good") in plugin) # We an add already substatiated instances into our Apprise object a.add(plugin) assert (len(a) == 1) # We can add entries as a list too (to add more then one) a.add([plugin, plugin, plugin]) assert (len(a) == 4) # Reset our object again a.clear() try: a.instantiate('throw://localhost', suppress_exceptions=False) assert (False) except TypeError: assert (True) assert (len(a) == 0) assert (a.instantiate('throw://localhost', suppress_exceptions=True) is None) assert (len(a) == 0) # # We rince and repeat the same tests as above, however we do them # using the dict version # # Reset our object a.clear() assert (len(a) == 0) # Instantiate a good object plugin = a.instantiate({'schema': 'good', 'host': 'localhost'}, tag="good") assert (isinstance(plugin, NotifyBase)) # Test simple tagging inside of the object assert ("good" in plugin) assert ("bad" not in plugin) # the in (__contains__ override) is based on or'ed content; so although # 'bad' isn't tagged as being in the plugin, 'good' is, so the return # value of this is True assert (["bad", "good"] in plugin) assert (set(["bad", "good"]) in plugin) assert (("bad", "good") in plugin) # We an add already substatiated instances into our Apprise object a.add(plugin) assert (len(a) == 1) # We can add entries as a list too (to add more then one) a.add([plugin, plugin, plugin]) assert (len(a) == 4) # Reset our object again a.clear() try: a.instantiate({ 'schema': 'throw', 'host': 'localhost' }, suppress_exceptions=False) assert (False) except TypeError: assert (True) assert (len(a) == 0) assert (a.instantiate({ 'schema': 'throw', 'host': 'localhost' }, suppress_exceptions=True) is None) assert (len(a) == 0)
def test_apprise_secure_logging(mock_post): """ API: Apprise() secure logging tests """ # Ensure we're not running in a disabled state logging.disable(logging.NOTSET) logger.setLevel(logging.CRITICAL) # Prepare Mock mock_post.return_value = requests.Request() mock_post.return_value.status_code = requests.codes.ok # Default Secure Logging is set to enabled asset = AppriseAsset() assert asset.secure_logging is True # Load our asset a = Apprise(asset=asset) with LogCapture(level=logging.DEBUG) as stream: # add a test server assert a.add("json://*****:*****@localhost") is True # Our servers should carry this flag a[0].asset.secure_logging is True logs = re.split(r'\r*\n', stream.getvalue().rstrip()) assert len(logs) == 1 entry = re.split(r'\s-\s', logs[0]) assert len(entry) == 3 assert entry[1] == 'DEBUG' assert entry[2].startswith( 'Loaded JSON URL: json://user:****@localhost/') # Send notification assert a.notify("test") is True # Test our call count assert mock_post.call_count == 1 # Reset mock_post.reset_mock() # Now we test the reverse configuration and turn off # secure logging. # Default Secure Logging is set to disable asset = AppriseAsset(secure_logging=False) assert asset.secure_logging is False # Load our asset a = Apprise(asset=asset) with LogCapture(level=logging.DEBUG) as stream: # add a test server assert a.add("json://*****:*****@localhost") is True # Our servers should carry this flag a[0].asset.secure_logging is False logs = re.split(r'\r*\n', stream.getvalue().rstrip()) assert len(logs) == 1 entry = re.split(r'\s-\s', logs[0]) assert len(entry) == 3 assert entry[1] == 'DEBUG' # Note that our password is no longer escaped (it is however # url encoded) assert entry[2].startswith( 'Loaded JSON URL: json://user:pass1%24-3%21@localhost/') # Disable Logging logging.disable(logging.CRITICAL)