Improve testing of NotifyDBus, NotifyGnome, and NotifyMacOSX (#689)

This commit is contained in:
Andreas Motl 2022-10-16 19:43:15 +02:00 committed by GitHub
parent c81d2465e4
commit f1836cff84
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 458 additions and 236 deletions

View File

@ -1,8 +1,8 @@
# Base
FROM python:3.10-buster
RUN apt-get update && \
apt-get install -y libdbus-1-dev build-essential musl-dev bash
RUN pip install dbus-python
apt-get install -y libdbus-1-dev libgirepository1.0-dev build-essential musl-dev bash
RUN pip install dbus-python PyGObject
# Apprise Setup
VOLUME ["/apprise"]

View File

@ -1,8 +1,8 @@
# Base
FROM python:3.6-buster
RUN apt-get update && \
apt-get install -y libdbus-1-dev build-essential musl-dev bash
RUN pip install dbus-python
apt-get install -y libdbus-1-dev libgirepository1.0-dev build-essential musl-dev bash
RUN pip install dbus-python PyGObject
# Apprise Setup
VOLUME ["/apprise"]

View File

@ -26,6 +26,7 @@
from __future__ import absolute_import
from __future__ import print_function
import sys
from .NotifyBase import NotifyBase
from ..common import NotifyImageSize
from ..common import NotifyType
@ -77,6 +78,13 @@ try:
NOTIFY_DBUS_SUPPORT_ENABLED = (
LOOP_GLIB is not None or LOOP_QT is not None)
# ImportError: When using gi.repository you must not import static modules
# like "gobject". Please change all occurrences of "import gobject" to
# "from gi.repository import GObject".
# See: https://bugzilla.gnome.org/show_bug.cgi?id=709183
if "gobject" in sys.modules: # pragma: no cover
del sys.modules["gobject"]
try:
# The following is required for Image/Icon loading only
import gi
@ -272,12 +280,9 @@ class NotifyDBus(NotifyBase):
self.x_axis = None
self.y_axis = None
# Track whether or not we want to send an image with our notification
# or not.
# Track whether we want to add an image to the notification.
self.include_image = include_image
return
def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
"""
Perform DBus Notification
@ -286,10 +291,10 @@ class NotifyDBus(NotifyBase):
try:
session = SessionBus(mainloop=MAINLOOP_MAP[self.schema])
except DBusException:
except DBusException as e:
# Handle exception
self.logger.warning('Failed to send DBus notification.')
self.logger.exception('DBus Exception')
self.logger.debug(f'DBus Exception: {e}')
return False
# If there is no title, but there is a body, swap the two to get rid
@ -342,8 +347,8 @@ class NotifyDBus(NotifyBase):
except Exception as e:
self.logger.warning(
"Could not load Gnome notification icon ({}): {}"
.format(icon_path, e))
"Could not load notification icon (%s).", icon_path)
self.logger.debug(f'DBus Exception: {e}')
try:
# Always call throttle() before any remote execution is made
@ -370,9 +375,9 @@ class NotifyDBus(NotifyBase):
self.logger.info('Sent DBus notification.')
except Exception:
except Exception as e:
self.logger.warning('Failed to send DBus notification.')
self.logger.exception('DBus Exception')
self.logger.debug(f'DBus Exception: {e}')
return False
return True

View File

@ -54,8 +54,8 @@ except (ImportError, ValueError, AttributeError):
# be in microsoft windows, or we just don't have the python-gobject
# library available to us (or maybe one we don't support)?
# Alternativey A ValueError will get thrown upon calling
# gi.require_version() if the requested Notify namespace isn't available
# Alternatively, a `ValueError` will get raised upon calling
# gi.require_version() if the requested Notify namespace isn't available.
pass
@ -175,12 +175,9 @@ class NotifyGnome(NotifyBase):
if str(urgency).lower().startswith(k)),
NotifyGnome.template_args['urgency']['default']))
# Track whether or not we want to send an image with our notification
# or not.
# Track whether we want to add an image to the notification.
self.include_image = include_image
return
def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
"""
Perform Gnome Notification
@ -214,15 +211,15 @@ class NotifyGnome(NotifyBase):
except Exception as e:
self.logger.warning(
"Could not load Gnome notification icon ({}): {}"
.format(icon_path, e))
"Could not load notification icon (%s). ", icon_path)
self.logger.debug(f'Gnome Exception: {e}')
notification.show()
self.logger.info('Sent Gnome notification.')
except Exception:
except Exception as e:
self.logger.warning('Failed to send Gnome notification.')
self.logger.exception('Gnome Exception')
self.logger.debug(f'Gnome Exception: {e}')
return False
return True

