qutebrowser/tests/unit/browser/test_notification.py

282 lines
8.4 KiB
Python

# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) <mail@qutebrowser.org>
#
# SPDX-License-Identifier: GPL-3.0-or-later
"""Unit tests for notification support."""
import logging
import itertools
import inspect
from typing import Any, Optional, TYPE_CHECKING
import pytest
from qutebrowser.qt.core import pyqtSignal, pyqtSlot, QUrl, QObject
from qutebrowser.qt.gui import QImage
from qutebrowser.qt.dbus import QDBusMessage, QDBus, QDBusConnection
pytest.importorskip("qutebrowser.qt.webenginecore")
if TYPE_CHECKING:
from qutebrowser.qt.webenginecore import QWebEngineNotification
from qutebrowser.config import configdata
from qutebrowser.misc import objects
from qutebrowser.browser.webengine import notification
class FakeDBusMessage:
def __init__(
self,
signature: str,
*arguments: Any,
typ: QDBusMessage.MessageType = QDBusMessage.MessageType.ReplyMessage,
error_name: Optional[str] = None,
) -> None:
self._signature = signature
self._arguments = arguments
self._type = typ
self._error_name = error_name
def arguments(self) -> list[Any]:
return self._arguments
def signature(self) -> str:
return self._signature
def type(self) -> QDBusMessage.MessageType:
return self._type
def errorName(self) -> str:
assert self._error_name is not None
return self._error_name
def errorMessage(self):
assert self._error_name is not None
return f"error: {self._error_name}"
@classmethod
def create_error(cls, error_name: str) -> "FakeDBusMessage":
return cls(
"s",
f"error argument: {error_name}",
typ=QDBusMessage.MessageType.ErrorMessage,
error_name=error_name,
)
class FakeDBusInterface:
CAPABILITIES_REPLY = FakeDBusMessage("as", ["actions"])
SERVER_INFO_REPLY = FakeDBusMessage(
"ssss",
"fake notification server", # name
"qutebrowser", # vendor
"v0.0.1", # version
"1.2", # spec version
)
def __init__(
self,
service: str,
path: str,
interface: str,
bus: QDBusConnection,
) -> None:
assert service.startswith(notification.DBusNotificationAdapter.TEST_SERVICE)
assert path == notification.DBusNotificationAdapter.PATH
assert interface == notification.DBusNotificationAdapter.INTERFACE
self.notify_reply = None
def isValid(self) -> bool:
return True
def call(self, mode: QDBus.CallMode, method: str, *args: Any) -> FakeDBusMessage:
meth = getattr(self, f"_call_{method}")
return meth(*args)
def _call_GetCapabilities(self) -> FakeDBusMessage:
return self.CAPABILITIES_REPLY
def _call_GetServerInformation(self) -> FakeDBusMessage:
return self.SERVER_INFO_REPLY
def _call_Notify(
self,
appname: str,
replaces_id: int,
icon: str,
title: str,
body: str,
actions: list[str],
hints: dict[str, Any],
timeout: int,
) -> FakeDBusMessage:
assert self.notify_reply is not None
return self.notify_reply
class FakeWebEngineNotification(QObject):
closed = pyqtSignal()
def origin(self) -> QUrl:
return QUrl("https://example.org")
def icon(self) -> QImage:
return QImage()
def title(self) -> str:
return "notification title"
def message(self) -> str:
return "notification message"
def tag(self) -> None:
return None
def show(self) -> None:
pass
@pytest.fixture
def fake_notification():
return FakeWebEngineNotification()
def _get_notification_adapters():
return [value for _name, value in inspect.getmembers(notification, lambda obj: (
inspect.isclass(obj) and
issubclass(obj, notification.AbstractNotificationAdapter) and
obj is not notification.AbstractNotificationAdapter
))]
@pytest.mark.parametrize("klass", _get_notification_adapters())
def test_name_attribute(klass, configdata_init):
values = configdata.DATA["content.notifications.presenter"].typ.valid_values
assert klass.NAME not in {"auto", "qt"}
assert klass.NAME in values
class FakeNotificationAdapter(notification.AbstractNotificationAdapter):
NAME = "fake"
def __init__(self) -> None:
super().__init__()
self.presented = []
self.id_gen = itertools.count(1)
def present(
self,
qt_notification: "QWebEngineNotification", *,
replaces_id: Optional[int],
) -> int:
self.presented.append(qt_notification)
return next(self.id_gen)
@pyqtSlot(int)
def on_web_closed(self, notification_id: int) -> None:
"""Called when a notification was closed by the website."""
raise NotImplementedError
@pytest.mark.linux
class TestDBus:
NO_REPLY_ERROR = FakeDBusMessage.create_error("org.freedesktop.DBus.Error.NoReply")
FATAL_ERROR = FakeDBusMessage.create_error("test")
@pytest.fixture
def dbus_adapter_patches(self, monkeypatch, config_stub):
monkeypatch.setattr(objects, "debug_flags", ["test-notification-service"])
monkeypatch.setattr(notification, "QDBusInterface", FakeDBusInterface)
@pytest.fixture
def dbus_adapter(self, dbus_adapter_patches):
return notification.DBusNotificationAdapter()
@pytest.fixture
def dbus_presenter(self, dbus_adapter_patches, monkeypatch):
monkeypatch.setattr(
notification.NotificationBridgePresenter,
"_get_adapter_candidates",
lambda _self, _setting: [
notification.DBusNotificationAdapter,
FakeNotificationAdapter,
],
)
return notification.NotificationBridgePresenter()
def test_notify_fatal_error(self, dbus_adapter, fake_notification):
dbus_adapter.interface.notify_reply = self.FATAL_ERROR
with pytest.raises(notification.DBusError):
dbus_adapter.present(fake_notification, replaces_id=None)
def test_notify_fatal_error_presenter(self, dbus_presenter, fake_notification):
dbus_presenter._init_adapter()
dbus_presenter._adapter.interface.notify_reply = self.FATAL_ERROR
with pytest.raises(notification.DBusError):
dbus_presenter.present(fake_notification)
def test_notify_non_fatal_error(self, qtbot, dbus_adapter, fake_notification):
dbus_adapter.interface.notify_reply = self.NO_REPLY_ERROR
with qtbot.wait_signal(dbus_adapter.error) as blocker:
dbus_adapter.present(fake_notification, replaces_id=None)
assert blocker.args == [f"error: {self.NO_REPLY_ERROR.errorName()}"]
def test_notify_non_fatal_error_presenter(
self,
dbus_presenter,
fake_notification,
caplog,
):
dbus_presenter._init_adapter()
dbus_presenter._adapter.interface.notify_reply = self.NO_REPLY_ERROR
with caplog.at_level(logging.ERROR):
dbus_presenter.present(fake_notification)
message = (
'Notification error from libnotify adapter: '
f'{self.NO_REPLY_ERROR.errorMessage()}'
)
assert message in caplog.messages
assert dbus_presenter._adapter is None # adapter dropped
@pytest.mark.parametrize("error, exctype", [
(NO_REPLY_ERROR, notification.DBusError),
(FATAL_ERROR, notification.Error),
])
def test_capabilities_error(
self,
dbus_adapter_patches,
monkeypatch,
error,
exctype,
):
monkeypatch.setattr(FakeDBusInterface, "CAPABILITIES_REPLY", error)
with pytest.raises(exctype):
notification.DBusNotificationAdapter()
@pytest.mark.parametrize("error", [NO_REPLY_ERROR, FATAL_ERROR],
ids=lambda e: e.errorName())
def test_capabilities_error_presenter(
self,
dbus_presenter,
fake_notification,
monkeypatch,
caplog,
error,
):
monkeypatch.setattr(FakeDBusInterface, "CAPABILITIES_REPLY", error)
dbus_presenter.present(fake_notification)
message = (
'Failed to initialize libnotify notification adapter: '
f'{error.errorName()}: {error.errorMessage()}'
)
assert message in caplog.messages
assert isinstance(dbus_presenter._adapter, FakeNotificationAdapter)
assert dbus_presenter._adapter.presented == [fake_notification]