mirror of https://github.com/caronc/apprise
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1406 lines
39 KiB
1406 lines
39 KiB
# -*- coding: utf-8 -*- |
|
# BSD 2-Clause License |
|
# |
|
# Apprise - Push Notification Library. |
|
# Copyright (c) 2024, Chris Caron <lead2gold@gmail.com> |
|
# |
|
# Redistribution and use in source and binary forms, with or without |
|
# modification, are permitted provided that the following conditions are met: |
|
# |
|
# 1. Redistributions of source code must retain the above copyright notice, |
|
# this list of conditions and the following disclaimer. |
|
# |
|
# 2. Redistributions in binary form must reproduce the above copyright notice, |
|
# this list of conditions and the following disclaimer in the documentation |
|
# and/or other materials provided with the distribution. |
|
# |
|
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" |
|
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE |
|
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE |
|
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE |
|
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR |
|
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF |
|
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS |
|
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN |
|
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) |
|
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE |
|
# POSSIBILITY OF SUCH DAMAGE. |
|
|
|
import sys |
|
import pytest |
|
from unittest import mock |
|
from apprise import NotifyFormat |
|
from apprise import ConfigFormat |
|
from apprise import ContentIncludeMode |
|
from apprise import Apprise |
|
from apprise import AppriseConfig |
|
from apprise import AppriseAsset |
|
from apprise.config import ConfigBase |
|
from apprise.plugins import NotifyBase |
|
from apprise import NotificationManager |
|
from apprise import ConfigurationManager |
|
|
|
from apprise.config.file import ConfigFile |
|
|
|
# Disable logging for a cleaner testing output |
|
import logging |
|
logging.disable(logging.CRITICAL) |
|
|
|
# Grant access to our Notification Manager Singleton |
|
N_MGR = NotificationManager() |
|
|
|
# Grant access to our Configuration Manager Singleton |
|
C_MGR = ConfigurationManager() |
|
|
|
|
|
def test_apprise_config(tmpdir): |
|
""" |
|
API: AppriseConfig basic testing |
|
|
|
""" |
|
|
|
# Create ourselves a config object |
|
ac = AppriseConfig() |
|
|
|
# There are no servers loaded |
|
assert len(ac) == 0 |
|
|
|
# Object can be directly checked as a boolean; response is False |
|
# when there are no entries loaded |
|
assert not ac |
|
|
|
# lets try anyway |
|
assert len(ac.servers()) == 0 |
|
|
|
t = tmpdir.mkdir("simple-formatting").join("apprise") |
|
t.write(""" |
|
# A comment line over top of a URL |
|
mailto://usera:pass@gmail.com |
|
|
|
# A line with mulitiple tag assignments to it |
|
taga,tagb=gnome:// |
|
|
|
# Event if there is accidental leading spaces, this configuation |
|
# is accepting of htat and will not exclude them |
|
tagc=kde:// |
|
|
|
# A very poorly structured url |
|
sns://:@/ |
|
|
|
# Just 1 token provided causes exception |
|
sns://T1JJ3T3L2/ |
|
|
|
# XML |
|
xml://localhost/?+HeaderEntry=Test&:IgnoredEntry=Ignored |
|
""") |
|
|
|
# Create ourselves a config object |
|
ac = AppriseConfig(paths=str(t)) |
|
|
|
# One configuration file should have been found |
|
assert len(ac) == 1 |
|
|
|
# Object can be directly checked as a boolean; response is True |
|
# when there is at least one entry |
|
assert ac |
|
|
|
# We should be able to read our 4 servers from that |
|
assert len(ac.servers()) == 4 |
|
|
|
# Get our URL back |
|
assert isinstance(ac[0].url(), str) |
|
|
|
# Test cases where our URL is invalid |
|
t = tmpdir.mkdir("strange-lines").join("apprise") |
|
t.write(""" |
|
# basicly this consists of defined tags and no url |
|
tag= |
|
""") |
|
|
|
# Create ourselves a config object |
|
ac = AppriseConfig(paths=str(t), asset=AppriseAsset()) |
|
|
|
# One configuration file should have been found |
|
assert len(ac) == 1 |
|
|
|
# No urls were set |
|
assert len(ac.servers()) == 0 |
|
|
|
# Create a ConfigBase object |
|
cb = ConfigBase() |
|
|
|
# Test adding of all entries |
|
assert ac.add(configs=cb, asset=AppriseAsset(), tag='test') is True |
|
|
|
# Test adding of all entries |
|
assert ac.add( |
|
configs=['file://?', ], asset=AppriseAsset(), tag='test') is False |
|
|
|
# Test the adding of garbage |
|
assert ac.add(configs=object()) is False |
|
|
|
# Try again but enforce our format |
|
ac = AppriseConfig(paths='file://{}?format=text'.format(str(t))) |
|
|
|
# One configuration file should have been found |
|
assert len(ac) == 1 |
|
|
|
# No urls were set |
|
assert len(ac.servers()) == 0 |
|
|
|
# |
|
# Test Internatialization and the handling of unicode characters |
|
# |
|
istr = """ |
|
# Iñtërnâtiônàlization Testing |
|
windows://""" |
|
|
|
# Write our content to our file |
|
t = tmpdir.mkdir("internationalization").join("apprise") |
|
with open(str(t), 'wb') as f: |
|
f.write(istr.encode('latin-1')) |
|
|
|
# Create ourselves a config object |
|
ac = AppriseConfig(paths=str(t)) |
|
|
|
# One configuration file should have been found |
|
assert len(ac) == 1 |
|
|
|
# This will fail because our default encoding is utf-8; however the file |
|
# we opened was not; it was latin-1 and could not be parsed. |
|
assert len(ac.servers()) == 0 |
|
|
|
# Test iterator |
|
count = 0 |
|
for entry in ac: |
|
count += 1 |
|
assert len(ac) == count |
|
|
|
# We can fix this though; set our encoding to latin-1 |
|
ac = AppriseConfig(paths='file://{}?encoding=latin-1'.format(str(t))) |
|
|
|
# One configuration file should have been found |
|
assert len(ac) == 1 |
|
|
|
# Our URL should be found |
|
assert len(ac.servers()) == 1 |
|
|
|
# Get our URL back |
|
assert isinstance(ac[0].url(), str) |
|
|
|
# pop an entry from our list |
|
assert isinstance(ac.pop(0), ConfigBase) |
|
|
|
# Determine we have no more configuration entries loaded |
|
assert len(ac) == 0 |
|
|
|
# |
|
# Test buffer handling (and overflow) |
|
t = tmpdir.mkdir("buffer-handling").join("apprise") |
|
buf = "gnome://" |
|
t.write(buf) |
|
|
|
# Reset our config object |
|
ac.clear() |
|
|
|
# Create ourselves a config object |
|
ac = AppriseConfig(paths=str(t)) |
|
|
|
# update our length to be the size of our actual file |
|
ac[0].max_buffer_size = len(buf) |
|
|
|
# One configuration file should have been found |
|
assert len(ac) == 1 |
|
|
|
assert len(ac.servers()) == 1 |
|
|
|
# update our buffer size to be slightly smaller then what we allow |
|
ac[0].max_buffer_size = len(buf) - 1 |
|
|
|
# Content is automatically cached; so even though we adjusted the buffer |
|
# above, our results have been cached so we get a 1 response. |
|
assert len(ac.servers()) == 1 |
|
|
|
|
|
def test_apprise_multi_config_entries(tmpdir): |
|
""" |
|
API: AppriseConfig basic multi-adding functionality |
|
|
|
""" |
|
# temporary file to work with |
|
t = tmpdir.mkdir("apprise-multi-add").join("apprise") |
|
buf = """ |
|
good://hostname |
|
""" |
|
t.write(buf) |
|
|
|
# temporary empty file to work with |
|
te = tmpdir.join("apprise-multi-add", "apprise-empty") |
|
te.write("") |
|
|
|
# Define our good:// url |
|
class GoodNotification(NotifyBase): |
|
def __init__(self, **kwargs): |
|
super().__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 |
|
N_MGR._schema_map['good'] = GoodNotification |
|
|
|
# Create ourselves a config object |
|
ac = AppriseConfig() |
|
|
|
# There are no servers loaded |
|
assert len(ac) == 0 |
|
|
|
# Support adding of muilt strings and objects: |
|
assert ac.add(configs=(str(t), str(t))) is True |
|
assert ac.add(configs=( |
|
ConfigFile(path=str(te)), ConfigFile(path=str(t)))) is True |
|
|
|
# don't support the adding of invalid content |
|
assert ac.add(configs=(object(), object())) is False |
|
assert ac.add(configs=object()) is False |
|
|
|
# Try to pop an element out of range |
|
with pytest.raises(IndexError): |
|
ac.server_pop(len(ac.servers())) |
|
|
|
# Pop our elements |
|
while len(ac.servers()) > 0: |
|
assert isinstance( |
|
ac.server_pop(len(ac.servers()) - 1), NotifyBase) |
|
|
|
|
|
def test_apprise_add_config(): |
|
""" |
|
API AppriseConfig.add_config() |
|
|
|
""" |
|
content = """ |
|
# A comment line over top of a URL |
|
mailto://usera:pass@gmail.com |
|
|
|
# A line with mulitiple tag assignments to it |
|
taga,tagb=gnome:// |
|
|
|
# Event if there is accidental leading spaces, this configuation |
|
# is accepting of htat and will not exclude them |
|
tagc=kde:// |
|
|
|
# A very poorly structured url |
|
sns://:@/ |
|
|
|
# Just 1 token provided causes exception |
|
sns://T1JJ3T3L2/ |
|
""" |
|
# Create ourselves a config object |
|
ac = AppriseConfig() |
|
assert ac.add_config(content=content) is True |
|
|
|
# One configuration file should have been found |
|
assert len(ac) == 1 |
|
assert ac[0].config_format is ConfigFormat.TEXT |
|
|
|
# Object can be directly checked as a boolean; response is True |
|
# when there is at least one entry |
|
assert ac |
|
|
|
# We should be able to read our 3 servers from that |
|
assert len(ac.servers()) == 3 |
|
|
|
# Get our URL back |
|
assert isinstance(ac[0].url(), str) |
|
|
|
# Test invalid content |
|
assert ac.add_config(content=object()) is False |
|
assert ac.add_config(content=42) is False |
|
assert ac.add_config(content=None) is False |
|
|
|
# Still only one server loaded |
|
assert len(ac) == 1 |
|
|
|
# Test having a pre-defined asset object and tag created |
|
assert ac.add_config( |
|
content=content, asset=AppriseAsset(), tag='a') is True |
|
|
|
# Now there are 2 servers loaded |
|
assert len(ac) == 2 |
|
|
|
# and 6 urls.. (as we've doubled up) |
|
assert len(ac.servers()) == 6 |
|
|
|
content = """ |
|
# A YAML File |
|
urls: |
|
- mailto://usera:pass@gmail.com |
|
- gnome://: |
|
tag: taga,tagb |
|
|
|
- json://localhost: |
|
+HeaderEntry1: 'a header entry' |
|
-HeaderEntryDepricated: 'a deprecated entry' |
|
:HeaderEntryIgnored: 'an ignored header entry' |
|
|
|
- xml://localhost: |
|
+HeaderEntry1: 'a header entry' |
|
-HeaderEntryDepricated: 'a deprecated entry' |
|
:HeaderEntryIgnored: 'an ignored header entry' |
|
""" |
|
|
|
# Create ourselves a config object |
|
ac = AppriseConfig() |
|
assert ac.add_config(content=content) is True |
|
|
|
# One configuration file should have been found |
|
assert len(ac) == 1 |
|
assert ac[0].config_format is ConfigFormat.YAML |
|
|
|
# Object can be directly checked as a boolean; response is True |
|
# when there is at least one entry |
|
assert ac |
|
|
|
# We should be able to read our 4 servers from that |
|
assert len(ac.servers()) == 4 |
|
|
|
# Now an invalid configuration file |
|
content = "invalid" |
|
|
|
# Create ourselves a config object |
|
ac = AppriseConfig() |
|
assert ac.add_config(content=content) is False |
|
|
|
# Nothing is loaded |
|
assert len(ac.servers()) == 0 |
|
|
|
|
|
def test_apprise_config_tagging(tmpdir): |
|
""" |
|
API: AppriseConfig tagging |
|
|
|
""" |
|
|
|
# temporary file to work with |
|
t = tmpdir.mkdir("tagging").join("apprise") |
|
buf = "gnome://" |
|
t.write(buf) |
|
|
|
# Create ourselves a config object |
|
ac = AppriseConfig() |
|
|
|
# Add an item associated with tag a |
|
assert ac.add(configs=str(t), asset=AppriseAsset(), tag='a') is True |
|
# Add an item associated with tag b |
|
assert ac.add(configs=str(t), asset=AppriseAsset(), tag='b') is True |
|
# Add an item associated with tag a or b |
|
assert ac.add(configs=str(t), asset=AppriseAsset(), tag='a,b') is True |
|
|
|
# Now filter: a: |
|
assert len(ac.servers(tag='a')) == 2 |
|
# Now filter: a or b: |
|
assert len(ac.servers(tag='a,b')) == 3 |
|
# Now filter: a and b |
|
assert len(ac.servers(tag=[('a', 'b')])) == 1 |
|
# all matches everything |
|
assert len(ac.servers(tag='all')) == 3 |
|
|
|
# Test cases using the `always` keyword |
|
# Create ourselves a config object |
|
ac = AppriseConfig() |
|
|
|
# Add an item associated with tag a |
|
assert ac.add(configs=str(t), asset=AppriseAsset(), tag='a,always') is True |
|
# Add an item associated with tag b |
|
assert ac.add(configs=str(t), asset=AppriseAsset(), tag='b') is True |
|
# Add an item associated with tag a or b |
|
assert ac.add(configs=str(t), asset=AppriseAsset(), tag='c,d') is True |
|
|
|
# Now filter: a: |
|
assert len(ac.servers(tag='a')) == 1 |
|
# Now filter: a or b: |
|
assert len(ac.servers(tag='a,b')) == 2 |
|
# Now filter: e |
|
# we'll match the `always' |
|
assert len(ac.servers(tag='e')) == 1 |
|
assert len(ac.servers(tag='e', match_always=False)) == 0 |
|
# all matches everything |
|
assert len(ac.servers(tag='all')) == 3 |
|
|
|
# Now filter: d |
|
# we'll match the `always' tag |
|
assert len(ac.servers(tag='d')) == 2 |
|
assert len(ac.servers(tag='d', match_always=False)) == 1 |
|
|
|
|
|
def test_apprise_config_instantiate(): |
|
""" |
|
API: AppriseConfig.instantiate() |
|
|
|
""" |
|
assert AppriseConfig.instantiate( |
|
'file://?', suppress_exceptions=True) is None |
|
|
|
assert AppriseConfig.instantiate( |
|
'invalid://?', suppress_exceptions=True) is None |
|
|
|
class BadConfig(ConfigBase): |
|
# always allow incusion |
|
allow_cross_includes = ContentIncludeMode.ALWAYS |
|
|
|
def __init__(self, **kwargs): |
|
super().__init__(**kwargs) |
|
|
|
# We fail whenever we're initialized |
|
raise TypeError() |
|
|
|
@staticmethod |
|
def parse_url(url, *args, **kwargs): |
|
# always parseable |
|
return ConfigBase.parse_url(url, verify_host=False) |
|
|
|
# Store our bad configuration in our schema map |
|
C_MGR['bad'] = BadConfig |
|
|
|
with pytest.raises(TypeError): |
|
AppriseConfig.instantiate( |
|
'bad://path', suppress_exceptions=False) |
|
|
|
# Same call but exceptions suppressed |
|
assert AppriseConfig.instantiate( |
|
'bad://path', suppress_exceptions=True) is None |
|
|
|
|
|
def test_invalid_apprise_config(tmpdir): |
|
""" |
|
Parse invalid configuration includes |
|
|
|
""" |
|
|
|
class BadConfig(ConfigBase): |
|
# always allow incusion |
|
allow_cross_includes = ContentIncludeMode.ALWAYS |
|
|
|
def __init__(self, **kwargs): |
|
super().__init__(**kwargs) |
|
|
|
# We intentionally fail whenever we're initialized |
|
raise TypeError() |
|
|
|
@staticmethod |
|
def parse_url(url, *args, **kwargs): |
|
# always parseable |
|
return ConfigBase.parse_url(url, verify_host=False) |
|
|
|
# Store our bad configuration in our schema map |
|
C_MGR['bad'] = BadConfig |
|
|
|
# temporary file to work with |
|
t = tmpdir.mkdir("apprise-bad-obj").join("invalid") |
|
buf = """ |
|
# Include an invalid schema |
|
include invalid:// |
|
|
|
# An unparsable valid schema |
|
include https:// |
|
|
|
# A valid configuration that will throw an exception |
|
include bad:// |
|
|
|
# Include ourselves (So our recursive includes fails as well) |
|
include {} |
|
|
|
""".format(str(t)) |
|
t.write(buf) |
|
|
|
# Create ourselves a config object with caching disbled |
|
ac = AppriseConfig(recursion=2, insecure_includes=True, cache=False) |
|
|
|
# Nothing loaded yet |
|
assert len(ac) == 0 |
|
|
|
# Add our config |
|
assert ac.add(configs=str(t), asset=AppriseAsset()) is True |
|
|
|
# One configuration file |
|
assert len(ac) == 1 |
|
|
|
# All of the servers were invalid and would not load |
|
assert len(ac.servers()) == 0 |
|
|
|
|
|
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().__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 |
|
N_MGR._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) |
|
|
|
# Our length is unchanged |
|
assert len(a) == 5 |
|
|
|
# pop the index |
|
ref_popped = a.pop(0) |
|
|
|
# Verify our response |
|
assert isinstance(ref_popped, NotifyBase) |
|
|
|
# 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 |
|
with pytest.raises(IndexError): |
|
a.pop(len(a)) |
|
|
|
# Our length remains unchanged |
|
assert len(a) == 4 |
|
|
|
# Reference content out of range |
|
with pytest.raises(IndexError): |
|
a[len(a)] |
|
|
|
# reference index at the end of our list |
|
ref = a[len(a) - 1] |
|
|
|
# Verify our response |
|
assert isinstance(ref, NotifyBase) |
|
|
|
# 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) |
|
|
|
# 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) |
|
|
|
# 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) |
|
|
|
# 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) |
|
|
|
|
|
def test_recursive_config_inclusion(tmpdir): |
|
""" |
|
API: Apprise() Recursive Config Inclusion |
|
|
|
""" |
|
|
|
# To test our config classes, we make three dummy configs |
|
class ConfigCrossPostAlways(ConfigFile): |
|
""" |
|
A dummy config that is set to always allow inclusion |
|
""" |
|
|
|
service_name = 'always' |
|
|
|
# protocol |
|
protocol = 'always' |
|
|
|
# Always type |
|
allow_cross_includes = ContentIncludeMode.ALWAYS |
|
|
|
class ConfigCrossPostStrict(ConfigFile): |
|
""" |
|
A dummy config that is set to strict inclusion |
|
""" |
|
|
|
service_name = 'strict' |
|
|
|
# protocol |
|
protocol = 'strict' |
|
|
|
# Always type |
|
allow_cross_includes = ContentIncludeMode.STRICT |
|
|
|
class ConfigCrossPostNever(ConfigFile): |
|
""" |
|
A dummy config that is set to never allow inclusion |
|
""" |
|
|
|
service_name = 'never' |
|
|
|
# protocol |
|
protocol = 'never' |
|
|
|
# Always type |
|
allow_cross_includes = ContentIncludeMode.NEVER |
|
|
|
# store our entries |
|
C_MGR['never'] = ConfigCrossPostNever |
|
C_MGR['strict'] = ConfigCrossPostStrict |
|
C_MGR['always'] = ConfigCrossPostAlways |
|
|
|
# Make our new path valid |
|
suite = tmpdir.mkdir("apprise_config_recursion") |
|
|
|
cfg01 = suite.join("cfg01.cfg") |
|
cfg02 = suite.mkdir("dir1").join("cfg02.cfg") |
|
cfg03 = suite.mkdir("dir2").join("cfg03.cfg") |
|
cfg04 = suite.mkdir("dir3").join("cfg04.cfg") |
|
|
|
# Populate our files with valid configuration include lines |
|
cfg01.write(""" |
|
# json entry |
|
json://localhost:8080 |
|
|
|
# absolute path inclusion to ourselves |
|
include {}""".format(str(cfg01))) |
|
|
|
cfg02.write(""" |
|
# json entry |
|
json://localhost:8080 |
|
|
|
# recursively include ourselves |
|
include cfg02.cfg""") |
|
|
|
cfg03.write(""" |
|
# xml entry |
|
xml://localhost:8080 |
|
|
|
# relative path inclusion |
|
include ../dir1/cfg02.cfg |
|
|
|
# test that we can't include invalid entries |
|
include invalid://entry |
|
|
|
# Include non includable type |
|
include memory://""") |
|
|
|
cfg04.write(""" |
|
# xml entry |
|
xml://localhost:8080 |
|
|
|
# always include of our file |
|
include always://{} |
|
|
|
# never include of our file |
|
include never://{} |
|
|
|
# strict include of our file |
|
include strict://{}""".format(str(cfg04), str(cfg04), str(cfg04))) |
|
|
|
# Create ourselves a config object |
|
ac = AppriseConfig() |
|
|
|
# There are no servers loaded |
|
assert len(ac) == 0 |
|
|
|
# load our configuration |
|
assert ac.add(configs=str(cfg01)) is True |
|
|
|
# verify it loaded |
|
assert len(ac) == 1 |
|
|
|
# 1 service will be loaded as there is no recursion at this point |
|
assert len(ac.servers()) == 1 |
|
|
|
# Create ourselves a config object |
|
ac = AppriseConfig(recursion=1) |
|
|
|
# load our configuration |
|
assert ac.add(configs=str(cfg01)) is True |
|
|
|
# verify one configuration file loaded however since it recursively |
|
# loaded itself 1 more time, it still doesn't impact the load count: |
|
assert len(ac) == 1 |
|
|
|
# 2 services loaded now that we loaded the same file twice |
|
assert len(ac.servers()) == 2 |
|
|
|
# |
|
# Now we test relative file inclusion |
|
# |
|
|
|
# Create ourselves a config object |
|
ac = AppriseConfig(recursion=10) |
|
|
|
# There are no servers loaded |
|
assert len(ac) == 0 |
|
|
|
# load our configuration |
|
assert ac.add(configs=str(cfg02)) is True |
|
|
|
# verify it loaded |
|
assert len(ac) == 1 |
|
|
|
# 11 services loaded because we reloaded ourselves 10 times |
|
# after loading the first entry |
|
assert len(ac.servers()) == 11 |
|
|
|
# Test our include modes (strict, always, and never) |
|
|
|
# Create ourselves a config object |
|
ac = AppriseConfig(recursion=1) |
|
|
|
# There are no servers loaded |
|
assert len(ac) == 0 |
|
|
|
# load our configuration |
|
assert ac.add(configs=str(cfg04)) is True |
|
|
|
# verify it loaded |
|
assert len(ac) == 1 |
|
|
|
# 2 servers loaded |
|
# 1 - from the file read (which is set at mode STRICT |
|
# 1 - from the always:// |
|
# |
|
# The never:// can ever be includeed, and the strict:// is ot of type |
|
# file:// (the one doing the include) so it is also ignored. |
|
# |
|
# By turning on the insecure_includes, we can include the strict files too |
|
assert len(ac.servers()) == 2 |
|
|
|
# Create ourselves a config object |
|
ac = AppriseConfig(recursion=1, insecure_includes=True) |
|
|
|
# There are no servers loaded |
|
assert len(ac) == 0 |
|
|
|
# load our configuration |
|
assert ac.add(configs=str(cfg04)) is True |
|
|
|
# verify it loaded |
|
assert len(ac) == 1 |
|
|
|
# 3 servers loaded |
|
# 1 - from the file read (which is set at mode STRICT |
|
# 1 - from the always:// |
|
# 1 - from the strict:// (due to insecure_includes set) |
|
assert len(ac.servers()) == 3 |
|
|
|
|
|
def test_apprise_config_matrix_load(): |
|
""" |
|
API: AppriseConfig() matrix initialization |
|
|
|
""" |
|
|
|
import apprise |
|
|
|
class ConfigDummy(ConfigBase): |
|
""" |
|
A dummy wrapper for testing the different options in the load_matrix |
|
function |
|
""" |
|
|
|
# The default descriptive name associated with the Notification |
|
service_name = 'dummy' |
|
|
|
# protocol as tuple |
|
protocol = ('uh', 'oh') |
|
|
|
# secure protocol as tuple |
|
secure_protocol = ('no', 'yes') |
|
|
|
class ConfigDummy2(ConfigBase): |
|
""" |
|
A dummy wrapper for testing the different options in the load_matrix |
|
function |
|
""" |
|
|
|
# The default descriptive name associated with the Notification |
|
service_name = 'dummy2' |
|
|
|
# secure protocol as tuple |
|
secure_protocol = ('true', 'false') |
|
|
|
class ConfigDummy3(ConfigBase): |
|
""" |
|
A dummy wrapper for testing the different options in the load_matrix |
|
function |
|
""" |
|
|
|
# The default descriptive name associated with the Notification |
|
service_name = 'dummy3' |
|
|
|
# secure protocol as string |
|
secure_protocol = 'true' |
|
|
|
class ConfigDummy4(ConfigBase): |
|
""" |
|
A dummy wrapper for testing the different options in the load_matrix |
|
function |
|
""" |
|
|
|
# The default descriptive name associated with the Notification |
|
service_name = 'dummy4' |
|
|
|
# protocol as string |
|
protocol = 'true' |
|
|
|
# Generate ourselves a fake entry |
|
apprise.config.ConfigDummy = ConfigDummy |
|
apprise.config.ConfigDummy2 = ConfigDummy2 |
|
apprise.config.ConfigDummy3 = ConfigDummy3 |
|
apprise.config.ConfigDummy4 = ConfigDummy4 |
|
|
|
|
|
def test_configmatrix_dynamic_importing(tmpdir): |
|
""" |
|
API: Apprise() Config Matrix Importing |
|
|
|
""" |
|
|
|
# Make our new path valid |
|
suite = tmpdir.mkdir("apprise_config_test_suite") |
|
suite.join("__init__.py").write('') |
|
|
|
module_name = 'badconfig' |
|
|
|
# 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('ConfigBadFile1.py').write( |
|
""" |
|
class ConfigBadFile1: |
|
pass""") |
|
|
|
# No class of the same name |
|
base.join('ConfigBadFile2.py').write( |
|
""" |
|
class BadClassName: |
|
pass""") |
|
|
|
# Exception thrown |
|
base.join('ConfigBadFile3.py').write("""raise ImportError()""") |
|
|
|
# Utilizes a schema:// already occupied (as string) |
|
base.join('ConfigGoober.py').write( |
|
""" |
|
from apprise.config import ConfigBase |
|
class ConfigGoober(ConfigBase): |
|
# 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 ConfigHTTP) |
|
protocol = ('http', 'goober') |
|
|
|
# The default secure protocol (used by ConfigHTTP) |
|
secure_protocol = 'https' |
|
|
|
@staticmethod |
|
def parse_url(url, *args, **kwargs): |
|
# always parseable |
|
return ConfigBase.parse_url(url, verify_host=False)""") |
|
|
|
# Utilizes a schema:// already occupied (as tuple) |
|
base.join('ConfigBugger.py').write(""" |
|
from apprise.config import ConfigBase |
|
class ConfigBugger(ConfigBase): |
|
# 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 ConfigHTTP), the other |
|
# isn't |
|
protocol = ('http', 'bugger-test' ) |
|
|
|
# The default secure protocol (used by ConfigHTTP), the other isn't |
|
secure_protocol = ('https', ['garbage']) |
|
|
|
@staticmethod |
|
def parse_url(url, *args, **kwargs): |
|
# always parseable |
|
return ConfigBase.parse_url(url, verify_host=False)""") |
|
|
|
|
|
@mock.patch('os.path.getsize') |
|
def test_config_base_parse_inaccessible_text_file(mock_getsize, tmpdir): |
|
""" |
|
API: ConfigBase.parse_inaccessible_text_file |
|
|
|
""" |
|
|
|
# temporary file to work with |
|
t = tmpdir.mkdir("inaccessible").join("apprise") |
|
buf = "gnome://" |
|
t.write(buf) |
|
|
|
# Set getsize return value |
|
mock_getsize.return_value = None |
|
mock_getsize.side_effect = OSError |
|
|
|
# Create ourselves a config object |
|
ac = AppriseConfig(paths=str(t)) |
|
|
|
# The following internally throws an exception but still counts |
|
# as a loaded configuration file |
|
assert len(ac) == 1 |
|
|
|
# Thus no notifications are loaded |
|
assert len(ac.servers()) == 0 |
|
|
|
|
|
def test_config_base_parse_yaml_file01(tmpdir): |
|
""" |
|
API: ConfigBase.parse_yaml_file (#1) |
|
|
|
""" |
|
t = tmpdir.mkdir("empty-file").join("apprise.yml") |
|
t.write("") |
|
|
|
# Create ourselves a config object |
|
ac = AppriseConfig(paths=str(t)) |
|
|
|
# The number of configuration files that exist |
|
assert len(ac) == 1 |
|
|
|
# no notifications are loaded |
|
assert len(ac.servers()) == 0 |
|
|
|
|
|
def test_config_base_parse_yaml_file02(tmpdir): |
|
""" |
|
API: ConfigBase.parse_yaml_file (#2) |
|
|
|
""" |
|
t = tmpdir.mkdir("matching-tags").join("apprise.yml") |
|
t.write("""urls: |
|
- pover://nsisxnvnqixq39t0cw54pxieyvtdd9@2jevtmstfg5a7hfxndiybasttxxfku: |
|
- tag: test1 |
|
- pover://rg8ta87qngcrkc6t4qbykxktou0uug@tqs3i88xlufexwl8t4asglt4zp5wfn: |
|
- tag: test2 |
|
- pover://jcqgnlyq2oetea4qg3iunahj8d5ijm@evalvutkhc8ipmz2lcgc70wtsm0qpb: |
|
- tag: test3""") |
|
|
|
# Create ourselves a config object |
|
ac = AppriseConfig(paths=str(t)) |
|
|
|
# The number of configuration files that exist |
|
assert len(ac) == 1 |
|
|
|
# no notifications are loaded |
|
assert len(ac.servers()) == 3 |
|
|
|
# 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 3 entry as they should have loaded successfully |
|
assert len(a) == 3 |
|
|
|
# No match |
|
assert sum(1 for _ in a.find('no-match')) == 0 |
|
# Match everything |
|
assert sum(1 for _ in a.find('all')) == 3 |
|
# Match test1 entry |
|
assert sum(1 for _ in a.find('test1')) == 1 |
|
# Match test2 entry |
|
assert sum(1 for _ in a.find('test2')) == 1 |
|
# Match test3 entry |
|
assert sum(1 for _ in a.find('test3')) == 1 |
|
# Match test1 or test3 entry |
|
assert sum(1 for _ in a.find('test1, test3')) == 2 |
|
|
|
|
|
def test_config_base_parse_yaml_file03(tmpdir): |
|
""" |
|
API: ConfigBase.parse_yaml_file (#3) |
|
|
|
""" |
|
|
|
t = tmpdir.mkdir("bad-first-entry").join("apprise.yml") |
|
# The first entry is -tag and not <dash><space>tag |
|
# The element is therefore not picked up; This causes us to display |
|
# some warning messages to the screen complaining of this typo yet |
|
# still allowing us to load the URL since it is valid |
|
t.write("""urls: |
|
- pover://nsisxnvnqixq39t0cw54pxieyvtdd9@2jevtmstfg5a7hfxndiybasttxxfku: |
|
-tag: test1 |
|
- pover://rg8ta87qngcrkc6t4qbykxktou0uug@tqs3i88xlufexwl8t4asglt4zp5wfn: |
|
- tag: test2 |
|
- pover://jcqgnlyq2oetea4qg3iunahj8d5ijm@evalvutkhc8ipmz2lcgc70wtsm0qpb: |
|
- tag: test3""") |
|
|
|
# Create ourselves a config object |
|
ac = AppriseConfig(paths=str(t)) |
|
|
|
# The number of configuration files that exist |
|
assert len(ac) == 1 |
|
|
|
# no notifications lines processed is 3 |
|
assert len(ac.servers()) == 3 |
|
|
|
# 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 3 entry as they should have loaded successfully |
|
assert len(a) == 3 |
|
|
|
# No match |
|
assert sum(1 for _ in a.find('no-match')) == 0 |
|
# Match everything |
|
assert sum(1 for _ in a.find('all')) == 3 |
|
# No match for bad entry |
|
assert sum(1 for _ in a.find('test1')) == 0 |
|
# Match test2 entry |
|
assert sum(1 for _ in a.find('test2')) == 1 |
|
# Match test3 entry |
|
assert sum(1 for _ in a.find('test3')) == 1 |
|
# Match test1 or test3 entry; (only matches test3) |
|
assert sum(1 for _ in a.find('test1, test3')) == 1 |
|
|
|
|
|
def test_config_base_parse_yaml_file04(tmpdir): |
|
""" |
|
API: ConfigBase.parse_yaml_file (#4) |
|
|
|
Test the always keyword |
|
|
|
""" |
|
t = tmpdir.mkdir("always-keyword").join("apprise.yml") |
|
t.write("""urls: |
|
- pover://nsisxnvnqixq39t0cw54pxieyvtdd9@2jevtmstfg5a7hfxndiybasttxxfku: |
|
- tag: test1,always |
|
- pover://rg8ta87qngcrkc6t4qbykxktou0uug@tqs3i88xlufexwl8t4asglt4zp5wfn: |
|
- tag: test2 |
|
- pover://jcqgnlyq2oetea4qg3iunahj8d5ijm@evalvutkhc8ipmz2lcgc70wtsm0qpb: |
|
- tag: test3""") |
|
|
|
# Create ourselves a config object |
|
ac = AppriseConfig(paths=str(t)) |
|
|
|
# The number of configuration files that exist |
|
assert len(ac) == 1 |
|
|
|
# no notifications are loaded |
|
assert len(ac.servers()) == 3 |
|
|
|
# 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 3 entry as they should have loaded successfully |
|
assert len(a) == 3 |
|
|
|
# No match still matches `always` keyword |
|
assert sum(1 for _ in a.find('no-match')) == 1 |
|
# Unless we explicitly do not look for that file |
|
assert sum(1 for _ in a.find('no-match', match_always=False)) == 0 |
|
# Match everything |
|
assert sum(1 for _ in a.find('all')) == 3 |
|
# Match test1 entry (also has `always` keyword |
|
assert sum(1 for _ in a.find('test1')) == 1 |
|
assert sum(1 for _ in a.find('test1', match_always=False)) == 1 |
|
# Match test2 entry (and test1 due to always keyword) |
|
assert sum(1 for _ in a.find('test2')) == 2 |
|
assert sum(1 for _ in a.find('test2', match_always=False)) == 1 |
|
# Match test3 entry (and test1 due to always keyword) |
|
assert sum(1 for _ in a.find('test3')) == 2 |
|
assert sum(1 for _ in a.find('test3', match_always=False)) == 1 |
|
# Match test1 or test3 entry |
|
assert sum(1 for _ in a.find('test1, test3')) == 2 |
|
|
|
|
|
def test_apprise_config_template_parse(tmpdir): |
|
""" |
|
API: AppriseConfig parsing of templates |
|
|
|
""" |
|
|
|
# Create ourselves a config object |
|
ac = AppriseConfig() |
|
|
|
t = tmpdir.mkdir("template-testing").join("apprise.yml") |
|
t.write(""" |
|
|
|
tag: |
|
- company |
|
|
|
# A comment line over top of a URL |
|
urls: |
|
- mailto://user:pass@example.com: |
|
- to: user1@gmail.com |
|
cc: test@hotmail.com |
|
|
|
- to: user2@gmail.com |
|
tag: co-worker |
|
""") |
|
|
|
# Create ourselves a config object |
|
ac = AppriseConfig(paths=str(t)) |
|
|
|
# 2 emails to be sent |
|
assert len(ac.servers()) == 2 |
|
|
|
# The below checks are very customized for NotifyMail but just |
|
# test that the content got passed correctly |
|
assert (False, 'user1@gmail.com') in ac[0][0].targets |
|
assert 'test@hotmail.com' in ac[0][0].cc |
|
assert 'company' in ac[0][1].tags |
|
|
|
assert (False, 'user2@gmail.com') in ac[0][1].targets |
|
assert 'company' in ac[0][1].tags |
|
assert 'co-worker' in ac[0][1].tags |
|
|
|
# |
|
# Specifically test _special_token_handler() |
|
# |
|
tokens = { |
|
# This maps to itself (bcc); no change here |
|
'bcc': 'user@test.com', |
|
# This should get mapped to 'targets' |
|
'to': 'user1@abc.com', |
|
# white space and tab is intentionally added to the end to verify we |
|
# do not play/tamper with information |
|
'targets': 'user2@abc.com, user3@abc.com \t', |
|
# If the end user provides a configuration for data we simply don't use |
|
# this isn't a proble... we simply don't touch it either; we leave it |
|
# as is. |
|
'ignore': 'not-used' |
|
} |
|
|
|
result = ConfigBase._special_token_handler('mailto', tokens) |
|
# to gets mapped to targets |
|
assert 'to' not in result |
|
|
|
# bcc is allowed here |
|
assert 'bcc' in result |
|
assert 'targets' in result |
|
# Not used, but also not touched; this entry should still be in our result |
|
# set |
|
assert 'ignore' in result |
|
# We'll concatinate all of our targets together |
|
assert len(result['targets']) == 2 |
|
assert 'user1@abc.com' in result['targets'] |
|
# Content is passed as is |
|
assert 'user2@abc.com, user3@abc.com \t' in result['targets'] |
|
|
|
# We re-do the simmiar test above. The very key difference is the |
|
# `targets` is a list already (it's expected type) so `to` can properly be |
|
# concatinated into the list vs the above (which tries to correct the |
|
# situation) |
|
tokens = { |
|
# This maps to itself (bcc); no change here |
|
'bcc': 'user@test.com', |
|
# This should get mapped to 'targets' |
|
'to': 'user1@abc.com', |
|
# similar to the above test except targets is now a proper |
|
# dictionary allowing the `to` (when translated to `targets`) to get |
|
# appended to it |
|
'targets': ['user2@abc.com', 'user3@abc.com'], |
|
# If the end user provides a configuration for data we simply don't use |
|
# this isn't a proble... we simply don't touch it either; we leave it |
|
# as is. |
|
'ignore': 'not-used' |
|
} |
|
|
|
result = ConfigBase._special_token_handler('mailto', tokens) |
|
# to gets mapped to targets |
|
assert 'to' not in result |
|
|
|
# bcc is allowed here |
|
assert 'bcc' in result |
|
assert 'targets' in result |
|
# Not used, but also not touched; this entry should still be in our result |
|
# set |
|
assert 'ignore' in result |
|
|
|
# Now we'll see the new user added as expected (concatinated into our list) |
|
assert len(result['targets']) == 3 |
|
assert 'user1@abc.com' in result['targets'] |
|
assert 'user2@abc.com' in result['targets'] |
|
assert 'user3@abc.com' in result['targets'] |
|
|
|
# Test providing a list |
|
t.write(""" |
|
# A comment line over top of a URL |
|
urls: |
|
- mailtos://user:pass@example.com: |
|
- smtp: smtp3-dev.google.gmail.com |
|
to: |
|
- John Smith <user1@gmail.com> |
|
- Jason Tater <user2@gmail.com> |
|
- user3@gmail.com |
|
|
|
- to: Henry Fisher <user4@gmail.com>, Jason Archie <user5@gmail.com> |
|
smtp_host: smtp5-dev.google.gmail.com |
|
tag: drinking-buddy |
|
|
|
# provide case where the URL includes some input too |
|
# In both of these cases, the cc and targets (to) get over-ridden |
|
# by values below |
|
- mailtos://user:pass@example.com/arnold@imdb.com/?cc=bill@micro.com/: |
|
to: |
|
- override01@gmail.com |
|
cc: |
|
- override02@gmail.com |
|
|
|
- sinch://: |
|
- spi: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa |
|
token: bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb |
|
|
|
# Test a case where we expect a string, but yaml reads it in as |
|
# a number |
|
from: 10005243890 |
|
to: +1(123)555-1234 |
|
""") |
|
|
|
# Create ourselves a config object |
|
ac = AppriseConfig(paths=str(t)) |
|
|
|
# 2 emails to be sent and 1 Sinch service call |
|
assert len(ac.servers()) == 4 |
|
|
|
# Verify our users got placed into the to |
|
assert len(ac[0][0].targets) == 3 |
|
assert ("John Smith", 'user1@gmail.com') in ac[0][0].targets |
|
assert ("Jason Tater", 'user2@gmail.com') in ac[0][0].targets |
|
assert (False, 'user3@gmail.com') in ac[0][0].targets |
|
assert ac[0][0].smtp_host == 'smtp3-dev.google.gmail.com' |
|
|
|
assert len(ac[0][1].targets) == 2 |
|
assert ("Henry Fisher", 'user4@gmail.com') in ac[0][1].targets |
|
assert ("Jason Archie", 'user5@gmail.com') in ac[0][1].targets |
|
assert 'drinking-buddy' in ac[0][1].tags |
|
assert ac[0][1].smtp_host == 'smtp5-dev.google.gmail.com' |
|
|
|
# Our third test tests cases where some variables are defined inline |
|
# and additional ones are defined below that share the same token space |
|
assert len(ac[0][2].targets) == 1 |
|
assert len(ac[0][2].cc) == 1 |
|
assert (False, 'override01@gmail.com') in ac[0][2].targets |
|
assert 'override02@gmail.com' in ac[0][2].cc |
|
|
|
# Test our Since configuration now: |
|
assert len(ac[0][3].targets) == 1 |
|
assert ac[0][3].service_plan_id == 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' |
|
assert ac[0][3].source == '+10005243890' |
|
assert ac[0][3].targets[0] == '+11235551234'
|
|
|