View File

@ -39,13 +39,15 @@ from ..AppriseLocale import gettext_lazy as _
# Default our global support flag
NOTIFY_MACOSX_SUPPORT_ENABLED = False
# TODO: The module will be easier to test without module-level code.
if platform.system() == 'Darwin':
# Check this is Mac OS X 10.8, or higher
major, minor = platform.mac_ver()[0].split('.')[:2]
# Toggle our enabled flag if verion is correct and executable
# Toggle our enabled flag, if version is correct and executable
# found. This is done in such a way to provide verbosity to the
# end user so they know why it may or may not work for them.
# end user, so they know why it may or may not work for them.
NOTIFY_MACOSX_SUPPORT_ENABLED = \
(int(major) > 10 or (int(major) == 10 and int(minor) >= 8))
@ -95,6 +97,8 @@ class NotifyMacOSX(NotifyBase):
notify_paths = (
'/opt/homebrew/bin/terminal-notifier',
'/usr/local/bin/terminal-notifier',
'/usr/bin/terminal-notifier',
'/bin/terminal-notifier',
)
# Define object templates
@ -126,17 +130,15 @@ class NotifyMacOSX(NotifyBase):
super().__init__(**kwargs)
# Track whether or not we want to send an image with our notification
# or not.
# Track whether we want to add an image to the notification.
self.include_image = include_image
# Acquire the notify path
# Acquire the path to the `terminal-notifier` program.
self.notify_path = next( # pragma: no branch
(p for p in self.notify_paths if os.access(p, os.X_OK)), None)
# Set sound object (no q/a for now)
self.sound = sound
return
def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
"""

View File

@ -1,6 +1,6 @@
version: "3.3"
services:
test.py35:
test.py36:
build:
context: .
dockerfile: Dockerfile.py36

View File

@ -22,24 +22,18 @@
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
from importlib import import_module, reload
import sys
from importlib import reload
def reload_plugin(name):
def reload_plugin(name, replace_in=None):
"""
Reload builtin plugin module, e.g. `NotifyGnome`.
set filename to plugin to be reloaded (for example NotifyGnome.py)
Reload built-in plugin module, e.g. `NotifyGnome`.
The following libraries need to be reloaded to prevent
TypeError: super(type, obj): obj must be an instance or subtype of type
This is better explained in this StackOverflow post:
https://stackoverflow.com/questions/31363311/\
any-way-to-manually-fix-operation-of-\
super-after-ipython-reload-avoiding-ty
Reloading plugin modules is needed when testing module-level code of
notification plugins.
See also https://stackoverflow.com/questions/31363311.
"""
module_name = f"apprise.plugins.{name}"
@ -53,3 +47,10 @@ def reload_plugin(name):
reload(sys.modules['apprise.Apprise'])
reload(sys.modules['apprise.utils'])
reload(sys.modules['apprise'])
# Fix reference to new plugin class in given module.
# Needed for updating the module-level import reference like
# `from apprise.plugins.NotifyMacOSX import NotifyMacOSX`.
if replace_in is not None:
mod = import_module(module_name)
setattr(replace_in, name, getattr(mod, name))

View File

@ -22,40 +22,37 @@
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
import importlib
import logging
import re
import pytest
from unittest import mock
import sys
import types
from unittest.mock import Mock, call, ANY
import pytest
import apprise
from helpers import reload_plugin
from importlib import reload
# Disable logging for a cleaner testing output
import logging
logging.disable(logging.CRITICAL)
# Skip tests when Python environment does not provide the `dbus` package.
if 'dbus' not in sys.modules:
# Environment doesn't allow for dbus
pytest.skip("Skipping dbus-python based tests", allow_module_level=True)
from dbus import DBusException # noqa E402
from apprise.plugins.NotifyDBus import DBusUrgency # noqa E402
from apprise.plugins.NotifyDBus import DBusUrgency, NotifyDBus # noqa E402
@mock.patch('dbus.SessionBus')
@mock.patch('dbus.Interface')
@mock.patch('dbus.ByteArray')
@mock.patch('dbus.Byte')
@mock.patch('dbus.mainloop')
def test_plugin_dbus_general(mock_mainloop, mock_byte, mock_bytearray,
mock_interface, mock_sessionbus):
def setup_glib_environment():
"""
NotifyDBus() General Tests
Setup a heavily mocked Glib environment.
"""
mock_mainloop = Mock()
# Our module base
gi_name = 'gi'
@ -68,15 +65,15 @@ def test_plugin_dbus_general(mock_mainloop, mock_byte, mock_bytearray,
# for the purpose of testing and capture the handling of the
# library when it is missing
del sys.modules[gi_name]
reload(sys.modules['apprise.plugins.NotifyDBus'])
importlib.reload(sys.modules['apprise.plugins.NotifyDBus'])
# We need to fake our dbus environment for testing purposes since
# the gi library isn't available in Travis CI
gi = types.ModuleType(gi_name)
gi.repository = types.ModuleType(gi_name + '.repository')
mock_pixbuf = mock.Mock()
mock_image = mock.Mock()
mock_pixbuf = Mock()
mock_image = Mock()
mock_pixbuf.new_from_file.return_value = mock_image
mock_image.get_width.return_value = 100
@ -92,7 +89,7 @@ def test_plugin_dbus_general(mock_mainloop, mock_byte, mock_bytearray,
gi.repository.GdkPixbuf.Pixbuf = mock_pixbuf
# Emulate require_version function:
gi.require_version = mock.Mock(
gi.require_version = Mock(
name=gi_name + '.require_version')
# Force the fake module to exist
@ -103,18 +100,53 @@ def test_plugin_dbus_general(mock_mainloop, mock_byte, mock_bytearray,
mock_mainloop.qt.DBusQtMainLoop.return_value = True
mock_mainloop.qt.DBusQtMainLoop.side_effect = ImportError
sys.modules['dbus.mainloop.qt'] = mock_mainloop.qt
reload(sys.modules['apprise.plugins.NotifyDBus'])
mock_mainloop.qt.DBusQtMainLoop.side_effect = None
mock_mainloop.glib.NativeMainLoop.return_value = True
mock_mainloop.glib.NativeMainLoop.side_effect = ImportError()
sys.modules['dbus.mainloop.glib'] = mock_mainloop.glib
reload(sys.modules['apprise.plugins.NotifyDBus'])
mock_mainloop.glib.DBusGMainLoop.side_effect = None
mock_mainloop.glib.NativeMainLoop.side_effect = None
reload_plugin('NotifyDBus')
from apprise.plugins.NotifyDBus import NotifyDBus
# When patching something which has a side effect on the module-level code
# of a plugin, make sure to reload it.
current_module = sys.modules[__name__]
reload_plugin('NotifyDBus', replace_in=current_module)
@pytest.fixture
def dbus_environment(mocker):
"""
Fixture to provide a mocked Dbus environment to test case functions.
"""
interface_mock = mocker.patch('dbus.Interface', spec=True,
Notify=Mock())
mocker.patch('dbus.SessionBus', spec=True,
**{"get_object.return_value": interface_mock})
@pytest.fixture
def glib_environment():
"""
Fixture to provide a mocked Glib environment to test case functions.
"""
setup_glib_environment()
@pytest.fixture
def dbus_glib_environment(dbus_environment, glib_environment):
"""
Fixture to provide a mocked Glib/DBus environment to test case functions.
"""
pass
def test_plugin_dbus_general_success(mocker, dbus_glib_environment):
"""
NotifyDBus() general tests
Test class loading using different arguments, provided via URL.
"""
# Create our instance (identify all supported types)
obj = apprise.Apprise.instantiate('dbus://', suppress_exceptions=False)
@ -135,10 +167,6 @@ def test_plugin_dbus_general(mock_mainloop, mock_byte, mock_bytearray,
assert obj.url().startswith('glib://_/')
obj.duration = 0
# Test our class loading using a series of arguments
with pytest.raises(TypeError):
NotifyDBus(**{'schema': 'invalid'})
# Set our X and Y coordinate and try the notification
assert NotifyDBus(
x_axis=0, y_axis=0, **{'schema': 'dbus'})\
@ -245,9 +273,21 @@ def test_plugin_dbus_general(mock_mainloop, mock_byte, mock_bytearray,
title='title', body='body',
notify_type=apprise.NotifyType.INFO) is True
def test_plugin_dbus_general_failure(dbus_glib_environment):
"""
Verify a few failure conditions.
"""
with pytest.raises(TypeError):
obj = apprise.Apprise.instantiate(
'dbus://_/?x=invalid&y=invalid', suppress_exceptions=False)
NotifyDBus(**{'schema': 'invalid'})
with pytest.raises(TypeError):
apprise.Apprise.instantiate('dbus://_/?x=invalid&y=invalid',
suppress_exceptions=False)
def test_plugin_dbus_parse_configuration(dbus_glib_environment):
# Test configuration parsing
content = """
@ -318,104 +358,160 @@ def test_plugin_dbus_general(mock_mainloop, mock_byte, mock_bytearray,
for s in aobj.find(tag='dbus_invalid'):
assert s.urgency == DBusUrgency.NORMAL
# If our underlining object throws for whatever rea on, we will
# gracefully fail
mock_notify = mock.Mock()
mock_interface.return_value = mock_notify
mock_notify.Notify.side_effect = AttributeError()
assert obj.notify(
title='', body='body',
notify_type=apprise.NotifyType.INFO) is False
mock_notify.Notify.side_effect = None
# Test our loading of our icon exception; it will still allow the
# notification to be sent
mock_pixbuf.new_from_file.side_effect = AttributeError()
def test_plugin_dbus_missing_icon(mocker, dbus_glib_environment):
"""
Test exception when loading icon; the notification will still be sent.
"""
# Inject error when loading icon.
gi = importlib.import_module("gi")
gi.repository.GdkPixbuf.Pixbuf.new_from_file.side_effect = \
AttributeError("Something failed")
obj = apprise.Apprise.instantiate('dbus://', suppress_exceptions=False)
logger: Mock = mocker.spy(obj, "logger")
assert obj.notify(
title='title', body='body',
notify_type=apprise.NotifyType.INFO) is True
# Undo our change
mock_pixbuf.new_from_file.side_effect = None
assert logger.mock_calls == [
call.warning('Could not load notification icon (%s). '
'Reason: Something failed', ANY),
call.info('Sent DBus notification.'),
]
def test_plugin_dbus_disabled_plugin(dbus_glib_environment):
"""
Verify notification will not be submitted if plugin is disabled.
"""
obj = apprise.Apprise.instantiate('dbus://', suppress_exceptions=False)
# Test our exception handling during initialization
# Toggle our testing for when we can't send notifications because the
# package has been made unavailable to us
obj.enabled = False
assert obj.notify(
title='title', body='body',
notify_type=apprise.NotifyType.INFO) is False
# Test the setting of a the urgency
def test_plugin_dbus_set_urgency():
"""
Test the setting of an urgency.
"""
NotifyDBus(urgency=0)
#
# We can still notify if the gi library is the only inaccessible
# compontent
#
# Emulate require_version function:
def test_plugin_dbus_gi_missing(dbus_glib_environment):
"""
Verify notification succeeds even if the `gi` package is not available.
"""
# Make `require_version` function raise an ImportError.
gi = importlib.import_module("gi")
gi.require_version.side_effect = ImportError()
reload_plugin('NotifyDBus')
from apprise.plugins.NotifyDBus import NotifyDBus
# Create our instance
# When patching something which has a side effect on the module-level code
# of a plugin, make sure to reload it.
current_module = sys.modules[__name__]
reload_plugin('NotifyDBus', replace_in=current_module)
# Create the instance.
obj = apprise.Apprise.instantiate('glib://', suppress_exceptions=False)
assert isinstance(obj, NotifyDBus) is True
obj.duration = 0
# Test url() call
# Test url() call.
assert isinstance(obj.url(), str) is True
# Our notification succeeds even though the gi library was not loaded
# The notification succeeds even though the gi library was not loaded.
assert obj.notify(
title='title', body='body',
notify_type=apprise.NotifyType.INFO) is True
# Verify this all works in the event a ValueError is also thronw
# out of the call to gi.require_version()
mock_sessionbus.side_effect = DBusException('test')
# Handle Dbus Session Initialization error
assert obj.notify(
title='title', body='body',
notify_type=apprise.NotifyType.INFO) is False
def test_plugin_dbus_gi_require_version_error(dbus_glib_environment):
"""
Verify notification succeeds even if `gi.require_version()` croaks.
"""
# Return side effect to normal
mock_sessionbus.side_effect = None
# Make `require_version` function raise a ValueError.
gi = importlib.import_module("gi")
gi.require_version.side_effect = ValueError("Something failed")
# Emulate require_version function:
gi.require_version.side_effect = ValueError()
reload_plugin('NotifyDBus')
from apprise.plugins.NotifyDBus import NotifyDBus
# When patching something which has a side effect on the module-level code
# of a plugin, make sure to reload it.
current_module = sys.modules[__name__]
reload_plugin('NotifyDBus', replace_in=current_module)
# Create our instance
# Create instance.
obj = apprise.Apprise.instantiate('glib://', suppress_exceptions=False)
assert isinstance(obj, NotifyDBus) is True
obj.duration = 0
# Test url() call
# Test url() call.
assert isinstance(obj.url(), str) is True
# Our notification succeeds even though the gi library was not loaded
# The notification succeeds even though the gi library was not loaded.
assert obj.notify(
title='title', body='body',
notify_type=apprise.NotifyType.INFO) is True
# Force a global import error
_session_bus = sys.modules['dbus']
sys.modules['dbus'] = compile('raise ImportError()', 'dbus', 'exec')
# Reload our modules
reload_plugin('NotifyDBus')
def test_plugin_dbus_module_croaks(mocker, dbus_glib_environment):
"""
Verify plugin is not available when `dbus` module is missing.
"""
# We can no longer instantiate an instance because dbus has been
# officialy marked unavailable and thus the module is marked
# as such
# Make importing `dbus` raise an ImportError.
mocker.patch.dict(
sys.modules, {'dbus': compile('raise ImportError()', 'dbus', 'exec')})
# When patching something which has a side effect on the module-level code
# of a plugin, make sure to reload it.
current_module = sys.modules[__name__]
reload_plugin('NotifyDBus', replace_in=current_module)
# Verify plugin is not available.
obj = apprise.Apprise.instantiate('glib://', suppress_exceptions=False)
assert obj is None
# Since playing with the sys.modules is not such a good idea,
# let's just put our old configuration back:
sys.modules['dbus'] = _session_bus
# Reload our modules
reload_plugin('NotifyDBus')
def test_plugin_dbus_session_croaks(mocker, dbus_glib_environment):
"""
Verify notification fails if DBus croaks.
"""
mocker.patch('dbus.SessionBus', side_effect=DBusException('test'))
setup_glib_environment()
obj = apprise.Apprise.instantiate('dbus://', suppress_exceptions=False)
# Emulate DBus session initialization error.
assert obj.notify(
title='title', body='body',
notify_type=apprise.NotifyType.INFO) is False
def test_plugin_dbus_interface_notify_croaks(mocker):
"""
Fail gracefully if underlying object croaks for whatever reason.
"""
# Inject an error when invoking `dbus.Interface().Notify()`.
mocker.patch('dbus.SessionBus', spec=True)
mocker.patch('dbus.Interface', spec=True,
Notify=Mock(side_effect=AttributeError("Something failed")))
setup_glib_environment()
obj = apprise.Apprise.instantiate('dbus://', suppress_exceptions=False)
assert isinstance(obj, NotifyDBus) is True
logger: Mock = mocker.spy(obj, "logger")
assert obj.notify(
title='title', body='body',
notify_type=apprise.NotifyType.INFO) is False
assert [
call.warning('Failed to send DBus notification. '
'Reason: Something failed'),
call.exception('DBus Exception')
] in logger.mock_calls

View File

@ -22,24 +22,26 @@
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
import importlib
import logging
import sys
import types
from unittest import mock
from unittest.mock import Mock, call, ANY
import pytest
import apprise
from apprise.plugins.NotifyGnome import GnomeUrgency
from apprise.plugins.NotifyGnome import GnomeUrgency, NotifyGnome
from helpers import reload_plugin
# Disable logging for a cleaner testing output
import logging
logging.disable(logging.CRITICAL)
def test_plugin_gnome_general():
def setup_glib_environment():
"""
NotifyGnome() General Checks
Setup a heavily mocked Glib environment.
"""
# Our module base
@ -89,12 +91,30 @@ def test_plugin_gnome_general():
mock_notify.new.return_value = notify_obj
mock_pixbuf.new_from_file.return_value = True
reload_plugin('NotifyGnome')
from apprise.plugins.NotifyGnome import NotifyGnome
# When patching something which has a side effect on the module-level code
# of a plugin, make sure to reload it.
current_module = sys.modules[__name__]
reload_plugin('NotifyGnome', replace_in=current_module)
@pytest.fixture
def glib_environment():
"""
Fixture to provide a mocked Glib environment to test case functions.
"""
setup_glib_environment()
@pytest.fixture
def obj(glib_environment):
"""
Fixture to provide a mocked Apprise instance.
"""
# Create our instance
obj = apprise.Apprise.instantiate('gnome://', suppress_exceptions=False)
assert obj is not None
assert isinstance(obj, NotifyGnome) is True
# Set our duration to 0 to speed up timeouts (for testing)
obj.duration = 0
@ -102,6 +122,14 @@ def test_plugin_gnome_general():
# Check that it found our mocked environments
assert obj.enabled is True
return obj
def test_plugin_gnome_general_success(obj):
"""
NotifyGnome() general checks
"""
# Test url() call
assert isinstance(obj.url(), str) is True
@ -113,9 +141,14 @@ def test_plugin_gnome_general():
assert obj.notify(title='', body='body',
notify_type=apprise.NotifyType.INFO) is True
def test_plugin_gnome_image_success(glib_environment):
"""
Verify using the `image` query argument works as intended.
"""
obj = apprise.Apprise.instantiate(
'gnome://_/?image=True', suppress_exceptions=False)
print("obj:", obj, type(obj))
assert isinstance(obj, NotifyGnome) is True
assert obj.notify(title='title', body='body',
notify_type=apprise.NotifyType.INFO) is True
@ -126,6 +159,12 @@ def test_plugin_gnome_general():
assert obj.notify(title='title', body='body',
notify_type=apprise.NotifyType.INFO) is True
def test_plugin_gnome_priority(glib_environment):
"""
Verify correctness of the `priority` query argument.
"""
# Test Priority (alias of urgency)
obj = apprise.Apprise.instantiate(
'gnome://_/?priority=invalid', suppress_exceptions=False)
@ -148,6 +187,12 @@ def test_plugin_gnome_general():
assert obj.notify(title='title', body='body',
notify_type=apprise.NotifyType.INFO) is True
def test_plugin_gnome_urgency(glib_environment):
"""
Verify correctness of the `urgency` query argument.
"""
# Test Urgeny
obj = apprise.Apprise.instantiate(
'gnome://_/?urgency=invalid', suppress_exceptions=False)
@ -170,6 +215,12 @@ def test_plugin_gnome_general():
assert obj.notify(title='title', body='body',
notify_type=apprise.NotifyType.INFO) is True
def test_plugin_gnome_parse_configuration(obj):
"""
Verify configuration parsing works correctly.
"""
# Test configuration parsing
content = """
urls:
@ -239,43 +290,82 @@ def test_plugin_gnome_general():
for s in aobj.find(tag='gnome_invalid'):
assert s.urgency == GnomeUrgency.NORMAL
# Test our loading of our icon exception; it will still allow the
# notification to be sent
mock_pixbuf.new_from_file.side_effect = AttributeError()
def test_plugin_gnome_missing_icon(mocker, obj):
"""
Verify the notification will be submitted, even if loading the icon fails.
"""
# Inject error when loading icon.
gi = importlib.import_module("gi")
gi.repository.GdkPixbuf.Pixbuf.new_from_file.side_effect = \
AttributeError("Something failed")
logger: Mock = mocker.spy(obj, "logger")
assert obj.notify(title='title', body='body',
notify_type=apprise.NotifyType.INFO) is True
# Undo our change
mock_pixbuf.new_from_file.side_effect = None
assert logger.mock_calls == [
call.warning('Could not load notification icon (%s). '
'Reason: Something failed', ANY),
call.info('Sent Gnome notification.'),
]
# Test our exception handling during initialization
sys.modules['gi.repository.Notify']\
.Notification.new.return_value = None
sys.modules['gi.repository.Notify']\
.Notification.new.side_effect = AttributeError()
assert obj.notify(title='title', body='body',
notify_type=apprise.NotifyType.INFO) is False
# Undo our change
sys.modules['gi.repository.Notify']\
.Notification.new.side_effect = None
# Toggle our testing for when we can't send notifications because the
# package has been made unavailable to us
def test_plugin_gnome_disabled_plugin(obj):
"""
Verify notification will not be submitted if plugin is disabled.
"""
obj.enabled = False
assert obj.notify(title='title', body='body',
notify_type=apprise.NotifyType.INFO) is False
# Test the setting of a the urgency (through priority keyword)
def test_plugin_gnome_set_urgency():
"""
Test the setting of an urgency, through `priority` keyword argument.
"""
NotifyGnome(priority=0)
# Verify this all works in the event a ValueError is also thronw
# out of the call to gi.require_version()
# Emulate require_version function:
gi.require_version.side_effect = ValueError()
reload_plugin('NotifyGnome')
def test_plugin_gnome_gi_croaks():
"""
Verify notification fails when `gi.require_version()` croaks.
"""
# We can now no longer load our instance
# The object internally is marked disabled
# Make `require_version` function raise an error.
try:
gi = importlib.import_module("gi")
except ModuleNotFoundError:
raise pytest.skip("`gi` package not installed")
gi.require_version.side_effect = ValueError("Something failed")
# When patching something which has a side effect on the module-level code
# of a plugin, make sure to reload it.
current_module = sys.modules[__name__]
reload_plugin('NotifyGnome', replace_in=current_module)
# Create instance.
obj = apprise.Apprise.instantiate('gnome://', suppress_exceptions=False)
# The notifier is marked disabled.
assert obj is None
def test_plugin_gnome_notify_croaks(mocker, obj):
"""
Fail gracefully if underlying object croaks for whatever reason.
"""
# Inject an error when invoking `gi.repository.Notify`.
mocker.patch('gi.repository.Notify.Notification.new',
side_effect=AttributeError("Something failed"))
logger: Mock = mocker.spy(obj, "logger")
assert obj.notify(
title='title', body='body',
notify_type=apprise.NotifyType.INFO) is False
assert logger.mock_calls == [
call.warning('Failed to send Gnome notification. '
'Reason: Something failed'),
call.exception('Gnome Exception')
]

View File

@ -22,49 +22,67 @@
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
import logging
import os
from unittest import mock
import sys
from unittest.mock import Mock
from helpers import reload_plugin
import pytest
import apprise
from apprise.plugins.NotifyMacOSX import NotifyMacOSX
from helpers import reload_plugin
# Disable logging for a cleaner testing output
import logging
# Disable logging for a cleaner testing output.
logging.disable(logging.CRITICAL)
@mock.patch('subprocess.Popen')
@mock.patch('platform.system')
@mock.patch('platform.mac_ver')
def test_plugin_macosx_general(mock_macver, mock_system, mock_popen, tmpdir):
@pytest.fixture
def pretend_macos(mocker):
"""
NotifyMacOSX() General Checks
Fixture to simulate a macOS environment.
"""
mocker.patch("platform.system", return_value="Darwin")
mocker.patch("platform.mac_ver", return_value=('10.8', ('', '', ''), ''))
# Create a temporary binary file we can reference
script = tmpdir.join("terminal-notifier")
script.write('')
# Give execute bit
os.chmod(str(script), 0o755)
mock_cmd_response = mock.Mock()
# Reload plugin module, in order to re-run module-level code.
current_module = sys.modules[__name__]
reload_plugin("NotifyMacOSX", replace_in=current_module)
# Set a successful response
mock_cmd_response.returncode = 0
# Simulate a Mac Environment
mock_system.return_value = 'Darwin'
mock_macver.return_value = ('10.8', ('', '', ''), '')
mock_popen.return_value = mock_cmd_response
@pytest.fixture
def terminal_notifier(mocker, tmp_path):
"""
Fixture for providing a surrogate for the `terminal-notifier` program.
"""
notifier_program = tmp_path.joinpath("terminal-notifier")
notifier_program.write_text('#!/bin/sh\n\necho hello')
# Ensure our environment is loaded with this configuration
reload_plugin('NotifyMacOSX')
from apprise.plugins.NotifyMacOSX import NotifyMacOSX
# Set execute bit.
os.chmod(notifier_program, 0o755)
# Point our object to our new temporary existing file
NotifyMacOSX.notify_paths = (str(script), )
# Make the notifier use the temporary file instead of `terminal-notifier`.
mocker.patch("apprise.plugins.NotifyMacOSX.NotifyMacOSX.notify_paths",
(str(notifier_program),))
yield notifier_program
@pytest.fixture
def macos_notify_environment(pretend_macos, terminal_notifier):
"""
Fixture to bundle general test case setup.
Use this fixture if you don't need access to the individual members.
"""
pass
def test_plugin_macosx_general_success(macos_notify_environment):
"""
NotifyMacOSX() general checks
"""
obj = apprise.Apprise.instantiate(
'macosx://_/?image=True', suppress_exceptions=False)
@ -103,68 +121,81 @@ def test_plugin_macosx_general(mock_macver, mock_system, mock_popen, tmpdir):
assert obj.notify(title='title', body='body',
notify_type=apprise.NotifyType.INFO) is True
# If our binary is inacccessible (or not executable), we can
# no longer send our notifications
os.chmod(str(script), 0o644)
def test_plugin_macosx_terminal_notifier_not_executable(
pretend_macos, terminal_notifier):
"""
When the `terminal-notifier` program is inaccessible or not executable,
we are unable to send notifications.
"""
obj = apprise.Apprise.instantiate('macosx://', suppress_exceptions=False)
# Unset the executable bit.
os.chmod(terminal_notifier, 0o644)
assert obj.notify(title='title', body='body',
notify_type=apprise.NotifyType.INFO) is False
# Restore permission
os.chmod(str(script), 0o755)
# But now let's disrupt the path location
def test_plugin_macosx_terminal_notifier_invalid(macos_notify_environment):
"""
When the `terminal-notifier` program is wrongly addressed,
notifications should fail.
"""
obj = apprise.Apprise.instantiate('macosx://', suppress_exceptions=False)
# Let's disrupt the path location.
obj.notify_path = 'invalid_missing-file'
assert not os.path.isfile(obj.notify_path)
assert obj.notify(title='title', body='body',
notify_type=apprise.NotifyType.INFO) is False
# Test cases where the script just flat out fails
mock_cmd_response.returncode = 1
obj = apprise.Apprise.instantiate(
'macosx://', suppress_exceptions=False)
def test_plugin_macosx_terminal_notifier_croaks(
mocker, macos_notify_environment):
"""
When the `terminal-notifier` program croaks on execution,
notifications should fail.
"""
# Emulate a failing program.
mocker.patch("subprocess.Popen", return_value=Mock(returncode=1))
obj = apprise.Apprise.instantiate('macosx://', suppress_exceptions=False)
assert isinstance(obj, NotifyMacOSX) is True
assert obj.notify(title='title', body='body',
notify_type=apprise.NotifyType.INFO) is False
# Restore script return value
mock_cmd_response.returncode = 0
# Test case where we simply aren't on a mac
mock_system.return_value = 'Linux'
reload_plugin('NotifyMacOSX')
def test_plugin_macosx_pretend_linux(mocker, pretend_macos):
"""
The notification object is disabled when pretending to run on Linux.
"""
# Point our object to our new temporary existing file
NotifyMacOSX.notify_paths = (str(script), )
# When patching something which has a side effect on the module-level code
# of a plugin, make sure to reload it.
mocker.patch("platform.system", return_value="Linux")
reload_plugin("NotifyMacOSX")
# Our object is disabled
obj = apprise.Apprise.instantiate(
'macosx://_/?sound=default', suppress_exceptions=False)
# Our object is disabled.
obj = apprise.Apprise.instantiate('macosx://', suppress_exceptions=False)
assert obj is None
# Restore mac environment
mock_system.return_value = 'Darwin'
# Now we must be Mac OS v10.8 or higher...
mock_macver.return_value = ('10.7', ('', '', ''), '')
reload_plugin('NotifyMacOSX')
@pytest.mark.parametrize("macos_version", ["9.12", "10.7"])
def test_plugin_macosx_pretend_old_macos(mocker, macos_version):
"""
The notification object is disabled when pretending to run on older macOS.
"""
# Point our object to our new temporary existing file
NotifyMacOSX.notify_paths = (str(script), )
# When patching something which has a side effect on the module-level code
# of a plugin, make sure to reload it.
mocker.patch("platform.mac_ver",
return_value=(macos_version, ('', '', ''), ''))
reload_plugin("NotifyMacOSX")
obj = apprise.Apprise.instantiate(
'macosx://_/?sound=default', suppress_exceptions=False)
assert obj is None
# A newer environment to test edge case where this is tested
mock_macver.return_value = ('9.12', ('', '', ''), '')
reload_plugin('NotifyMacOSX')
# Point our object to our new temporary existing file
NotifyMacOSX.notify_paths = (str(script), )
# This is just to test that the the minor (in this case .12)
# is only weighed with respect to the major number as wel
# with respect to the versioning
obj = apprise.Apprise.instantiate(
'macosx://_/?sound=default', suppress_exceptions=False)
obj = apprise.Apprise.instantiate('macosx://', suppress_exceptions=False)
assert obj is None