def test_notify_matrix_dynamic_importing(tmpdir): """ API: Apprise() Notify Matrix Importing """ # Make our new path valid suite = tmpdir.mkdir("apprise_notify_test_suite") suite.join("__init__.py").write('') module_name = 'badnotify' # Update our path to point to our new test suite sys.path.insert(0, str(suite)) # Create a base area to work within base = suite.mkdir(module_name) base.join("__init__.py").write('') # Test no app_id base.join('NotifyBadFile1.py').write(""" class NotifyBadFile1(object): pass""") # No class of the same name base.join('NotifyBadFile2.py').write(""" class BadClassName(object): pass""") # Exception thrown base.join('NotifyBadFile3.py').write("""raise ImportError()""") # Utilizes a schema:// already occupied (as string) base.join('NotifyGoober.py').write(""" from apprise import NotifyBase class NotifyGoober(NotifyBase): # This class tests the fact we have a new class name, but we're # trying to over-ride items previously used # The default simple (insecure) protocol (used by NotifyMail) protocol = 'mailto' # The default secure protocol (used by NotifyMail) secure_protocol = 'mailtos'""") # Utilizes a schema:// already occupied (as tuple) base.join('NotifyBugger.py').write(""" from apprise import NotifyBase class NotifyBugger(NotifyBase): # This class tests the fact we have a new class name, but we're # trying to over-ride items previously used # The default simple (insecure) protocol (used by NotifyMail), the other # isn't protocol = ('mailto', 'bugger-test' ) # The default secure protocol (used by NotifyMail), the other isn't secure_protocol = ('mailtos', 'bugger-tests')""") __load_matrix(path=str(base), name=module_name)
def test_apprise_schemas(tmpdir): """ API: Apprise().schema() 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 # Garbage Protocol Entries protocol = None secure_protocol = (None, object) class HtmlNotification(NotifyBase): protocol = ('html', 'htm') secure_protocol = ('htmls', 'htms') class MarkDownNotification(NotifyBase): protocol = 'markdown' secure_protocol = 'markdowns' # Store our notifications into our schema map SCHEMA_MAP['text'] = TextNotification SCHEMA_MAP['html'] = HtmlNotification SCHEMA_MAP['markdown'] = MarkDownNotification schemas = URLBase.schemas(TextNotification) assert isinstance(schemas, set) is True # We didn't define a protocol or secure protocol assert len(schemas) == 0 schemas = URLBase.schemas(HtmlNotification) assert isinstance(schemas, set) is True assert len(schemas) == 4 assert 'html' in schemas assert 'htm' in schemas assert 'htmls' in schemas assert 'htms' in schemas # Invalid entries do not disrupt schema calls for garbage in (object(), None, 42): schemas = URLBase.schemas(garbage) assert isinstance(schemas, set) is True assert len(schemas) == 0
def test_apprise_details(): """ API: Apprise() Details """ # 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() # Details object details = a.details() # Dictionary response assert isinstance(details, dict) # Apprise version assert 'version' in details assert details.get('version') == __version__ # Defined schemas identify each plugin assert 'schemas' in details assert isinstance(details.get('schemas'), list) # We have an entry per defined plugin assert 'asset' in details assert isinstance(details.get('asset'), dict) assert 'app_id' in details['asset'] assert 'app_desc' in details['asset'] assert 'default_extension' in details['asset'] assert 'theme' in details['asset'] assert 'image_path_mask' in details['asset'] assert 'image_url_mask' in details['asset'] assert 'image_url_logo' in details['asset'] # All plugins must have a name defined; the below generates # a list of entrys that do not have a string defined. assert (not len([ x['service_name'] for x in details['schemas'] if not isinstance(x['service_name'], six.string_types) ]))
def test_apprise_details_plugin_verification(): """ API: Apprise() Details Plugin Verification """ # Reset our matrix __reset_matrix() __load_matrix() a = Apprise() # Details object details = a.details() # Dictionary response assert isinstance(details, dict) # Details object with language defined: details = a.details(lang='en') # Dictionary response assert isinstance(details, dict) # Details object with unsupported language: details = a.details(lang='xx') # Dictionary response assert isinstance(details, dict) # Apprise version assert 'version' in details assert details.get('version') == __version__ # Defined schemas identify each plugin assert 'schemas' in details assert isinstance(details.get('schemas'), list) # We have an entry per defined plugin assert 'asset' in details assert isinstance(details.get('asset'), dict) assert 'app_id' in details['asset'] assert 'app_desc' in details['asset'] assert 'default_extension' in details['asset'] assert 'theme' in details['asset'] assert 'image_path_mask' in details['asset'] assert 'image_url_mask' in details['asset'] assert 'image_url_logo' in details['asset'] # Valid Type Regular Expression Checker # Case Sensitive and MUST match the following: is_valid_type_re = re.compile(r'((choice|list):)?(string|bool|int|float)') # match tokens found in templates so we can cross reference them back # to see if they have a matching argument template_token_re = re.compile(r'{([^}]+)}[^{]*?(?=$|{)') # Define acceptable map_to arguments that can be tied in with the # kwargs function definitions. valid_kwargs = set([ # URL prepared kwargs 'user', 'password', 'port', 'host', 'schema', 'fullpath', # URLBase and NotifyBase args: 'verify', 'format', 'overflow', ]) # Valid Schema Entries: valid_schema_keys = ( 'name', 'private', 'required', 'type', 'values', 'min', 'max', 'regex', 'default', 'list', 'delim', 'prefix', 'map_to', 'alias_of', ) for entry in details['schemas']: # Track the map_to entries (if specified); We need to make sure that # these properly map back map_to_entries = set() # Track the alias_of entries map_to_aliases = set() # A Service Name MUST be defined assert 'service_name' in entry assert isinstance(entry['service_name'], six.string_types) # Acquire our protocols protocols = parse_list(entry['protocols'], entry['secure_protocols']) # At least one schema/protocol MUST be defined assert len(protocols) > 0 # our details assert 'details' in entry assert isinstance(entry['details'], dict) # All schema details should include args for section in ['kwargs', 'args', 'tokens']: assert section in entry['details'] assert isinstance(entry['details'][section], dict) for key, arg in entry['details'][section].items(): # Validate keys (case-sensitive) assert len( [k for k in arg.keys() if k not in valid_schema_keys]) == 0 # Test our argument assert isinstance(arg, dict) if 'alias_of' not in arg: # Minimum requirement of an argument assert 'name' in arg assert isinstance(arg['name'], six.string_types) assert 'type' in arg assert isinstance(arg['type'], six.string_types) assert is_valid_type_re.match(arg['type']) is not None if 'min' in arg: assert arg['type'].endswith('float') \ or arg['type'].endswith('int') assert isinstance(arg['min'], (int, float)) if 'max' in arg: # If a min and max was specified, at least check # to confirm the min is less then the max assert arg['min'] < arg['max'] if 'max' in arg: assert arg['type'].endswith('float') \ or arg['type'].endswith('int') assert isinstance(arg['max'], (int, float)) if 'private' in arg: assert isinstance(arg['private'], bool) if 'required' in arg: assert isinstance(arg['required'], bool) if 'prefix' in arg: assert isinstance(arg['prefix'], six.string_types) if section == 'kwargs': # The only acceptable prefix types for kwargs assert arg['prefix'] in ('+', '-') else: # kwargs requires that the 'prefix' is defined assert section != 'kwargs' if 'map_to' in arg: # must be a string assert isinstance(arg['map_to'], six.string_types) # Track our map_to object map_to_entries.add(arg['map_to']) else: map_to_entries.add(key) # Some verification if arg['type'].startswith('choice'): # choice:bool is redundant and should be swapped to # just bool assert not arg['type'].endswith('bool') # Choices require that a values list is provided assert 'values' in arg assert isinstance(arg['values'], (list, tuple)) assert len(arg['values']) > 0 # Test default if 'default' in arg: # if a default is provided on a choice object, # it better be in the list of values assert arg['default'] in arg['values'] if arg['type'].startswith('bool'): # Boolean choices are less restrictive but require a # default value assert 'default' in arg assert isinstance(arg['default'], bool) if 'regex' in arg: # Regex must ALWAYS be in the format (regex, option) assert isinstance(arg['regex'], (tuple, list)) assert len(arg['regex']) == 2 assert isinstance(arg['regex'][0], six.string_types) assert arg['regex'][1] is None or isinstance( arg['regex'][1], six.string_types) # Compile the regular expression to verify that it is # valid try: re.compile(arg['regex'][0]) except: assert '{} is an invalid regex'\ .format(arg['regex'][0]) # Regex should never start and/or end with ^/$; leave # that up to the user making use of the regex instead assert re.match(r'^[()\s]*\^', arg['regex'][0]) is None assert re.match(r'[()\s$]*\$', arg['regex'][0]) is None if arg['type'].startswith('list'): # Delimiters MUST be defined assert 'delim' in arg assert isinstance(arg['delim'], (list, tuple)) assert len(arg['delim']) > 0 else: # alias_of is in the object # must be a string assert isinstance(arg['alias_of'], six.string_types) # Track our alias_of object map_to_aliases.add(arg['alias_of']) # We should never map to ourselves assert arg['alias_of'] != key # 2 entries (name, and alias_of only!) assert len(entry['details'][section][key]) == 1 # inspect our object spec = inspect.getargspec(SCHEMA_MAP[protocols[0]].__init__) function_args = \ (set(parse_list(spec.keywords)) - set(['kwargs'])) \ | (set(spec.args) - set(['self'])) | valid_kwargs # Iterate over our map_to_entries and make sure that everything # maps to a function argument for arg in map_to_entries: if arg not in function_args: # This print statement just makes the error easier to # troubleshoot print( '{}:// template/arg/func reference missing error.'.format( protocols[0])) assert arg in function_args # Iterate over all of the function arguments and make sure that # it maps back to a key function_args -= valid_kwargs for arg in function_args: if arg not in map_to_entries: # This print statement just makes the error easier to # troubleshoot print( '{}:// template/func/arg reference missing error.'.format( protocols[0])) assert arg in map_to_entries # Iterate over our map_to_aliases and make sure they were defined in # either the as a token or arg for arg in map_to_aliases: assert arg in set(entry['details']['args'].keys()) \ | set(entry['details']['tokens'].keys()) # Template verification assert 'templates' in entry['details'] assert isinstance(entry['details']['templates'], (set, tuple, list)) # Iterate over our templates and parse our arguments for template in entry['details']['templates']: # Ensure we've properly opened and closed all of our tokens assert template.count('{') == template.count('}') expected_tokens = template.count('}') args = template_token_re.findall(template) assert expected_tokens == len(args) # Build a cross reference set of our current defined objects defined_tokens = set() for key, arg in entry['details']['tokens'].items(): defined_tokens.add(key) if 'alias_of' in arg: defined_tokens.add(arg['alias_of']) # We want to make sure all of our defined tokens have been # accounted for in at least one defined template for arg in args: assert arg in set(entry['details']['args'].keys()) \ | set(entry['details']['tokens'].keys()) # The reverse of the above; make sure that each entry defined # in the template_tokens is accounted for in at least one of # the defined templates assert arg in defined_tokens
def test_apprise_details(): """ API: Apprise() Details """ # Reset our matrix __reset_matrix() # This is a made up class that is just used to verify class TestDetailNotification(NotifyBase): """ This class is used to test various configurations supported """ # Minimum requirements for a plugin to produce details service_name = 'Detail Testing' # The default simple (insecure) protocol (used by NotifyMail) protocol = 'details' # Set test_bool flag always_true = True always_false = False # Define object templates templates = ( '{schema}://{host}', '{schema}://{host}:{port}', '{schema}://{user}@{host}:{port}', '{schema}://{user}:{pass}@{host}:{port}', ) # Define our tokens; these are the minimum tokens required required to # be passed into this function (as arguments). The syntax appends any # previously defined in the base package and builds onto them template_tokens = dict( NotifyBase.template_tokens, **{ 'notype': { # Nothing defined is still valid }, 'regex_test01': { 'name': _('RegexTest'), 'type': 'string', 'regex': r'[A-Z0-9]', }, 'regex_test02': { 'name': _('RegexTest'), # Support regex options too 'regex': (r'[A-Z0-9]', 'i'), }, 'regex_test03': { 'name': _('RegexTest'), # Support regex option without a second option 'regex': (r'[A-Z0-9]'), }, 'regex_test04': { # this entry would just end up getting removed 'regex': None, }, # List without delimiters (causes defaults to kick in) 'mylistA': { 'name': 'fruit', 'type': 'list:string', }, # A list with a delimiter list 'mylistB': { 'name': 'softdrinks', 'type': 'list:string', 'delim': ['|', '-'], }, }) template_args = dict( NotifyBase.template_args, **{ # Test _exist_if logic 'test_exists_if_01': { 'name': 'Always False', 'type': 'bool', # Provide a default 'default': False, # Base the existance of this key/value entry on the lookup # of this class value at runtime. Hence: # if not NotifyObject.always_false # del this_entry # '_exists_if': 'always_false', }, # Test _exist_if logic 'test_exists_if_02': { 'name': 'Always True', 'type': 'bool', # Provide a default 'default': False, # Base the existance of this key/value entry on the lookup # of this class value at runtime. Hence: # if not NotifyObject.always_true # del this_entry # '_exists_if': 'always_true', }, }) def url(self): # Support URL return '' def notify(self, **kwargs): # Pretend everything is okay (so we don't break other tests) return True # Store our good detail notification in our schema map SCHEMA_MAP['details'] = TestDetailNotification # Create our Apprise instance a = Apprise() # Dictionary response assert isinstance(a.details(), dict) # Reset our matrix __reset_matrix() __load_matrix()
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_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 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_apprise_cli_details(tmpdir): """ API: Apprise() Disabled Plugin States """ runner = CliRunner() # # Testing the printout of our details # --details or -l # result = runner.invoke(cli.main, [ '--details', ]) assert result.exit_code == 0 result = runner.invoke(cli.main, [ '-l', ]) assert result.exit_code == 0 # Reset our matrix __reset_matrix() # This is a made up class that is just used to verify class TestReq01Notification(NotifyBase): """ This class is used to test various requirement configurations """ # Set some requirements requirements = { 'packages_required': [ 'cryptography <= 3.4', 'ultrasync', ], 'packages_recommended': 'django', } def url(self, **kwargs): # Support URL return '' def send(self, **kwargs): # Pretend everything is okay (so we don't break other tests) return True SCHEMA_MAP['req01'] = TestReq01Notification # This is a made up class that is just used to verify class TestReq02Notification(NotifyBase): """ This class is used to test various requirement configurations """ # Just not enabled at all enabled = False # Set some requirements requirements = { # None and/or [] is implied, but jsut to show that the code won't # crash if explicitly set this way: 'packages_required': None, 'packages_recommended': [ 'cryptography <= 3.4', ] } def url(self, **kwargs): # Support URL return '' def send(self, **kwargs): # Pretend everything is okay (so we don't break other tests) return True SCHEMA_MAP['req02'] = TestReq02Notification # This is a made up class that is just used to verify class TestReq03Notification(NotifyBase): """ This class is used to test various requirement configurations """ # Set some requirements (but additionally include a details over-ride) requirements = { # We can over-ride the default details assigned to our plugin if # specified 'details': _('some specified requirement details'), # We can set a string value as well (it does not have to be a list) 'packages_recommended': 'cryptography <= 3.4' } def url(self, **kwargs): # Support URL return '' def send(self, **kwargs): # Pretend everything is okay (so we don't break other tests) return True SCHEMA_MAP['req03'] = TestReq03Notification # This is a made up class that is just used to verify class TestReq04Notification(NotifyBase): """ This class is used to test a case where our requirements is fixed to a None """ # This is the same as saying there are no requirements requirements = None def url(self, **kwargs): # Support URL return '' def send(self, **kwargs): # Pretend everything is okay (so we don't break other tests) return True SCHEMA_MAP['req04'] = TestReq04Notification # This is a made up class that is just used to verify class TestReq05Notification(NotifyBase): """ This class is used to test a case where only packages_recommended is identified """ requirements = {'packages_recommended': 'cryptography <= 3.4'} def url(self, **kwargs): # Support URL return '' def send(self, **kwargs): # Pretend everything is okay (so we don't break other tests) return True SCHEMA_MAP['req05'] = TestReq04Notification class TestDisabled01Notification(NotifyBase): """ This class is used to test a pre-disabled state """ # Just flat out disable our service enabled = False # we'll use this as a key to make our service easier to find # in the next part of the testing service_name = 'na01' def url(self, **kwargs): # Support URL return '' def notify(self, **kwargs): # Pretend everything is okay (so we don't break other tests) return True SCHEMA_MAP['na01'] = TestDisabled01Notification class TestDisabled02Notification(NotifyBase): """ This class is used to test a post-disabled state """ # we'll use this as a key to make our service easier to find # in the next part of the testing service_name = 'na02' def __init__(self, *args, **kwargs): super(TestDisabled02Notification, self).__init__(**kwargs) # enable state changes **AFTER** we initialize self.enabled = False def url(self, **kwargs): # Support URL return '' def notify(self, **kwargs): # Pretend everything is okay (so we don't break other tests) return True SCHEMA_MAP['na02'] = TestDisabled02Notification # We'll add a good notification to our list class TesEnabled01Notification(NotifyBase): """ This class is just a simple enabled one """ # we'll use this as a key to make our service easier to find # in the next part of the testing service_name = 'good' def url(self, **kwargs): # Support URL return '' def send(self, **kwargs): # Pretend everything is okay (so we don't break other tests) return True SCHEMA_MAP['good'] = TesEnabled01Notification # Verify that we can pass through all of our different details result = runner.invoke(cli.main, [ '--details', ]) assert result.exit_code == 0 result = runner.invoke(cli.main, [ '-l', ]) assert result.exit_code == 0 # Reset our matrix __reset_matrix() __load_matrix()