Some checks failed
Docker. / Ubuntu (push) Has been cancelled
User-agent updater. / User-agent (push) Failing after 15s
Lock Threads / lock (push) Failing after 10s
Waiting for answer. / waiting-for-answer (push) Failing after 22s
Needs user action. / needs-user-action (push) Failing after 8s
Can't reproduce. / cant-reproduce (push) Failing after 8s
Close stale issues and PRs / stale (push) Has been cancelled
532 lines
16 KiB
Python
532 lines
16 KiB
Python
# SPDX-License-Identifier: LGPL-2.1-or-later
|
|
#
|
|
# This file is formatted with Python Black
|
|
|
|
import tests as xdp
|
|
|
|
import pytest
|
|
import tempfile
|
|
import os
|
|
from pathlib import Path
|
|
from gi.repository import GLib, Gio
|
|
|
|
SVG_IMAGE_DATA = """<?xml version="1.0" encoding="UTF-8"?>
|
|
<svg xmlns="http://www.w3.org/2000/svg" height="16px" width="16px"/>
|
|
"""
|
|
|
|
SOUND_DATA = (
|
|
b"\x52\x49\x46\x46\x24\x00\x00\x00\x57\x41\x56\x45" +
|
|
b"\x66\x6d\x74\x20\x10\x00\x00\x00\x01\x00\x01\x00" +
|
|
b"\x44\xac\x00\x00\x88\x58\x01\x00\x02\x00\x10\x00" +
|
|
b"\x64\x61\x74\x61\x00\x00\x00\x00"
|
|
) # fmt: skip
|
|
|
|
|
|
SUPPORTED_OPTIONS = {
|
|
"foo": "bar",
|
|
}
|
|
|
|
NOTIFICATION_BASIC = {
|
|
"title": GLib.Variant("s", "title"),
|
|
"body": GLib.Variant("s", "test notification body"),
|
|
"priority": GLib.Variant("s", "normal"),
|
|
"default-action": GLib.Variant("s", "test-action"),
|
|
}
|
|
|
|
NOTIFICATION_BUTTONS = {
|
|
"title": GLib.Variant("s", "test notification 2"),
|
|
"body": GLib.Variant("s", "test notification body 2"),
|
|
"priority": GLib.Variant("s", "low"),
|
|
"default-action": GLib.Variant("s", "test-action"),
|
|
"buttons": GLib.Variant(
|
|
"aa{sv}",
|
|
[
|
|
{
|
|
"label": GLib.Variant("s", "button1"),
|
|
"action": GLib.Variant("s", "action1"),
|
|
},
|
|
{
|
|
"label": GLib.Variant("s", "button2"),
|
|
"action": GLib.Variant("s", "action2"),
|
|
},
|
|
],
|
|
),
|
|
}
|
|
|
|
|
|
@pytest.fixture
|
|
def required_templates():
|
|
return {
|
|
"notification": {
|
|
"SupportedOptions": SUPPORTED_OPTIONS,
|
|
},
|
|
}
|
|
|
|
|
|
class NotificationPortal(xdp.GDBusIface):
|
|
def __init__(self):
|
|
super().__init__(
|
|
"org.freedesktop.portal.Desktop",
|
|
"/org/freedesktop/portal/desktop",
|
|
"org.freedesktop.portal.Notification",
|
|
)
|
|
|
|
def AddNotification(self, id, notification, fds=[]):
|
|
return self._call(
|
|
"AddNotification",
|
|
GLib.Variant("(sa{sv})", (id, notification)),
|
|
fds,
|
|
)
|
|
|
|
def RemoveNotification(self, id):
|
|
return self._call(
|
|
"RemoveNotification",
|
|
GLib.Variant("(s)", (id,)),
|
|
)
|
|
|
|
|
|
class TestNotification:
|
|
def check_notification(
|
|
self, dbus_con, app_id, id, notification_in, notification_expected
|
|
):
|
|
notification_intf = NotificationPortal()
|
|
mock_intf = xdp.get_mock_iface(dbus_con)
|
|
|
|
method_calls = mock_intf.GetMethodCalls("AddNotification")
|
|
backend_calls = len(method_calls)
|
|
|
|
notification_intf.AddNotification(id, notification_in)
|
|
|
|
# Check the impl portal was called with the right args
|
|
method_calls = mock_intf.GetMethodCalls("AddNotification")
|
|
assert len(method_calls) == backend_calls + 1
|
|
_, args = method_calls[-1]
|
|
assert args[0] == app_id
|
|
assert args[1] == id
|
|
|
|
mock_notification = args[2]
|
|
assert (
|
|
mock_notification == GLib.Variant("a{sv}", notification_expected).unpack()
|
|
)
|
|
|
|
def test_version(self, portals, dbus_con):
|
|
xdp.check_version(dbus_con, "Notification", 2)
|
|
|
|
def test_basic(self, portals, dbus_con, app_id):
|
|
self.check_notification(
|
|
dbus_con,
|
|
app_id,
|
|
"test1",
|
|
NOTIFICATION_BASIC,
|
|
NOTIFICATION_BASIC,
|
|
)
|
|
|
|
def test_remove(self, portals, dbus_con, app_id):
|
|
notification_intf = NotificationPortal()
|
|
mock_intf = xdp.get_mock_iface(dbus_con)
|
|
|
|
id = "test1"
|
|
|
|
notification_intf.AddNotification(id, NOTIFICATION_BASIC)
|
|
method_calls = mock_intf.GetMethodCalls("AddNotification")
|
|
assert len(method_calls) == 1
|
|
_, args = method_calls[-1]
|
|
assert args[0] == app_id
|
|
assert args[1] == id
|
|
|
|
notification_intf.RemoveNotification(id)
|
|
method_calls = mock_intf.GetMethodCalls("RemoveNotification")
|
|
assert len(method_calls) == 1
|
|
_, args = method_calls[-1]
|
|
assert args[0] == app_id
|
|
assert args[1] == id
|
|
|
|
def test_buttons(self, portals, dbus_con, app_id):
|
|
self.check_notification(
|
|
dbus_con,
|
|
app_id,
|
|
"test1",
|
|
NOTIFICATION_BUTTONS,
|
|
NOTIFICATION_BUTTONS,
|
|
)
|
|
|
|
def test_markup(self, portals, dbus_con, app_id):
|
|
bodies = [
|
|
(
|
|
"test <b>notification</b> body <i>italic</i>",
|
|
"test <b>notification</b> body <i>italic</i>",
|
|
),
|
|
(
|
|
'test <a href="https://example.com"><b>Some link</b></a>',
|
|
'test <a href="https://example.com"><b>Some link</b></a>',
|
|
),
|
|
(
|
|
"<html>",
|
|
"<html>",
|
|
),
|
|
(
|
|
'<a href="https://xkcd.com/327/#"><html>"></a>',
|
|
'<a href="https://xkcd.com/327/#"><html>"></a>',
|
|
),
|
|
(
|
|
"test \n newline \n\n some more space \n with trailing space ",
|
|
"test newline some more space with trailing space",
|
|
),
|
|
(
|
|
"test <custom> tag </custom>",
|
|
"test tag",
|
|
),
|
|
(
|
|
"test <b>notification<b> body",
|
|
False,
|
|
),
|
|
(
|
|
"<b>foo<i>bar</b></i>",
|
|
False,
|
|
),
|
|
(
|
|
"test <markup><i>notification</i><markup> body",
|
|
False,
|
|
),
|
|
]
|
|
|
|
i = 0
|
|
for body_in, body_expected in bodies:
|
|
notification_in = NOTIFICATION_BASIC.copy()
|
|
notification_in["markup-body"] = GLib.Variant("s", body_in)
|
|
|
|
notification_expected = NOTIFICATION_BASIC.copy()
|
|
if body_expected:
|
|
notification_expected["markup-body"] = GLib.Variant("s", body_expected)
|
|
|
|
try:
|
|
self.check_notification(
|
|
dbus_con,
|
|
app_id,
|
|
f"test{i}",
|
|
notification_in,
|
|
notification_expected,
|
|
)
|
|
assert body_expected
|
|
except GLib.GError as e:
|
|
assert "invalid markup-body" in e.message
|
|
|
|
i += 1
|
|
|
|
def test_bad_arg(self, portals, dbus_con, app_id):
|
|
notification = NOTIFICATION_BASIC.copy()
|
|
notification["bodx"] = GLib.Variant("s", "Xtest")
|
|
|
|
self.check_notification(
|
|
dbus_con,
|
|
app_id,
|
|
"test1",
|
|
notification,
|
|
NOTIFICATION_BASIC,
|
|
)
|
|
|
|
def test_bad_priority(self, portals, dbus_con, app_id):
|
|
notification = NOTIFICATION_BASIC.copy()
|
|
notification["priority"] = GLib.Variant("s", "invalid")
|
|
|
|
try:
|
|
self.check_notification(
|
|
dbus_con,
|
|
app_id,
|
|
"test1",
|
|
notification,
|
|
notification,
|
|
)
|
|
assert False, "This statement should not be reached"
|
|
except GLib.GError as e:
|
|
assert "invalid not a priority" in e.message
|
|
|
|
def test_bad_button(self, portals, dbus_con, app_id):
|
|
notification = NOTIFICATION_BUTTONS.copy()
|
|
notification["buttons"] = GLib.Variant(
|
|
"aa{sv}",
|
|
[
|
|
{
|
|
"labex": GLib.Variant("s", "button1"),
|
|
"action": GLib.Variant("s", "action1"),
|
|
},
|
|
],
|
|
)
|
|
|
|
try:
|
|
self.check_notification(
|
|
dbus_con,
|
|
app_id,
|
|
"test1",
|
|
notification,
|
|
notification,
|
|
)
|
|
assert False, "This statement should not be reached"
|
|
except GLib.GError as e:
|
|
assert "invalid button" in e.message
|
|
|
|
def test_display_hint(self, portals, dbus_con, app_id):
|
|
notification = NOTIFICATION_BASIC.copy()
|
|
notification["display-hint"] = GLib.Variant(
|
|
"as",
|
|
[
|
|
"transient",
|
|
"show-as-new",
|
|
],
|
|
)
|
|
|
|
self.check_notification(
|
|
dbus_con,
|
|
app_id,
|
|
"test1",
|
|
notification,
|
|
notification,
|
|
)
|
|
|
|
notification = NOTIFICATION_BASIC.copy()
|
|
notification["display-hint"] = GLib.Variant(
|
|
"as",
|
|
[
|
|
"unsupported-hint",
|
|
],
|
|
)
|
|
|
|
try:
|
|
self.check_notification(
|
|
dbus_con,
|
|
app_id,
|
|
"test1",
|
|
notification,
|
|
notification,
|
|
)
|
|
assert False, "This statement should not be reached"
|
|
except GLib.GError as e:
|
|
assert "not a display-hint" in e.message
|
|
|
|
def test_category(self, portals, dbus_con, app_id):
|
|
notification = NOTIFICATION_BASIC.copy()
|
|
notification["category"] = GLib.Variant("s", "im.received")
|
|
|
|
self.check_notification(
|
|
dbus_con,
|
|
app_id,
|
|
"test1",
|
|
notification,
|
|
notification,
|
|
)
|
|
|
|
notification = NOTIFICATION_BASIC.copy()
|
|
notification["category"] = GLib.Variant("s", "x-vendor.custom")
|
|
|
|
self.check_notification(
|
|
dbus_con,
|
|
app_id,
|
|
"test1",
|
|
notification,
|
|
notification,
|
|
)
|
|
|
|
notification = NOTIFICATION_BASIC.copy()
|
|
notification["category"] = GLib.Variant("s", "unsupported-type")
|
|
|
|
try:
|
|
self.check_notification(
|
|
dbus_con,
|
|
app_id,
|
|
"test1",
|
|
notification,
|
|
notification,
|
|
)
|
|
assert False, "This statement should not be reached"
|
|
except GLib.GError as e:
|
|
assert "not a supported category" in e.message
|
|
|
|
def test_supported_options(self, portals, dbus_con, app_id):
|
|
properties_intf = xdp.get_iface(dbus_con, "org.freedesktop.DBus.Properties")
|
|
|
|
options = properties_intf.Get(
|
|
"org.freedesktop.portal.Notification", "SupportedOptions"
|
|
)
|
|
|
|
assert options == SUPPORTED_OPTIONS
|
|
|
|
def test_icon_themed(self, portals, dbus_con, app_id):
|
|
notification_intf = NotificationPortal()
|
|
icon = Gio.ThemedIcon.new("test-icon-symbolic")
|
|
|
|
notification = NOTIFICATION_BASIC.copy()
|
|
notification["icon"] = icon.serialize()
|
|
|
|
notification_intf.AddNotification("test1", notification)
|
|
|
|
def test_icon_bytes(self, portals, dbus_con, app_id):
|
|
notification_intf = NotificationPortal()
|
|
bytes = GLib.Bytes.new(SVG_IMAGE_DATA.encode("utf-8"))
|
|
icon = Gio.BytesIcon.new(bytes)
|
|
|
|
notification = NOTIFICATION_BASIC.copy()
|
|
notification["icon"] = icon.serialize()
|
|
|
|
notification_intf.AddNotification("test1", notification)
|
|
|
|
def test_icon_file(self, portals, dbus_con, app_id):
|
|
notification_intf = NotificationPortal()
|
|
fd, file_path = tempfile.mkstemp(prefix="notification_icon_", dir=Path.home())
|
|
os.write(fd, SVG_IMAGE_DATA.encode("utf-8"))
|
|
|
|
file = Gio.File.new_for_path(file_path)
|
|
icon = Gio.FileIcon.new(file)
|
|
|
|
notification = NOTIFICATION_BASIC.copy()
|
|
notification["icon"] = icon.serialize()
|
|
|
|
notification = {
|
|
"title": GLib.Variant("s", "title"),
|
|
"icon": icon.serialize(),
|
|
}
|
|
|
|
notification_intf.AddNotification("test1", notification)
|
|
|
|
def test_icon_bad(self, portals, dbus_con, app_id):
|
|
notification_intf = NotificationPortal()
|
|
|
|
notification = NOTIFICATION_BASIC.copy()
|
|
|
|
bad_icons = [
|
|
GLib.Variant("(sv)", ["themed", GLib.Variant("s", "test-icon-symbolic")]),
|
|
GLib.Variant(
|
|
"(sv)",
|
|
["bytes", GLib.Variant("as", ["test-icon-symbolic", "test-icon"])],
|
|
),
|
|
GLib.Variant("(sv)", ["file-descriptor", GLib.Variant("s", "")]),
|
|
GLib.Variant("(sv)", ["file-descriptor", GLib.Variant("h", 0)]),
|
|
]
|
|
|
|
for icon in bad_icons:
|
|
notification["icon"] = icon
|
|
try:
|
|
notification_intf.AddNotification("test1", notification)
|
|
assert False, "This statement should not be reached"
|
|
except GLib.GError as e:
|
|
assert e.matches(Gio.io_error_quark(), Gio.IOErrorEnum.DBUS_ERROR)
|
|
|
|
def test_sound_simple(self, portals, dbus_con, app_id):
|
|
notification = NOTIFICATION_BASIC.copy()
|
|
notification["sound"] = GLib.Variant("s", "default")
|
|
|
|
self.check_notification(
|
|
dbus_con,
|
|
app_id,
|
|
"test1",
|
|
notification,
|
|
notification,
|
|
)
|
|
|
|
notification = NOTIFICATION_BASIC.copy()
|
|
notification["sound"] = GLib.Variant("s", "silent")
|
|
|
|
self.check_notification(
|
|
dbus_con,
|
|
app_id,
|
|
"test1",
|
|
notification,
|
|
notification,
|
|
)
|
|
|
|
notification = NOTIFICATION_BASIC.copy()
|
|
notification["sound"] = GLib.Variant("s", "bad")
|
|
|
|
try:
|
|
self.check_notification(
|
|
dbus_con,
|
|
app_id,
|
|
"test1",
|
|
notification,
|
|
notification,
|
|
)
|
|
assert False, "This statement should not be reached"
|
|
except GLib.GError as e:
|
|
assert "invalid sound: invalid option" in e.message
|
|
|
|
def test_sound_file(self, portals, dbus_con, app_id):
|
|
notification_intf = NotificationPortal()
|
|
mock_intf = xdp.get_mock_iface(dbus_con)
|
|
|
|
fd, file_path = tempfile.mkstemp(prefix="notification_sound_", dir=Path.home())
|
|
os.write(fd, SOUND_DATA)
|
|
|
|
file = Gio.File.new_for_path(file_path)
|
|
|
|
notification = NOTIFICATION_BASIC.copy()
|
|
notification["sound"] = GLib.Variant(
|
|
"(sv)",
|
|
(
|
|
"file",
|
|
GLib.Variant("s", file.get_uri()),
|
|
),
|
|
)
|
|
|
|
notification_intf.AddNotification("test1", notification)
|
|
|
|
method_calls = mock_intf.GetMethodCalls("AddNotification")
|
|
assert len(method_calls) == 1
|
|
_, args = method_calls[-1]
|
|
mock_notification = args[2]
|
|
|
|
assert "sound" not in mock_notification
|
|
|
|
def test_sound_fd(self, portals, dbus_con, app_id):
|
|
notification_intf = NotificationPortal()
|
|
mock_intf = xdp.get_mock_iface(dbus_con)
|
|
|
|
fd = os.memfd_create("notification_sound_test", os.MFD_ALLOW_SEALING)
|
|
os.write(fd, SOUND_DATA)
|
|
|
|
notification = NOTIFICATION_BASIC.copy()
|
|
notification["sound"] = GLib.Variant(
|
|
"(sv)",
|
|
(
|
|
"file-descriptor",
|
|
GLib.Variant("h", 0),
|
|
),
|
|
)
|
|
|
|
notification_intf.AddNotification("test1", notification, [fd])
|
|
|
|
method_calls = mock_intf.GetMethodCalls("AddNotification")
|
|
assert len(method_calls) == 1
|
|
_, args = method_calls[-1]
|
|
mock_notification = args[2]
|
|
|
|
assert mock_notification["sound"][0] == "file-descriptor"
|
|
mock_fd = mock_notification["sound"][1]
|
|
mock_fd = mock_fd.take()
|
|
|
|
os.lseek(fd, 0, os.SEEK_SET)
|
|
fd_contents = os.read(mock_fd, 1000)
|
|
assert fd_contents == SOUND_DATA
|
|
|
|
os.close(mock_fd)
|
|
os.close(fd)
|
|
|
|
def test_sound_bad(self, portals, dbus_con, app_id):
|
|
notification_intf = NotificationPortal()
|
|
|
|
notification = NOTIFICATION_BASIC.copy()
|
|
|
|
bad_sounds = [
|
|
# bad type
|
|
GLib.Variant("(sv)", ["file-descriptor", GLib.Variant("s", "")]),
|
|
# not sending the FD for the handle
|
|
GLib.Variant("(sv)", ["file-descriptor", GLib.Variant("h", 13)]),
|
|
]
|
|
|
|
for sound in bad_sounds:
|
|
notification["sound"] = sound
|
|
try:
|
|
notification_intf.AddNotification("test1", notification)
|
|
assert False, "This statement should not be reached"
|
|
except GLib.GError as e:
|
|
assert e.matches(Gio.io_error_quark(), Gio.IOErrorEnum.DBUS_ERROR)
|
|
pass
|