init
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

This commit is contained in:
allhaileris
2026-02-16 15:50:16 +03:00
commit afb81b8278
13816 changed files with 3689732 additions and 0 deletions

View File

@@ -0,0 +1,105 @@
xdg-desktop-portal test suite
=============================
## Unit tests
This directory contains a number of unit tests. The tests are written in C and
are using the glib testing framework (https://docs.gtk.org/glib/testing.html).
The files follow the pattern `test-$NAME.c` and are compiled by meson. The tests
can be run with `meson test --suite unit`.
## Integration tests
The integration tests usually test a specific portal in a fully integrated
environment. The tests are written in python using the pytest framework.
The files follow the pattern `test_$NAME.py`. The tests can be run with
`meson test --suite integration` or with `run-test.sh` in the source directory.
The environment is being set up by fixtures in `conftest.py` which can be
overwritten or parameterized by the tests themselves. There are a bunch of
convenient functions and classes in `__init__.py`. The portal backends are
implemented using dbusmock templates in the `templates` directory.
### Environment
Some environment variables need to be set for the integration tests to function
properly and the harness will refuse to launch if they are not set. If the
harness is executed by meson or run-test.sh, they will be set automatically.
* `XDG_DESKTOP_PORTAL_PATH`: The path to the xdg-desktop-portal binary
* `XDG_PERMISSION_STORE_PATH`: The path to the xdg-permission-store binary
* `XDG_DOCUMENT_PORTAL_PATH`: The path to the xdg-document-portal binary
* `XDP_VALIDATE_ICON`: The path to the xdg-desktop-portal-validate-icon binary
* `XDP_VALIDATE_SOUND`: The path to the xdg-desktop-portal-validate-sound binary
* `XDP_VALIDATE_AUTO`: If set, automatically discovers the icon and sound
validators (only useful for installed tests) instead of using
`XDP_VALIDATE_ICON` and `XDP_VALIDATE_SOUND`.
Some optional environment variables that can be set to influence how the test
harness behaves.
* `XDP_TEST_IN_CI`: If set (to any value), some unreliable tests might get
skipped and some tests might run less iterations or otherwise test less
thoroughly.
Set this for automated QA testing, leave it unset during development.
* `XDP_TEST_RUN_LONG`: If set (to any value), some tests will run more
iterations or otherwise test more thoroughly
* `FLATPAK_BWRAP`: Path to the **bwrap**(1) executable
(default: discovered at build-time)
* `XDP_VALIDATE_ICON_INSECURE`: If set (to any value), x-d-p doesn't
sandbox the icon validator using **bwrap**(1), even if sandboxed
validation was enabled at compile time.
This can be used to run build-time tests in a chroot or unprivileged
container environment, where **bwrap**(1) normally can't work.
It should never be set on a production system that will be validating
untrusted icons!
* `XDP_VALIDATE_SOUND_INSECURE`: Same as `XDP_VALIDATE_ICON_INSECURE`,
but for sounds
Some optional environment variables that can be set to help with debugging.
* `XDP_DBUS_MONITOR`: If set, starts dbus-monitor on the test dbus server
* `XDP_DBUS_TIMEOUT`: Maximum timeout for dbus calls in ms (default: 5s)
* `XDG_DESKTOP_PORTAL_WAIT_FOR_DEBUGGER`: Makes xdg-desktop-portal wait for
a debugger to attach by raising SIGSTOP
* `XDG_DOCUMENT_PORTAL_WAIT_FOR_DEBUGGER`: Makes xdg-document-portal wait
for a debugger to attach by raising SIGSTOP
* `XDG_PERMISSION_STORE_WAIT_FOR_DEBUGGER`: Makes xdg-permission-store wait
for a debugger to attach by raising SIGSTOP
Internal environment variables the tests use via pytest fixtures to set up the
environment they need.
* `XDG_DESKTOP_PORTAL_TEST_APP_ID`: If set, the portal will use a host
XdpAppInfo with the app id set to the variable. This is used to get a
predictable app id for tests.
* `XDG_DESKTOP_PORTAL_TEST_USB_QUERIES`: The USB queries for the USB device
portal testing
### Adding new tests
Make sure the required portals are listed in
`xdg_desktop_portal_dir_default_files` in `conftest.py`.
Add a `test_${name}.py` file to this directory and add the file to
`meson.build`.
If the portal that is being tested requires a backend implementation, add
it to the `templates` directory and add the file to `meson.build`. See the
dbusmock documentation for details on those templates.

View File

@@ -0,0 +1,620 @@
#!/usr/bin/env python3
#
# SPDX-License-Identifier: LGPL-2.1-or-later
#
# This file is formatted with Python Black
from dbus.mainloop.glib import DBusGMainLoop
from gi.repository import GLib, Gio
from itertools import count
from typing import Any, Dict, Optional, NamedTuple, Callable, List
import os
import dbus
import dbus.proxies
import dbusmock
import logging
import subprocess
DBusGMainLoop(set_as_default=True)
# Anything that takes longer than 5s needs to fail
DBUS_TIMEOUT = int(os.environ.get("XDP_DBUS_TIMEOUT", "5000"))
_counter = count()
ASV = Dict[str, Any]
def init_logger(name: str) -> logging.Logger:
"""
Common logging setup for tests. Use as:
>>> import tests as xdp
>>> logger = xdp.init_logger(__name__)
>>> logger.debug("foo")
"""
logging.basicConfig(
format="%(levelname).1s|%(name)s: %(message)s", level=logging.DEBUG
)
logger = logging.getLogger(f"xdp.{name}")
logger.setLevel(logging.DEBUG)
return logger
logger = init_logger("utils")
def is_in_ci() -> bool:
return os.environ.get("XDP_TEST_IN_CI") is not None
def is_in_container() -> bool:
return is_in_ci() or (
"container" in os.environ
and (os.environ["container"] == "docker" or os.environ["container"] == "podman")
)
def run_long_tests() -> bool:
return os.environ.get("XDP_TEST_RUN_LONG") is not None
def check_program_success(cmd) -> bool:
proc = subprocess.Popen(
cmd, stdout=None, stderr=None, shell=True, universal_newlines=True
)
_ = proc.communicate()
return proc.returncode == 0
class FuseNotSupportedException(Exception):
pass
def ensure_fuse_supported() -> None:
if not check_program_success("fusermount3 --version"):
raise FuseNotSupportedException("no fusermount3")
if not check_program_success(
"capsh --print | grep -q 'Bounding set.*[^a-z]cap_sys_admin'"
):
raise FuseNotSupportedException(
"No cap_sys_admin in bounding set, can't use FUSE"
)
if not check_program_success("[ -w /dev/fuse ]"):
raise FuseNotSupportedException("no write access to /dev/fuse")
if not check_program_success("[ -e /etc/mtab ]"):
raise FuseNotSupportedException("no /etc/mtab")
def wait(ms: int):
"""
Waits for the specified amount of milliseconds.
"""
mainloop = GLib.MainLoop()
GLib.timeout_add(ms, mainloop.quit)
mainloop.run()
def wait_for(fn: Callable[[], bool]):
"""
Waits and dispatches to mainloop until the function fn returns true. This is
useful in combination with a lambda which captures a variable:
my_var = False
def callback():
my_var = True
do_something_later(callback)
xdp.wait_for(lambda: my_var)
"""
mainloop = GLib.MainLoop()
while not fn():
GLib.timeout_add(50, mainloop.quit)
mainloop.run()
def get_permission_store_iface(bus: dbus.Bus) -> dbus.Interface:
"""
Returns the dbus interface of the xdg-permission-store.
"""
obj = bus.get_object(
"org.freedesktop.impl.portal.PermissionStore",
"/org/freedesktop/impl/portal/PermissionStore",
)
return dbus.Interface(obj, "org.freedesktop.impl.portal.PermissionStore")
def get_document_portal_iface(bus: dbus.Bus) -> dbus.Interface:
"""
Returns the dbus interface of the xdg-document-portal.
"""
obj = bus.get_object(
"org.freedesktop.portal.Documents",
"/org/freedesktop/portal/documents",
)
return dbus.Interface(obj, "org.freedesktop.portal.Documents")
def get_mock_iface(bus: dbus.Bus, bus_name: Optional[str] = None) -> dbus.Interface:
"""
Returns the mock interface of the xdg-desktop-portal.
"""
if not bus_name:
bus_name = "org.freedesktop.impl.portal.Test"
obj = bus.get_object(bus_name, "/org/freedesktop/portal/desktop")
return dbus.Interface(obj, dbusmock.MOCK_IFACE)
def portal_interface_name(portal_name: str, domain: Optional[str] = None) -> str:
"""
Returns the fully qualified interface for a portal name.
"""
if domain:
return f"org.freedesktop.{domain}.portal.{portal_name}"
else:
return f"org.freedesktop.portal.{portal_name}"
def get_portal_iface(
bus: dbus.Bus, name: str, domain: Optional[str] = None
) -> dbus.Interface:
"""
Returns the dbus interface for a portal name.
"""
name = portal_interface_name(name, domain)
return get_iface(bus, name)
def get_iface(bus: dbus.Bus, name: str) -> dbus.Interface:
"""
Returns a named interface of the main portal object.
"""
try:
ifaces = bus._xdp_portal_ifaces
except AttributeError:
ifaces = bus._xdp_portal_ifaces = {}
try:
intf = ifaces[name]
except KeyError:
intf = dbus.Interface(get_xdp_dbus_object(bus), name)
assert intf
ifaces[name] = intf
return intf
def get_xdp_dbus_object(bus: dbus.Bus) -> dbus.proxies.ProxyObject:
"""
Returns the main portal object.
"""
try:
obj = getattr(bus, "_xdp_dbus_object")
except AttributeError:
obj = bus.get_object(
"org.freedesktop.portal.Desktop", "/org/freedesktop/portal/desktop"
)
assert obj
bus._xdp_dbus_object = obj
return obj
def check_version(bus: dbus.Bus, portal_name: str, expected_version: int):
"""
Checks that the portal_name portal version is equal to expected_version.
"""
properties_intf = dbus.Interface(
get_xdp_dbus_object(bus), "org.freedesktop.DBus.Properties"
)
portal_iface_name = portal_interface_name(portal_name)
try:
portal_version = properties_intf.Get(portal_iface_name, "version")
assert int(portal_version) == expected_version
except dbus.exceptions.DBusException as e:
logger.critical(e)
assert e is None, str(e)
class Response(NamedTuple):
"""
Response as returned by a completed :class:`Request`
"""
response: int
results: ASV
class ResponseTimeout(Exception):
"""
Exception raised by :meth:`Request.call` if the Request did not receive a
Response in time.
"""
pass
class Closable:
"""
Parent class for both Session and Request. Both of these have a Close()
method.
"""
def __init__(self, bus: dbus.Bus, objpath: str):
self.objpath = objpath
# GLib makes assertions in callbacks impossible, so we wrap all
# callbacks into a try: except and store the error on the request to
# be raised later when we're back in the main context
self.error: Optional[Exception] = None
self._mainloop: Optional[GLib.MainLoop] = None
self._impl_closed = False
self._bus = bus
self._closable = type(self).__name__
assert self._closable in ("Request", "Session")
proxy = bus.get_object("org.freedesktop.portal.Desktop", objpath)
self._closable_interface = dbus.Interface(
proxy, f"org.freedesktop.portal.{self._closable}"
)
@property
def bus(self) -> dbus.Bus:
return self._bus
@property
def closed(self) -> bool:
"""
True if the impl.portal was closed
"""
return self._impl_closed
def close(self) -> None:
signal_match = None
def cb_impl_closed_by_portal(handle) -> None:
if handle == self.objpath:
logger.debug(f"Impl{self._closable} {self.objpath} was closed")
signal_match.remove() # type: ignore
self._impl_closed = True
if self.closed and self._mainloop:
self._mainloop.quit()
# See :class:`ImplRequest`, this signal is a side-channel for the
# impl.portal template to notify us when the impl.Request was really
# closed by the portal.
signal_match = self._bus.add_signal_receiver(
cb_impl_closed_by_portal,
f"{self._closable}Closed",
dbus_interface="org.freedesktop.impl.portal.Mock",
)
logger.debug(f"Closing {self._closable} {self.objpath}")
self._closable_interface.Close()
def schedule_close(self, timeout_ms=300):
"""
Schedule an automatic Close() on the given timeout in milliseconds.
"""
assert 0 < timeout_ms < DBUS_TIMEOUT
GLib.timeout_add(timeout_ms, self.close)
class Request(Closable):
"""
Helper class for executing methods that use Requests. This calls takes
care of subscribing to the signals and invokes the method on the
interface with the expected behaviors. A typical invocation is:
>>> response = Request(connection, interface).call("Foo", bar="bar")
>>> assert response.response == 0
Requests can only be used once, to call a second method you must
instantiate a new Request object.
"""
def __init__(self, bus: dbus.Bus, interface: dbus.Interface):
def sanitize(name):
return name.lstrip(":").replace(".", "_")
sender_token = sanitize(bus.get_unique_name())
self._handle_token = f"request{next(_counter)}"
self.handle = f"/org/freedesktop/portal/desktop/request/{sender_token}/{self._handle_token}"
# The Closable
super().__init__(bus, self.handle)
self.interface = interface
self.response: Optional[Response] = None
self.used = False
# GLib makes assertions in callbacks impossible, so we wrap all
# callbacks into a try: except and store the error on the request to
# be raised later when we're back in the main context
self.error: Optional[Exception] = None
proxy = bus.get_object("org.freedesktop.portal.Desktop", self.handle)
self.mock_interface = dbus.Interface(proxy, dbusmock.MOCK_IFACE)
self._proxy = bus.get_object("org.freedesktop.portal.Desktop", self.handle)
def cb_response(response: int, results: ASV) -> None:
try:
logger.debug(f"Response received on {self.handle}")
assert self.response is None
self.response = Response(response, results)
if self._mainloop:
self._mainloop.quit()
except Exception as e:
self.error = e
self.request_interface = dbus.Interface(proxy, "org.freedesktop.portal.Request")
self.request_interface.connect_to_signal("Response", cb_response)
@property
def handle_token(self) -> dbus.String:
"""
Returns the dbus-ready handle_token, ready to be put into the options
"""
return dbus.String(self._handle_token, variant_level=1)
def call(self, methodname: str, **kwargs) -> Optional[Response]:
"""
Semi-synchronously call method ``methodname`` on the interface given
in the Request's constructor. The kwargs must be specified in the
order the DBus method takes them but the handle_token is automatically
filled in.
>>> response = Request(connection, interface).call("Foo", bar="bar")
>>> if response.response != 0:
... print("some error occured")
The DBus call itself is asynchronous (required for signals to work)
but this method does not return until the Response is received, the
Request is closed or an error occurs. If the Request is closed, the
Response is None.
If the "reply_handler" and "error_handler" keywords are present, those
callbacks are called just like they would be as dbus.service.ProxyObject.
"""
assert not self.used
self.used = True
# Make sure options exists and has the handle_token set
try:
options = kwargs["options"]
except KeyError:
options = dbus.Dictionary({}, signature="sv")
if "handle_token" not in options:
options["handle_token"] = self.handle_token
# Anything that takes longer than 5s needs to fail
self._mainloop = GLib.MainLoop()
GLib.timeout_add(DBUS_TIMEOUT, self._mainloop.quit)
method = getattr(self.interface, methodname)
assert method
reply_handler = kwargs.pop("reply_handler", None)
error_handler = kwargs.pop("error_handler", None)
# Handle the normal method reply which returns is the Request object
# path. We don't exit the mainloop here, we're waiting for either the
# Response signal on the Request itself or the Close() handling
def reply_cb(handle):
try:
logger.debug(f"Reply to {methodname} with {self.handle}")
assert handle == self.handle
if reply_handler:
reply_handler(handle)
except Exception as e:
self.error = e
# Handle any exceptions during the actual method call (not the Request
# handling itself). Can exit the mainloop if that happens
def error_cb(error):
try:
logger.debug(f"Error after {methodname} with {error}")
if error_handler:
error_handler(error)
self.error = error
except Exception as e:
self.error = e
finally:
if self._mainloop:
self._mainloop.quit()
# Method is invoked async, otherwise we can't mix and match signals
# and other calls. It's still sync as seen by the caller in that we
# have a mainloop that waits for us to finish though.
method(
*list(kwargs.values()),
reply_handler=reply_cb,
error_handler=error_cb,
)
self._mainloop.run()
if self.error:
raise self.error
elif not self.closed and self.response is None:
raise ResponseTimeout(f"Timed out waiting for response from {methodname}")
return self.response
class Session(Closable):
"""
Helper class for a Session created by a portal. This class takes care of
subscribing to the `Closed` signals. A typical invocation is:
>>> response = Request(connection, interface).call("CreateSession")
>>> session = Session.from_response(response)
# Now run the main loop and do other stuff
# Check if the session was closed
>>> if session.closed:
... pass
# or close the session explicitly
>>> session.close() # to close the session or
"""
def __init__(self, bus: dbus.Bus, handle: str):
assert handle
super().__init__(bus, handle)
self.handle = handle
self.details = None
# GLib makes assertions in callbacks impossible, so we wrap all
# callbacks into a try: except and store the error on the request to
# be raised later when we're back in the main context
self.error = None
self._closed_sig_received = False
def cb_closed(details: ASV) -> None:
try:
logger.debug(f"Session.Closed received on {self.handle}")
assert not self._closed_sig_received
self._closed_sig_received = True
self.details = details
if self._mainloop:
self._mainloop.quit()
except Exception as e:
self.error = e
proxy = bus.get_object("org.freedesktop.portal.Desktop", handle)
self.session_interface = dbus.Interface(proxy, "org.freedesktop.portal.Session")
self.session_interface.connect_to_signal("Closed", cb_closed)
@property
def closed(self):
"""
Returns True if the session was closed by the backend
"""
return self._closed_sig_received or super().closed
@classmethod
def from_response(cls, bus: dbus.Bus, response: Response) -> "Session":
return cls(bus, response.results["session_handle"])
class GDBusIfaceSignal:
"""
Helper class which represents a connected signal on a GDBusIface and can be
used to disconnect from the signal.
"""
def __init__(self, signal_id: int, proxy: Gio.DBusProxy):
self.signal_id = signal_id
self.proxy = proxy
def disconnect(self):
"""
Disconnects the signal
"""
self.proxy.disconnect(self.signal_id)
class GDBusIface:
"""
Helper class for calling dbus interfaces with complex arguments.
Usually you want to use python-dbus on the dbus_con fixture with
get_portal_iface , get_mock_iface or get_iface. This is convenient but
might not be sufficient for complex arguments or for asynchronously calling
a method.
"""
def __init__(self, bus: str, obj: str, iface: str):
"""
Creates a GDBusIface for a specific bus, object and interface on the
session bus.
"""
address = Gio.dbus_address_get_for_bus_sync(Gio.BusType.SESSION, None)
session_bus = Gio.DBusConnection.new_for_address_sync(
address,
Gio.DBusConnectionFlags.AUTHENTICATION_CLIENT
| Gio.DBusConnectionFlags.MESSAGE_BUS_CONNECTION,
None,
None,
)
assert session_bus
self._proxy = Gio.DBusProxy.new_sync(
session_bus,
Gio.DBusProxyFlags.NONE,
None,
bus,
obj,
iface,
None,
)
def _call(
self, method_name: str, args_variant: GLib.Variant, fds: List[int] = []
) -> GLib.Variant:
"""
Calls a method synchronously with the arguments passed in args_variant,
passing the file descriptors specified in fds.
Returns the result of the dbus call.
"""
fdlist = Gio.UnixFDList.new()
for fd in fds:
fdlist.append(fd)
return self._proxy.call_with_unix_fd_list_sync(
method_name,
args_variant,
0,
-1,
fdlist,
None,
)
def _call_async(
self,
method_name: str,
args_variant: GLib.Variant,
fds: List[int] = [],
cb: Optional[Callable[[GLib.Variant], None]] = None,
) -> None:
"""
Calls a method asynchronously with the arguments passed in args_variant,
passing the file descriptors specified in fds.
Invokes the callback cb when the call finished.
"""
fdlist = Gio.UnixFDList.new()
for fd in fds:
fdlist.append(fd)
def internal_cb(s, res, _):
res = s.call_finish(res)
if cb:
cb(res)
self._proxy.call_with_unix_fd_list(
method_name,
args_variant,
0,
-1,
fdlist,
None,
internal_cb,
None,
)
def connect_to_signal(
self, name: str, cb: Callable[[GLib.Variant], None]
) -> GDBusIfaceSignal:
"""
Connects to the dbus signal name to the callback cb. Returns an object
representing the connection which can be used to disconnect it again.
"""
def internal_cb(proxy, sender_name, signal_name, parameters):
if signal_name != name:
return
cb(parameters)
signal_id = self._proxy.connect("g-signal", internal_cb)
return GDBusIfaceSignal(signal_id, self._proxy)

View File

@@ -0,0 +1,8 @@
# external (GIO?)
leak:g_dbus_message_new_from_blob
# Bugs in our code
# Take a look at them and try to figure out what's going on!
leak:permission_db_entry_set_app_permissions
leak:test_color_delay
leak:test_color_basic
leak:test_color_parallel

View File

@@ -0,0 +1,590 @@
# SPDX-License-Identifier: LGPL-2.1-or-later
#
# This file is formatted with Python Black
from typing import Any, Dict, Iterator, Optional
from types import ModuleType
import pytest
import dbus
import dbusmock
import os
import sys
import tempfile
import subprocess
import time
import signal
from pathlib import Path
from contextlib import chdir
import gi
gi.require_version("UMockdev", "1.0")
from gi.repository import UMockdev # noqa E402
def pytest_configure() -> None:
ensure_environment_set()
ensure_umockdev_loaded()
def pytest_sessionfinish(session, exitstatus):
# Meson and ginsttest-runner expect tests to exit with status 77 if all
# tests were skipped
if exitstatus == pytest.ExitCode.NO_TESTS_COLLECTED:
session.exitstatus = 77
def ensure_environment_set() -> None:
env_vars = [
"XDG_DESKTOP_PORTAL_PATH",
"XDG_PERMISSION_STORE_PATH",
"XDG_DOCUMENT_PORTAL_PATH",
]
if not os.getenv("XDP_VALIDATE_AUTO"):
env_vars += [
"XDP_VALIDATE_ICON",
"XDP_VALIDATE_SOUND",
]
else:
os.environ.pop("XDP_VALIDATE_ICON", None)
os.environ.pop("XDP_VALIDATE_SOUND", None)
for env_var in env_vars:
if not os.getenv(env_var):
raise Exception(f"{env_var} must be set")
def ensure_umockdev_loaded() -> None:
umockdev_preload = "libumockdev-preload.so"
preload = os.environ.get("LD_PRELOAD", "")
if umockdev_preload not in preload:
os.environ["LD_PRELOAD"] = f"{umockdev_preload}:{preload}"
os.execv(sys.executable, [sys.executable] + sys.argv)
def test_dir() -> Path:
return Path(__file__).resolve().parent
@pytest.fixture
def xdg_desktop_portal_path() -> Path:
return Path(os.environ["XDG_DESKTOP_PORTAL_PATH"])
@pytest.fixture
def xdg_permission_store_path() -> Path:
return Path(os.environ["XDG_PERMISSION_STORE_PATH"])
@pytest.fixture
def xdg_document_portal_path() -> Path:
return Path(os.environ["XDG_DOCUMENT_PORTAL_PATH"])
@pytest.fixture(autouse=True)
def create_test_dirs(umockdev: Optional[UMockdev.Testbed]) -> Iterator[None]:
# The umockdev argument is to make sure the testbed
# is created before we create the tmpdir
env_dirs = [
"HOME",
"TMPDIR",
"XDG_CACHE_HOME",
"XDG_CONFIG_HOME",
"XDG_DATA_HOME",
"XDG_RUNTIME_DIR",
"XDG_DESKTOP_PORTAL_DIR",
]
test_root = tempfile.TemporaryDirectory(
prefix="xdp-testroot-", ignore_cleanup_errors=True
)
for env_dir in env_dirs:
directory = Path(test_root.name) / env_dir.lower()
directory.mkdir(mode=0o700, parents=True)
os.environ[env_dir] = directory.absolute().as_posix()
yield
test_root.cleanup()
@pytest.fixture
def xdg_data_home_files() -> Dict[str, bytes]:
"""
Default fixture which can be used to create files in the temporary
XDG_DATA_HOME directory of the test.
"""
return {}
@pytest.fixture(autouse=True)
def ensure_xdg_data_home(
create_test_dirs: Any, xdg_data_home_files: Dict[str, bytes]
) -> None:
files = xdg_data_home_files
for name, content in files.items():
file_path = Path(os.environ["XDG_DATA_HOME"]) / name
file_path.parent.mkdir(parents=True, exist_ok=True)
with open(file_path.absolute().as_posix(), "wb") as f:
f.write(content)
@pytest.fixture
def xdg_desktop_portal_dir_files() -> Dict[str, bytes]:
"""
Default fixture which can be used to create files in the temporary
XDG_DESKTOP_PORTAL_DIR directory of the test.
"""
return {}
@pytest.fixture
def xdg_desktop_portal_dir_default_files() -> Dict[str, bytes]:
files = {}
portals = [
"org.freedesktop.impl.portal.Access",
"org.freedesktop.impl.portal.Account",
"org.freedesktop.impl.portal.AppChooser",
"org.freedesktop.impl.portal.Background",
"org.freedesktop.impl.portal.Clipboard",
"org.freedesktop.impl.portal.DynamicLauncher",
"org.freedesktop.impl.portal.Email",
"org.freedesktop.impl.portal.FileChooser",
"org.freedesktop.impl.portal.GlobalShortcuts",
"org.freedesktop.impl.portal.Inhibit",
"org.freedesktop.impl.portal.InputCapture",
"org.freedesktop.impl.portal.Lockdown",
"org.freedesktop.impl.portal.Notification",
"org.freedesktop.impl.portal.Print",
"org.freedesktop.impl.portal.RemoteDesktop",
"org.freedesktop.impl.portal.Screenshot",
"org.freedesktop.impl.portal.Settings",
"org.freedesktop.impl.portal.Usb",
"org.freedesktop.impl.portal.Wallpaper",
]
files["test-portals.conf"] = b"""
[preferred]
default=test;
"""
files["test.portal"] = """
[portal]
DBusName=org.freedesktop.impl.portal.Test
Interfaces={}
""".format(";".join(portals)).encode("utf-8")
return files
@pytest.fixture(autouse=True)
def ensure_xdg_desktop_portal_dir(
create_test_dirs: Any,
xdg_desktop_portal_dir_files: Dict[str, bytes],
xdg_desktop_portal_dir_default_files: Dict[str, bytes],
) -> None:
files = xdg_desktop_portal_dir_default_files | xdg_desktop_portal_dir_files
for name, content in files.items():
file_path = Path(os.environ["XDG_DESKTOP_PORTAL_DIR"]) / name
file_path.parent.mkdir(parents=True, exist_ok=True)
with open(file_path.absolute().as_posix(), "wb") as f:
f.write(content)
@pytest.fixture(autouse=True)
def create_test_dbus() -> Iterator[dbusmock.DBusTestCase]:
bus = dbusmock.DBusTestCase()
bus.setUp()
bus.start_session_bus()
bus.start_system_bus()
yield bus
bus.tearDown()
bus.tearDownClass()
@pytest.fixture(autouse=True)
def create_dbus_monitor(create_test_dbus) -> Iterator[Optional[subprocess.Popen]]:
if not os.getenv("XDP_DBUS_MONITOR"):
yield None
return
dbus_monitor = subprocess.Popen(["dbus-monitor", "--session"])
yield dbus_monitor
dbus_monitor.terminate()
dbus_monitor.wait()
def _get_server_for_module(
busses: dict[dbusmock.BusType, dict[str, dbusmock.SpawnedMock]],
module: ModuleType,
bustype: dbusmock.BusType,
) -> dbusmock.SpawnedMock:
assert bustype in dbusmock.BusType
try:
return busses[bustype][module.BUS_NAME]
except KeyError:
server = dbusmock.SpawnedMock.spawn_for_name(
module.BUS_NAME,
"/dbusmock",
dbusmock.OBJECT_MANAGER_IFACE,
bustype,
stdout=None,
stderr=None,
)
busses[bustype][module.BUS_NAME] = server
return server
def _get_main_obj_for_module(
server: dbusmock.SpawnedMock, module: ModuleType, bustype: dbusmock.BusType
) -> dbusmock.DBusMockObject:
try:
server.obj.AddObject(
module.MAIN_OBJ,
"com.example.EmptyInterface",
{},
[],
dbus_interface=dbusmock.MOCK_IFACE,
)
except Exception:
pass
bustype.wait_for_bus_object(module.BUS_NAME, module.MAIN_OBJ)
bus = bustype.get_connection()
return bus.get_object(module.BUS_NAME, module.MAIN_OBJ)
def _terminate_mock_p(process: subprocess.Popen) -> None:
process.terminate()
process.wait()
def _terminate_servers(
busses: dict[dbusmock.BusType, dict[str, dbusmock.SpawnedMock]],
) -> None:
for server in busses[dbusmock.BusType.SYSTEM].values():
_terminate_mock_p(server.process)
for server in busses[dbusmock.BusType.SESSION].values():
_terminate_mock_p(server.process)
def _start_template(
busses: dict[dbusmock.BusType, dict[str, dbusmock.SpawnedMock]],
template: str,
bus_name: Optional[str],
params: Dict[str, Any] = {},
) -> None:
"""
Start the template and potentially start a server for it
"""
module_path_dir = (test_dir()).parent.absolute().as_posix()
template_path = test_dir() / f"templates/{template.lower()}.py"
template = template_path.absolute().as_posix()
# we cd to the parent dir of the test_dir so that the module search path for
# the templates is the same as opening the modules from here
with chdir(module_path_dir):
module = dbusmock.mockobject.load_module(template)
bustype = (
dbusmock.BusType.SYSTEM if module.SYSTEM_BUS else dbusmock.BusType.SESSION
)
if bus_name:
module.BUS_NAME = bus_name
server = _get_server_for_module(busses, module, bustype)
main_obj = _get_main_obj_for_module(server, module, bustype)
main_obj.AddTemplate(
template,
dbus.Dictionary(params, signature="sv"),
dbus_interface=dbusmock.MOCK_IFACE,
)
@pytest.fixture
def template_params() -> dict[str, dict[str, Any]]:
"""
Default fixture for overriding the parameters which should be passed to the
mocking templates. Use required_templates to specify the default parameters
and override it for specific test cases via
@pytest.mark.parametrize("template_params", ({"Template": {"foo": "bar"}},))
"""
return {}
@pytest.fixture
def required_templates() -> dict[str, dict[str, Any]]:
"""
Default fixture for enumerating the mocking templates the test case requires
to be started. This is a map from a template spec to the parameters which
should be passed to the template. The template spec is the name of a
template in the templates directory and an optional dbus bus name to start
the template at, separated by a colon.
This starts the `settings` and `email` templates on the bus names specified
with BUS_NAME in the templates and passes parameters to settings.
{
"settings": {
"some_param": true,
},
"email": {},
}
This starts two instances of the settings template with their own parameters
once on the bus name specified in BUS_NAME in the template, and once on the
bus name `org.freedesktop.impl.portal.OtherImpl`.
{
"settings": {
"some_param": true,
},
"settings:org.freedesktop.impl.portal.OtherImpl": {
"some_param": false,
},
}
"""
return {}
@pytest.fixture
def templates(
required_templates: dict[str, dict[str, Any]],
template_params: dict[str, dict[str, Any]],
) -> Iterator[None]:
"""
Fixture which starts the required templates with their parameters. Usually
the `portals` fixture is what you're looking for because it also starts
the portal frontend and the permission store.
"""
busses: dict[dbusmock.BusType, dict[str, dbusmock.SpawnedMock]] = {
dbusmock.BusType.SYSTEM: {},
dbusmock.BusType.SESSION: {},
}
for template_data, params in required_templates.items():
template, bus_name = (template_data.split(":") + [None])[:2]
assert template
params = template_params.get(template, params)
_start_template(busses, template, bus_name, params)
yield
_terminate_servers(busses)
@pytest.fixture
def xdp_overwrite_env() -> dict[str, str]:
"""
Default fixture which can be used to override the environment that gets
passed to xdg-desktop-portal, xdg-document-portal and xdg-permission-store.
"""
return {}
@pytest.fixture
def app_id() -> str:
"""
Default fixture which can be used to override the app id that the portal
frontend will discover for incoming connections.
"""
return "org.example.Test"
@pytest.fixture
def xdp_env(
xdp_overwrite_env: dict[str, str],
app_id: str,
usb_queries: Optional[str],
umockdev: Optional[UMockdev.Testbed],
) -> dict[str, str]:
env = os.environ.copy()
env["G_DEBUG"] = "fatal-criticals"
env["XDG_CURRENT_DESKTOP"] = "test"
if app_id:
env["XDG_DESKTOP_PORTAL_TEST_APP_ID"] = app_id
if usb_queries:
env["XDG_DESKTOP_PORTAL_TEST_USB_QUERIES"] = usb_queries
if umockdev:
env["UMOCKDEV_DIR"] = umockdev.get_root_dir()
asan_suppression = test_dir() / "asan.suppression"
if not asan_suppression.exists():
raise FileNotFoundError(f"{asan_suppression} does not exist")
env["LSAN_OPTIONS"] = f"suppressions={asan_suppression}"
for key, val in xdp_overwrite_env.items():
env[key] = val
return env
def _maybe_add_asan_preload(executable: Path, env: dict[str, str]) -> None:
# ASAN really wants to be the first library to get loaded but we also
# LD_PRELOAD umockdev and LD_PRELOAD gets loaded before any "normally"
# linked libraries. This uses ldd to find the version of libasan.so that
# should be loaded and puts it in front of LD_PRELOAD.
# This way, LD_PRELOAD and ASAN can be used at the same time.
ldd = subprocess.check_output(["ldd", executable]).decode("utf-8")
libs = [line.split()[0] for line in ldd.splitlines()]
try:
libasan = next(filter(lambda lib: lib.startswith("libasan"), libs))
except StopIteration:
return
preload = env.get("LD_PRELOAD", "")
env["LD_PRELOAD"] = f"{libasan}:{preload}"
@pytest.fixture
def xdg_desktop_portal(
dbus_con: dbus.Bus, xdg_desktop_portal_path: Path, xdp_env: dict[str, str]
) -> Iterator[subprocess.Popen]:
"""
Fixture which starts and eventually stops xdg-desktop-portal
"""
if not xdg_desktop_portal_path.exists():
raise FileNotFoundError(f"{xdg_desktop_portal_path} does not exist")
env = xdp_env.copy()
_maybe_add_asan_preload(xdg_desktop_portal_path, env)
xdg_desktop_portal = subprocess.Popen([xdg_desktop_portal_path], env=env)
while not dbus_con.name_has_owner("org.freedesktop.portal.Desktop"):
returncode = xdg_desktop_portal.poll()
if returncode is not None:
raise subprocess.SubprocessError(
f"xdg-desktop-portal exited with {returncode}"
)
time.sleep(0.1)
yield xdg_desktop_portal
xdg_desktop_portal.send_signal(signal.SIGHUP)
returncode = xdg_desktop_portal.wait()
assert returncode == 0
@pytest.fixture
def xdg_permission_store(
dbus_con: dbus.Bus, xdg_permission_store_path: Path, xdp_env: dict[str, str]
) -> Iterator[subprocess.Popen]:
"""
Fixture which starts and eventually stops xdg-permission-store
"""
if not xdg_permission_store_path.exists():
raise FileNotFoundError(f"{xdg_permission_store_path} does not exist")
env = xdp_env.copy()
_maybe_add_asan_preload(xdg_permission_store_path, env)
permission_store = subprocess.Popen([xdg_permission_store_path], env=env)
while not dbus_con.name_has_owner("org.freedesktop.impl.portal.PermissionStore"):
returncode = permission_store.poll()
if returncode is not None:
raise subprocess.SubprocessError(
f"xdg-permission-store exited with {returncode}"
)
time.sleep(0.1)
yield permission_store
permission_store.send_signal(signal.SIGHUP)
permission_store.wait()
# The permission store does not shut down cleanly currently
# returncode = permission_store.wait()
# assert returncode == 0
@pytest.fixture
def xdg_document_portal(
dbus_con: dbus.Bus, xdg_document_portal_path: Path, xdp_env: dict[str, str]
) -> Iterator[subprocess.Popen]:
"""
Fixture which starts and eventually stops xdg-document-portal
"""
if not xdg_document_portal_path.exists():
raise FileNotFoundError(f"{xdg_document_portal_path} does not exist")
# FUSE and LD_PRELOAD don't like each other. Not sure what exactly is going
# wrong but it usually just results in a weird hang that needs SIGKILL
env = xdp_env.copy()
env.pop("LD_PRELOAD", None)
document_portal = subprocess.Popen([xdg_document_portal_path], env=env)
while not dbus_con.name_has_owner("org.freedesktop.portal.Documents"):
returncode = document_portal.poll()
if returncode is not None:
raise subprocess.SubprocessError(
f"xdg-document-portal exited with {returncode}"
)
time.sleep(0.1)
yield document_portal
document_portal.send_signal(signal.SIGHUP)
returncode = document_portal.wait()
assert returncode == 0
@pytest.fixture
def portals(templates: Any, xdg_desktop_portal: Any, xdg_permission_store: Any) -> None:
"""
Fixture which starts the required templates, xdg-desktop-portal,
xdg-document-portal and xdg-permission-store. Most tests require this.
"""
return None
@pytest.fixture
def usb_queries() -> Optional[str]:
"""
Default fixture providing the usb queries the connecting process can
enumerate
"""
return None
@pytest.fixture
def umockdev() -> Optional[UMockdev.Testbed]:
"""
Default fixture providing a umockdev testbed
"""
return None
@pytest.fixture
def dbus_con(create_test_dbus: dbusmock.DBusTestCase) -> dbus.Bus:
"""
Default fixture which provides the python-dbus session bus of the test.
"""
con = create_test_dbus.get_dbus(system_bus=False)
assert con
return con
@pytest.fixture
def dbus_con_sys(create_test_dbus: dbusmock.DBusTestCase) -> dbus.Bus:
"""
Default fixture which provides the python-dbus system bus of the test.
"""
con_sys = create_test_dbus.get_dbus(system_bus=True)
assert con_sys
return con_sys

View File

@@ -0,0 +1,4 @@
configure_file(input: 'no_tables',
output: '@PLAINNAME@',
copy: true
)

Binary file not shown.

View File

@@ -0,0 +1,191 @@
env_tests = environment()
env_tests.set('XDG_DATA_DIRS', meson.current_build_dir() / 'share')
env_tests.set('G_TEST_SRCDIR', meson.current_source_dir())
env_tests.set('G_TEST_BUILDDIR', meson.current_build_dir())
env_tests.set('G_DEBUG', 'gc-friendly') # from glib-tap.mk
env_tests.set('LSAN_OPTIONS', 'suppressions=' + meson.current_source_dir() / 'asan.suppression')
if glib_dep.version().version_compare('>= 2.68')
test_protocol = 'tap'
else
test_protocol = 'exitcode'
endif
subdir('dbs')
test_permission_db = executable(
'test-permission-db',
['test-permission-db.c'] + db_sources,
dependencies: [common_deps],
include_directories: [common_includes],
install: enable_installed_tests,
install_dir: installed_tests_dir,
)
test(
'unit/permission-db',
test_permission_db,
suite: ['unit'],
env: env_tests,
is_parallel: false,
protocol: test_protocol,
)
test_xdp_utils = executable(
'test-xdp-utils',
'test-xdp-utils.c',
xdp_utils_sources,
dependencies: [common_deps, xdp_utils_deps],
include_directories: [common_includes, xdp_utils_includes],
install: enable_installed_tests,
install_dir: installed_tests_dir,
)
test(
'unit/xdp-utils',
test_xdp_utils,
suite: ['unit'],
env: env_tests,
is_parallel: false,
protocol: test_protocol,
)
test_method_info = executable(
'test-xdp-method-info',
'test-xdp-method-info.c',
xdp_method_info_sources,
dependencies: [common_deps],
include_directories: [common_includes, xdp_utils_includes],
install: enable_installed_tests,
install_dir: installed_tests_dir,
)
test(
'unit/xdp-method-info',
test_method_info,
suite: ['unit'],
env: env_tests,
is_parallel: true,
protocol: test_protocol,
)
run_test = find_program('run-test.sh')
pytest_args = ['--verbose', '--log-level=DEBUG']
pytest_env = environment()
pytest_env.set('BUILDDIR', meson.project_build_root())
# pytest xdist is nice because it significantly speeds up our
# test process, but it's not required
if pymod.find_installation('python3', modules: ['xdist'], required: false).found()
# using auto can easily start too many tests which will block each other
# a value of around 5 seems to work well
pytest_args += ['-n', '5']
endif
pytest_files = [
'test_account.py',
'test_background.py',
'test_camera.py',
'test_clipboard.py',
'test_documents.py',
'test_document_fuse.py',
'test_dynamiclauncher.py',
'test_email.py',
'test_filechooser.py',
'test_globalshortcuts.py',
'test_inhibit.py',
'test_inputcapture.py',
'test_location.py',
'test_notification.py',
'test_openuri.py',
'test_permission_store.py',
'test_print.py',
'test_registry.py',
'test_remotedesktop.py',
'test_settings.py',
'test_screenshot.py',
'test_trash.py',
'test_usb.py',
'test_wallpaper.py',
]
template_files = [
'templates/access.py',
'templates/account.py',
'templates/appchooser.py',
'templates/background.py',
'templates/clipboard.py',
'templates/dynamiclauncher.py',
'templates/email.py',
'templates/filechooser.py',
'templates/geoclue2.py',
'templates/globalshortcuts.py',
'templates/inhibit.py',
'templates/__init__.py',
'templates/inputcapture.py',
'templates/lockdown.py',
'templates/notification.py',
'templates/print.py',
'templates/remotedesktop.py',
'templates/screenshot.py',
'templates/settings.py',
'templates/usb.py',
'templates/wallpaper.py',
]
foreach pytest_file : pytest_files
testname = pytest_file.replace('.py', '').replace('test_', '')
test(
'integration/@0@'.format(testname),
run_test,
args: [meson.current_source_dir() / pytest_file] + pytest_args,
env: pytest_env,
suite: ['integration'],
timeout: 120,
)
endforeach
if enable_installed_tests
install_data(
pytest_files,
'__init__.py',
'conftest.py',
'asan.suppression',
install_dir: installed_tests_dir / 'tests',
)
install_data(
template_files,
install_dir: installed_tests_dir / 'tests' / 'templates',
)
installed_env = {
'XDG_DESKTOP_PORTAL_PATH': libexecdir / 'xdg-desktop-portal',
'XDG_PERMISSION_STORE_PATH': libexecdir / 'xdg-permission-store',
'XDG_DOCUMENT_PORTAL_PATH': libexecdir / 'xdg-document-portal',
'XDP_VALIDATE_AUTO': '1',
}
env = ''
foreach key, value : installed_env
env += f'@key@=@value@ '
endforeach
foreach pytest_file : pytest_files
testname = pytest_file.replace('.py', '').replace('test_', '')
exec = [pytest.full_path(), installed_tests_dir / 'tests' / pytest_file]
exec += pytest_args
exec += ['-p', 'no:cacheprovider']
exec = ' '.join(exec)
data = configuration_data()
data.set('exec', exec)
data.set('env', env)
data.set('libdir', libdir)
configure_file(
input: 'template.test.in',
output: 'integration-@0@.test'.format(testname),
configuration: data,
install: true,
install_dir: installed_tests_data_dir,
)
endforeach
endif

View File

@@ -0,0 +1,44 @@
#!/bin/bash
#
# - Runs pytest with the required environment to run tests on an x-d-p build
# - By default, the tests run on the first x-d-p build directory that is found
# inside the source tree
# - The BUILDDIR environment variable can be set to a specific x-d-p build
# directory
# - All arguments are passed along to pytest
# - Check tests/README.md for useful environment variables
#
# Examples:
#
# ./run-test.sh ./test_camera.py -k test_version -v -s
#
# BUILDDIR=../_build ./run-test.sh ./test_usb.py
#
set -euo pipefail
function fail()
{
sed -n '/^#$/,/^$/p' "${BASH_SOURCE[0]}"
echo "$1"
exit 1
}
SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
PYTEST=$(command -v "pytest-3" || command -v "pytest") || fail "pytest is missing"
BUILDDIR=${BUILDDIR:-$(find "${SCRIPT_DIR}/.." -maxdepth 2 -name "build.ninja" -printf "%h\n" -quit)}
[ ! -f "${BUILDDIR}/build.ninja" ] && fail "Path '${BUILDDIR}' does not appear to be a build dir"
echo "Running tests on build dir: $(readlink -f "${BUILDDIR}")"
echo ""
export XDP_VALIDATE_SOUND="$BUILDDIR/src/xdg-desktop-portal-validate-sound"
export XDP_VALIDATE_ICON="$BUILDDIR/src/xdg-desktop-portal-validate-icon"
export XDG_DESKTOP_PORTAL_PATH="$BUILDDIR/src/xdg-desktop-portal"
export XDG_DOCUMENT_PORTAL_PATH="$BUILDDIR/document-portal/xdg-document-portal"
export XDG_PERMISSION_STORE_PATH="$BUILDDIR/document-portal/xdg-permission-store"
exec "$PYTEST" "$@"

View File

@@ -0,0 +1,3 @@
[Test]
Type=session
Exec=env LSAN_OPTIONS=exitcode=0 LD_LIBRARY_PATH=@libdir@:$LD_LIBRARY_PATH @env@ @exec@

View File

@@ -0,0 +1,286 @@
# SPDX-License-Identifier: LGPL-2.1-or-later
#
# This file is formatted with Python Black
from typing import Callable, Dict, Optional, NamedTuple
from gi.repository import GLib
import dbus
import dbusmock
import logging
def init_logger(name: str) -> logging.Logger:
"""
Common logging setup for the impl.portal templates. Use as:
>>> from tests.templates import init_logger
>>> logger = init_logger(__name__)
>>> logger.debug("foo")
"""
logging.basicConfig(
format="%(levelname).1s|%(name)s: %(message)s", level=logging.DEBUG
)
logger = logging.getLogger(f"templates.{name}")
logger.setLevel(logging.DEBUG)
return logger
logger = init_logger("utils")
class Response(NamedTuple):
response: int
results: Dict
class ImplRequest:
"""
Implementation of an ``org.freedesktop.impl.portal.Request`` object exposed
on the object path ``handle``.
The dbus method implementations need to be invoked asynchronously and the
async callbacks must be passed in ``cb_success`` and ``cb_error``.
The request either waits until it is closed by x-d-p (``wait_for_close``) or
responds to the request (``respond``).
"""
def __init__(
self,
mock: dbusmock.DBusMockObject,
busname: str,
handle: str,
logger: logging.Logger,
cb_success: Callable,
cb_error: Callable,
):
self.mock = mock
bus = mock.connection
proxy = bus.get_object(busname, handle)
self.mock_interface = dbus.Interface(proxy, dbusmock.MOCK_IFACE)
self.handle = handle
self.logger = logger
self.cb_success = cb_success
self.cb_error = cb_error
def respond(
self,
response: Callable | Response,
delay: int = 0,
done_cb: Callable | None = None,
) -> None:
def reply():
nonlocal response
res = None
logger.debug(f"Request {self.handle}: trying to reply")
if callable(response):
try:
res = response()
except Exception as e:
logger.critical(
f"Request {self.handle}: failed getting response: {e}"
)
self.cb_error(e)
self._unexport()
return
else:
res = response
assert res
logger.debug(f"Request {self.handle}: replying {res}")
self.cb_success(res.response, res.results)
self._unexport()
if done_cb:
done_cb()
self._export()
if delay > 0:
logger.debug(f"Request {self.handle}: scheduling delay of {delay}ms")
GLib.timeout_add(delay, reply)
else:
reply()
def wait_for_close(
self,
close_callback: Callable | None = None,
) -> None:
def closed():
logger.debug(f"Request {self.handle}: closed")
self.mock.EmitSignal(
"org.freedesktop.impl.portal.Mock",
"RequestClosed",
"s",
(self.handle,),
)
if close_callback:
try:
close_callback()
except Exception as e:
logger.critical(
f"Request {self.handle}: failed running close callback: {e}"
)
self.cb_error(e)
self._unexport()
return
response = Response(2, {})
self.cb_success(response.response, response.results)
self._unexport()
def cb_methodcall(name, args):
if name == "Close":
closed()
self._export()
logger.debug(f"Request {self.handle}: waiting for x-d-p to call close")
self.mock_interface.connect_to_signal("MethodCalled", cb_methodcall)
def _export(self):
# In the future we can pass a class extending
# dbusmock.mockobject.DBusMockObject as mock_class to avoid going
# through the mock MethodCalled signal
self.mock.AddObject(
path=self.handle,
interface="org.freedesktop.impl.portal.Request",
properties={},
methods=[
(
"Close",
"",
"",
"",
)
],
)
def _unexport(self):
self.mock.RemoveObject(self.handle)
def __str__(self):
return f"ImplRequest {self.handle}"
class ImplSession:
"""
Implementation of a org.freedesktop.impl.portal.Session object. Do not
instantiate this directly, instead use ``ImplSession.export()``. Typically
like this:
>>> s = ImplSession.export(mock, "org.freedesktop.impl.portal.Test", "/path/foo")
Where the test or the backend implementation relies on the Closed() method
of the ImplSession, provide a callback to be invoked.
>>> r.export(close_callback=my_callback)
Note that the latter only works if the test invokes methods
asynchronously.
.. attribute:: closed
Set to True if the Close() method on the Session was invoked
.. attribute:: handle
The session's object path
"""
def __init__(
self,
mock: dbusmock.DBusMockObject,
busname: str,
handle: str,
app_id: str,
):
self.mock = mock # the main mock object
self.handle = handle
self.app_id = app_id
self.closed = False
self._close_callback: Optional[Callable] = None
self.mock_object: Optional[dbusmock.DBusMockObject] = None
bus = mock.connection
proxy = bus.get_object(busname, handle)
mock_interface = dbus.Interface(proxy, dbusmock.MOCK_IFACE)
# Register for the Close() call on the impl.Session. If it gets
# called, use the side-channel SessionClosed signal so we can notify
# the test that the impl.Session was actually closed by the
# xdg-desktop-portal
def cb_methodcall(name, args):
if name == "Close":
self.closed = True
logger.debug(f"Session.Close() on {self.handle}")
if self._close_callback:
self._close_callback()
self.mock.EmitSignal(
"org.freedesktop.impl.portal.Mock",
"SessionClosed",
"s",
(self.handle,),
)
self._unexport()
mock_interface.connect_to_signal("MethodCalled", cb_methodcall)
def export(
self,
close_callback: Optional[Callable] = None,
) -> "ImplSession":
"""
Create the session on the bus. If ``close_callback`` is not None, that
callback will be invoked in response to the Close() method called on
this object.
"""
self.mock.AddObject(
path=self.handle,
interface="org.freedesktop.impl.portal.Session",
properties={},
methods=[
(
"Close",
"",
"",
"",
)
],
)
# This is a bit awkward. We need our session's DBusMockObject for
# EmitSignal of impl.portal.Session.Close. This is available in
# dbusmock.get_object() since our template runs as part of the server.
#
# In theory, EmitSignal should work on self.mock_interface but
# it doesn't and I can't figure out why.
self.mock_object = dbusmock.get_object(self.handle)
self._close_callback = close_callback
return self
def _unexport(self):
self.mock.RemoveObject(path=self.handle)
def close(self):
"""
Send out Closed signal and remove this session from the bus.
"""
assert self.mock_object is not None, "Session was never exported"
logger.debug(f"Signal Session.Closed on {self.handle}")
self.mock_object.EmitSignal(
interface="org.freedesktop.impl.portal.Session",
name="Closed",
signature="",
sigargs=(),
)
self.closed = True
self._unexport()
def __str__(self):
return f"ImplSession {self.handle}"

View File

@@ -0,0 +1,74 @@
# SPDX-License-Identifier: LGPL-2.1-or-later
#
# This file is formatted with Python Black
# mypy: disable-error-code="misc"
from tests.templates import Response, init_logger, ImplRequest
import dbus.service
from dataclasses import dataclass
BUS_NAME = "org.freedesktop.impl.portal.Test"
MAIN_OBJ = "/org/freedesktop/portal/desktop"
SYSTEM_BUS = False
MAIN_IFACE = "org.freedesktop.impl.portal.Access"
logger = init_logger(__name__)
@dataclass
class AccessParameters:
delay: int
response: int
expect_close: bool
def load(mock, parameters={}):
logger.debug(f"Loading parameters: {parameters}")
assert not hasattr(mock, "access_params")
mock.access_params = AccessParameters(
delay=parameters.get("delay", 200),
response=parameters.get("response", 0),
expect_close=parameters.get("expect-close", False),
)
@dbus.service.method(
MAIN_IFACE,
in_signature="osssssa{sv}",
out_signature="ua{sv}",
async_callbacks=("cb_success", "cb_error"),
)
def AccessDialog(
self,
handle,
app_id,
parent_window,
title,
subtitle,
body,
options,
cb_success,
cb_error,
):
logger.debug(
f"AccessDialog({handle}, {app_id}, {parent_window}, {title}, {subtitle}, {body}, {options})"
)
params = self.access_params
request = ImplRequest(
self,
BUS_NAME,
handle,
logger,
cb_success,
cb_error,
)
if params.expect_close:
request.wait_for_close()
else:
request.respond(Response(params.response, {}), delay=params.delay)

View File

@@ -0,0 +1,64 @@
# SPDX-License-Identifier: LGPL-2.1-or-later
#
# This file is formatted with Python Black
# mypy: disable-error-code="misc"
from tests.templates import Response, init_logger, ImplRequest
import dbus.service
import dbus
from dataclasses import dataclass
BUS_NAME = "org.freedesktop.impl.portal.Test"
MAIN_OBJ = "/org/freedesktop/portal/desktop"
SYSTEM_BUS = False
MAIN_IFACE = "org.freedesktop.impl.portal.Account"
logger = init_logger(__name__)
@dataclass
class AccountParameters:
delay: int
response: int
results: dict
expect_close: bool
def load(mock, parameters={}):
logger.debug(f"Loading parameters: {parameters}")
assert not hasattr(mock, "account_params")
mock.account_params = AccountParameters(
delay=parameters.get("delay", 200),
response=parameters.get("response", 0),
results=parameters.get("results", {}),
expect_close=parameters.get("expect-close", False),
)
@dbus.service.method(
MAIN_IFACE,
in_signature="ossa{sv}",
out_signature="ua{sv}",
async_callbacks=("cb_success", "cb_error"),
)
def GetUserInformation(self, handle, app_id, window, options, cb_success, cb_error):
logger.debug(f"GetUserInformation({handle}, {app_id}, {window}, {options})")
params = self.account_params
request = ImplRequest(
self,
BUS_NAME,
handle,
logger,
cb_success,
cb_error,
)
if params.expect_close:
request.wait_for_close()
else:
request.respond(Response(params.response, params.results), delay=params.delay)

View File

@@ -0,0 +1,84 @@
# SPDX-License-Identifier: LGPL-2.1-or-later
#
# This file is formatted with Python Black
# mypy: disable-error-code="misc"
from tests.templates import Response, init_logger, ImplRequest
import dbus.service
from dataclasses import dataclass
BUS_NAME = "org.freedesktop.impl.portal.Test"
MAIN_OBJ = "/org/freedesktop/portal/desktop"
SYSTEM_BUS = False
MAIN_IFACE = "org.freedesktop.impl.portal.AppChooser"
VERSION = 2
logger = init_logger(__name__)
@dataclass
class AppchooserParameters:
delay: int
response: int
expect_close: bool
def load(mock, parameters={}):
logger.debug(f"Loading parameters: {parameters}")
assert not hasattr(mock, "appchooser_params")
mock.appchooser_params = AppchooserParameters(
delay=parameters.get("delay", 200),
response=parameters.get("response", 0),
expect_close=parameters.get("expect-close", False),
)
mock.AddProperties(
MAIN_IFACE,
dbus.Dictionary(
{
"version": dbus.UInt32(parameters.get("version", VERSION)),
}
),
)
@dbus.service.method(
MAIN_IFACE,
in_signature="ossasa{sv}",
out_signature="ua{sv}",
async_callbacks=("cb_success", "cb_error"),
)
def ChooseApplication(
self, handle, app_id, parent_window, choices, options, cb_success, cb_error
):
logger.debug(
f"ChooseApplication({handle}, {app_id}, {parent_window}, {choices}, {options})"
)
params = self.appchooser_params
request = ImplRequest(
self,
BUS_NAME,
handle,
logger,
cb_success,
cb_error,
)
if params.expect_close:
request.wait_for_close()
else:
request.respond(Response(params.response, {}), delay=params.delay)
@dbus.service.method(
MAIN_IFACE,
in_signature="oas",
out_signature="",
)
def UpdateChoices(self, handle, choices):
logger.debug(f"UpdateChoices({handle}, {choices})")

View File

@@ -0,0 +1,76 @@
# SPDX-License-Identifier: LGPL-2.1-or-later
#
# This file is formatted with Python Black
# mypy: disable-error-code="misc"
from tests.templates import init_logger
import dbus.service
import dbus
from gi.repository import GLib
from dataclasses import dataclass
BUS_NAME = "org.freedesktop.impl.portal.Test"
MAIN_OBJ = "/org/freedesktop/portal/desktop"
SYSTEM_BUS = False
MAIN_IFACE = "org.freedesktop.impl.portal.Background"
VERSION = 1
logger = init_logger(__name__)
@dataclass
class BackgroundParameters:
delay: int
def load(mock, parameters={}):
logger.debug(f"Loading parameters: {parameters}")
assert not hasattr(mock, "background_params")
mock.background_params = BackgroundParameters(
delay=parameters.get("delay", 200),
)
@dbus.service.method(
MAIN_IFACE,
in_signature="",
out_signature="a{sv}",
async_callbacks=("cb_success", "cb_error"),
)
def GetAppState(self, cb_success, cb_error):
logger.debug("GetAppState()")
params = self.background_params
# FIXME: implement?
def reply():
cb_success({})
logger.debug(f"scheduling delay of {params.delay}")
GLib.timeout_add(params.delay, reply)
@dbus.service.method(
MAIN_IFACE,
in_signature="oss",
out_signature="ua{sv}",
async_callbacks=("cb_success", "cb_error"),
)
def NotifyBackground(self, handle, app_id, name, cb_success, cb_error):
logger.debug(f"NotifyBackground({handle}, {app_id}, {name})")
params = self.background_params
logger.debug(f"scheduling delay of {params.delay}")
GLib.timeout_add(params.delay, cb_success)
@dbus.service.method(
MAIN_IFACE,
in_signature="sbasu",
out_signature="b",
)
def EnableAutostart(self, app_id, enable, commandline, flags):
raise dbus.exceptions.DBusException("EnableAutostart is deprecated")

View File

@@ -0,0 +1,168 @@
# SPDX-License-Identifier: LGPL-2.1-or-later
#
# This file is formatted with Python Black
# mypy: disable-error-code="misc"
from tests.templates import init_logger
import dbus.service
import dbus
import tempfile
from gi.repository import GLib
from dataclasses import dataclass
BUS_NAME = "org.freedesktop.impl.portal.Test"
MAIN_OBJ = "/org/freedesktop/portal/desktop"
SYSTEM_BUS = False
MAIN_IFACE = "org.freedesktop.impl.portal.Clipboard"
VERSION = 1
logger = init_logger(__name__)
@dataclass
class ClipboardParameters:
delay: int
response: int
expect_close: bool
def load(mock, parameters={}):
logger.debug(f"Loading parameters: {parameters}")
assert not hasattr(mock, "clipboard_params")
mock.clipboard_params = ClipboardParameters(
delay=parameters.get("delay", 200),
response=parameters.get("response", 0),
expect_close=parameters.get("expect-close", False),
)
mock.AddProperties(
MAIN_IFACE,
dbus.Dictionary(
{
"version": dbus.UInt32(parameters.get("version", VERSION)),
}
),
)
@dbus.service.method(
MAIN_IFACE,
in_signature="oa{sv}",
out_signature="",
async_callbacks=("cb_success", "cb_error"),
)
def RequestClipboard(self, session_handle, options, cb_success, cb_error):
try:
logger.debug(f"RequestClipboard({session_handle}, {options})")
params = self.clipboard_params
if params.expect_close:
cb_success()
else:
logger.debug(f"scheduling delay of {params.delay}")
GLib.timeout_add(params.delay, cb_success)
except Exception as e:
logger.critical(e)
cb_error(e)
@dbus.service.method(
MAIN_IFACE,
in_signature="oa{sv}",
out_signature="",
async_callbacks=("cb_success", "cb_error"),
)
def SetSelection(self, session_handle, options, cb_success, cb_error):
try:
logger.debug(f"SetSelection({session_handle}, {options})")
params = self.clipboard_params
if params.expect_close:
cb_success()
else:
logger.debug(f"scheduling delay of {params.delay}")
GLib.timeout_add(params.delay, cb_success)
except Exception as e:
logger.critical(e)
cb_error(e)
@dbus.service.method(
MAIN_IFACE,
in_signature="ou",
out_signature="h",
async_callbacks=("cb_success", "cb_error"),
)
def SelectionWrite(self, session_handle, serial, cb_success, cb_error):
try:
logger.debug(f"SelectionWrite({session_handle}, {serial})")
params = self.clipboard_params
temp_file = tempfile.TemporaryFile()
fd = dbus.types.UnixFd(temp_file.fileno())
if params.expect_close:
cb_success(fd)
else:
def reply():
cb_success(fd)
logger.debug(f"scheduling delay of {params.delay}")
GLib.timeout_add(params.delay, reply)
except Exception as e:
logger.critical(e)
cb_error(e)
@dbus.service.method(
MAIN_IFACE,
in_signature="oub",
out_signature="",
async_callbacks=("cb_success", "cb_error"),
)
def SelectionWriteDone(self, session_handle, serial, success, cb_success, cb_error):
try:
logger.debug(f"SelectionWriteDone({session_handle}, {serial}, {success})")
params = self.clipboard_params
if params.expect_close:
cb_success()
else:
logger.debug(f"scheduling delay of {params.delay}")
GLib.timeout_add(params.delay, cb_success)
except Exception as e:
logger.critical(e)
cb_error(e)
@dbus.service.method(
MAIN_IFACE,
in_signature="os",
out_signature="h",
async_callbacks=("cb_success", "cb_error"),
)
def SelectionRead(self, session_handle, mime_type, cb_success, cb_error):
try:
logger.debug(f"SelectionRead({session_handle}, {mime_type})")
params = self.clipboard_params
temp_file = tempfile.TemporaryFile()
fd = dbus.types.UnixFd(temp_file.fileno())
if params.expect_close:
cb_success(fd)
else:
def reply():
cb_success(fd)
logger.debug(f"scheduling delay of {params.delay}")
GLib.timeout_add(params.delay, reply)
except Exception as e:
logger.critical(e)
cb_error(e)

View File

@@ -0,0 +1,85 @@
# SPDX-License-Identifier: LGPL-2.1-or-later
#
# This file is formatted with Python Black
# mypy: disable-error-code="misc"
from tests.templates import Response, init_logger, ImplRequest
import dbus.service
from dataclasses import dataclass
BUS_NAME = "org.freedesktop.impl.portal.Test"
MAIN_OBJ = "/org/freedesktop/portal/desktop"
SYSTEM_BUS = False
MAIN_IFACE = "org.freedesktop.impl.portal.DynamicLauncher"
VERSION = 1
logger = init_logger(__name__)
@dataclass
class DynamiclauncherParameters:
delay: int
response: int
expect_close: bool
launcher_name: str
def load(mock, parameters={}):
logger.debug(f"Loading parameters: {parameters}")
assert not hasattr(mock, "dynamiclauncher_params")
mock.dynamiclauncher_params = DynamiclauncherParameters(
delay=parameters.get("delay", 200),
response=parameters.get("response", 0),
expect_close=parameters.get("expect-close", False),
launcher_name=parameters.get("launcher-name", None),
)
mock.AddProperties(
MAIN_IFACE,
dbus.Dictionary(
{
"version": dbus.UInt32(parameters.get("version", VERSION)),
}
),
)
@dbus.service.method(
MAIN_IFACE,
in_signature="osssva{sv}",
out_signature="ua{sv}",
async_callbacks=("cb_success", "cb_error"),
)
def PrepareInstall(
self, handle, app_id, parent_window, name, icon_v, options, cb_success, cb_error
):
logger.debug(
f"PrepareInstall({handle}, {app_id}, {parent_window}, {name}, {icon_v}, {options})"
)
params = self.dynamiclauncher_params
request = ImplRequest(
self,
BUS_NAME,
handle,
logger,
cb_success,
cb_error,
)
response = Response(
params.response,
{
"name": params.launcher_name if params.launcher_name else name,
"icon": dbus.Struct(list(icon_v), signature="sv", variant_level=2),
},
)
if params.expect_close:
request.wait_for_close()
else:
request.respond(response, delay=params.delay)

View File

@@ -0,0 +1,71 @@
# SPDX-License-Identifier: LGPL-2.1-or-later
#
# This file is formatted with Python Black
# mypy: disable-error-code="misc"
from tests.templates import Response, init_logger, ImplRequest
import dbus.service
from dataclasses import dataclass
BUS_NAME = "org.freedesktop.impl.portal.Test"
MAIN_OBJ = "/org/freedesktop/portal/desktop"
SYSTEM_BUS = False
MAIN_IFACE = "org.freedesktop.impl.portal.Email"
VERSION = 3
logger = init_logger(__name__)
@dataclass
class EmailParameters:
delay: int
response: int
expect_close: bool
def load(mock, parameters={}):
logger.debug(f"Loading parameters: {parameters}")
assert not hasattr(mock, "email_params")
mock.email_params = EmailParameters(
delay=parameters.get("delay", 200),
response=parameters.get("response", 0),
expect_close=parameters.get("expect-close", False),
)
mock.AddProperties(
MAIN_IFACE,
dbus.Dictionary(
{
"version": dbus.UInt32(parameters.get("version", VERSION)),
}
),
)
@dbus.service.method(
MAIN_IFACE,
in_signature="ossa{sv}",
out_signature="ua{sv}",
async_callbacks=("cb_success", "cb_error"),
)
def ComposeEmail(self, handle, app_id, parent_window, options, cb_success, cb_error):
logger.debug(f"ComposeEmail({handle}, {app_id}, {parent_window}, {options})")
params = self.email_params
request = ImplRequest(
self,
BUS_NAME,
handle,
logger,
cb_success,
cb_error,
)
if params.expect_close:
request.wait_for_close()
else:
request.respond(Response(params.response, {}), delay=params.delay)

View File

@@ -0,0 +1,98 @@
# SPDX-License-Identifier: LGPL-2.1-or-later
#
# This file is formatted with Python Black
# mypy: disable-error-code="misc"
from tests.templates import Response, init_logger, ImplRequest
import dbus.service
from dataclasses import dataclass
BUS_NAME = "org.freedesktop.impl.portal.Test"
MAIN_OBJ = "/org/freedesktop/portal/desktop"
SYSTEM_BUS = False
MAIN_IFACE = "org.freedesktop.impl.portal.FileChooser"
VERSION = 4
logger = init_logger(__name__)
@dataclass
class FilechooserParameters:
delay: int
response: int
results: dict
expect_close: bool
def load(mock, parameters={}):
logger.debug(f"Loading parameters: {parameters}")
assert not hasattr(mock, "filechooser_params")
mock.filechooser_params = FilechooserParameters(
delay=parameters.get("delay", 200),
response=parameters.get("response", 0),
results=parameters.get("results", {}),
expect_close=parameters.get("expect-close", False),
)
mock.AddProperties(
MAIN_IFACE,
dbus.Dictionary(
{
"version": dbus.UInt32(parameters.get("version", VERSION)),
}
),
)
@dbus.service.method(
MAIN_IFACE,
in_signature="osssa{sv}",
out_signature="ua{sv}",
async_callbacks=("cb_success", "cb_error"),
)
def OpenFile(self, handle, app_id, parent_window, title, options, cb_success, cb_error):
logger.debug(f"OpenFile({handle}, {app_id}, {parent_window}, {title}, {options})")
params = self.filechooser_params
request = ImplRequest(
self,
BUS_NAME,
handle,
logger,
cb_success,
cb_error,
)
if params.expect_close:
request.wait_for_close()
else:
request.respond(Response(params.response, params.results), delay=params.delay)
@dbus.service.method(
MAIN_IFACE,
in_signature="osssa{sv}",
out_signature="ua{sv}",
async_callbacks=("cb_success", "cb_error"),
)
def SaveFile(self, handle, app_id, parent_window, title, options, cb_success, cb_error):
logger.debug(f"SaveFile({handle}, {app_id}, {parent_window}, {title}, {options})")
params = self.filechooser_params
request = ImplRequest(
self,
BUS_NAME,
handle,
logger,
cb_success,
cb_error,
)
if params.expect_close:
request.wait_for_close()
else:
request.respond(Response(params.response, params.results), delay=params.delay)

View File

@@ -0,0 +1,121 @@
# SPDX-License-Identifier: LGPL-2.1-or-later
# mypy: disable-error-code="misc"
from tests.templates import init_logger
import dbus.service
import dbus
from dbusmock import mockobject
BUS_NAME = "org.freedesktop.GeoClue2"
MAIN_OBJ = "/org/freedesktop/GeoClue2/Manager"
MAIN_IFACE = "org.freedesktop.GeoClue2.Manager"
CLIENT_IFACE = "org.freedesktop.GeoClue2.Client"
LOCATION_IFACE = "org.freedesktop.GeoClue2.Location"
MOCK_IFACE = "org.freedesktop.GeoClue2.Mock"
SYSTEM_BUS = True
VERSION = 1
logger = init_logger(__name__)
def load(mock, parameters={}):
mock.AddMethods(
MAIN_IFACE,
[
(
"GetClient",
"",
"o",
'ret = dbus.ObjectPath("/org/freedesktop/GeoClue2/Client/1")',
),
],
)
mock.AddObject(
"/org/freedesktop/GeoClue2/Client/1",
CLIENT_IFACE,
{
"DesktopId": "",
"DistanceThreshold": 0,
"TimeThreshold": 0,
"RequestedAccuracyLevel": 0,
},
[
("Start", "", "", Start),
("Stop", "", "", Stop),
],
)
mock.client = mockobject.objects["/org/freedesktop/GeoClue2/Client/1"]
mock.client.manager = mock
mock.client.started = False
mock.client.location = 0
mock.client.props = {"Latitude": 0, "Longitude": 0, "Accuracy": 0}
mock.client.AddMethod(MOCK_IFACE, "ChangeLocation", "a{sv}", "", ChangeLocation)
@dbus.service.method(
CLIENT_IFACE,
in_signature="",
out_signature="",
)
def Start(self):
logger.debug("Start()")
self.started = True
self.ChangeLocation(self.props)
@dbus.service.method(
CLIENT_IFACE,
in_signature="",
out_signature="",
)
def Stop(self):
logger.debug("Stop()")
self.started = False
self.RemoveObject(f"/org/freedesktop/GeoClue2/Location/{self.location}")
@dbus.service.method(
MOCK_IFACE,
in_signature="a{sv}",
out_signature="",
)
def ChangeLocation(self, props):
logger.debug(f"ChangeLocation({props})")
self.props = props
if not self.started:
return
old_path = "/"
if self.location > 0:
old_path = f"/org/freedesktop/GeoClue2/Location/{self.location}"
self.location = self.location + 1
new_path = f"/org/freedesktop/GeoClue2/Location/{self.location}"
self.AddObject(
new_path,
LOCATION_IFACE,
{
"Latitude": props["Latitude"],
"Longitude": props["Longitude"],
"Accuracy": props["Accuracy"],
},
[],
)
if old_path != "/":
self.RemoveObject(old_path)
self.EmitSignal(
CLIENT_IFACE,
"LocationUpdated",
"oo",
[
dbus.ObjectPath(old_path),
dbus.ObjectPath(new_path),
],
)

View File

@@ -0,0 +1,165 @@
# SPDX-License-Identifier: LGPL-2.1-or-later
#
# This file is formatted with Python Black
# mypy: disable-error-code="misc"
from tests.templates import Response, init_logger, ImplRequest, ImplSession
import dbus
import dbus.service
import time
from dbusmock import MOCK_IFACE
from gi.repository import GLib
from dataclasses import dataclass
BUS_NAME = "org.freedesktop.impl.portal.Test"
MAIN_OBJ = "/org/freedesktop/portal/desktop"
SYSTEM_BUS = False
MAIN_IFACE = "org.freedesktop.impl.portal.GlobalShortcuts"
VERSION = 1
logger = init_logger(__name__)
@dataclass
class GlobalshortcutsParameters:
delay: int
response: int
expect_close: bool
force_close: int
def load(mock, parameters={}):
logger.debug(f"Loading parameters: {parameters}")
assert not hasattr(mock, "globalshortcuts_params")
mock.globalshortcuts_params = GlobalshortcutsParameters(
delay=parameters.get("delay", 200),
response=parameters.get("response", 0),
expect_close=parameters.get("expect-close", False),
force_close=parameters.get("force-close", 0),
)
mock.AddProperties(
MAIN_IFACE,
dbus.Dictionary(
{
"version": dbus.UInt32(parameters.get("version", VERSION)),
}
),
)
mock.sessions: dict[str, ImplSession] = {}
@dbus.service.method(
MAIN_IFACE,
in_signature="oosa{sv}",
out_signature="ua{sv}",
async_callbacks=("cb_success", "cb_error"),
)
def CreateSession(self, handle, session_handle, app_id, options, cb_success, cb_error):
logger.debug(f"CreateSession({handle}, {session_handle}, {app_id}, {options})")
params = self.globalshortcuts_params
session = ImplSession(self, BUS_NAME, session_handle, app_id).export()
self.sessions[session_handle] = session
request = ImplRequest(
self,
BUS_NAME,
handle,
logger,
cb_success,
cb_error,
)
if params.expect_close:
request.wait_for_close()
else:
request.respond(
Response(params.response, {"session_handle": session.handle}),
delay=params.delay,
)
if params.force_close > 0:
GLib.timeout_add(params.force_close, session.close)
@dbus.service.method(
MAIN_IFACE,
in_signature="ooa(sa{sv})sa{sv}",
out_signature="ua{sv}",
async_callbacks=("cb_success", "cb_error"),
)
def BindShortcuts(
self,
handle,
session_handle,
shortcuts,
parent_window,
options,
cb_success,
cb_error,
):
logger.debug(f"BindShortcuts({handle}, {session_handle}, {shortcuts}, {options})")
params = self.globalshortcuts_params
assert session_handle in self.sessions
request = ImplRequest(
self,
BUS_NAME,
handle,
logger,
cb_success,
cb_error,
)
if params.expect_close:
request.wait_for_close()
else:
def reply():
logger.debug(f"BindShortcuts with shortcuts {shortcuts}")
self.sessions[session_handle].shortcuts = shortcuts
return Response(params.response, {})
request.respond(reply, delay=params.delay)
@dbus.service.method(
MAIN_IFACE,
in_signature="oo",
out_signature="ua{sv}",
)
def ListShortcuts(
self,
handle,
session_handle,
):
shortcuts = self.sessions[session_handle].shortcuts
return (0, {"shortcuts": shortcuts})
@dbus.service.method(
MOCK_IFACE,
in_signature="os",
out_signature="",
)
def Trigger(self, session_handle, shortcut_id):
now_since_epoch = int(time.time() * 1000000)
self.EmitSignal(
MAIN_IFACE,
"Activated",
"osta{sv}",
[session_handle, shortcut_id, now_since_epoch, {}],
)
time.sleep(0.2)
now_since_epoch = int(time.time() * 1000000)
self.EmitSignal(
MAIN_IFACE,
"Deactivated",
"osta{sv}",
[session_handle, shortcut_id, now_since_epoch, {}],
)

View File

@@ -0,0 +1,164 @@
# SPDX-License-Identifier: LGPL-2.1-or-later
#
# This file is formatted with Python Black
# mypy: disable-error-code="misc"
from tests.templates import Response, init_logger, ImplRequest, ImplSession
import dbus.service
from gi.repository import GLib
from enum import Enum
from dbusmock import MOCK_IFACE
from dataclasses import dataclass
BUS_NAME = "org.freedesktop.impl.portal.Test"
MAIN_OBJ = "/org/freedesktop/portal/desktop"
SYSTEM_BUS = False
MAIN_IFACE = "org.freedesktop.impl.portal.Inhibit"
VERSION = 3
logger = init_logger(__name__)
class SessionState(Enum):
RUNNING = 1
QUERY_END = 2
ENDING = 3
@dataclass
class InhibitParameters:
delay: int
response: int
expect_close: bool
def load(mock, parameters={}):
logger.debug(f"Loading parameters: {parameters}")
assert not hasattr(mock, "inhibit_params")
mock.inhibit_params = InhibitParameters(
delay=parameters.get("delay", 200),
response=parameters.get("response", 0),
expect_close=parameters.get("expect-close", False),
)
mock.AddProperties(
MAIN_IFACE,
dbus.Dictionary(
{
"version": dbus.UInt32(parameters.get("version", VERSION)),
}
),
)
mock.sessions: dict[str, ImplSession] = {}
mock.session_timers = {}
@dbus.service.method(
MAIN_IFACE,
in_signature="ossua{sv}",
out_signature="",
async_callbacks=("cb_success", "cb_error"),
)
def Inhibit(self, handle, app_id, window, flags, options, cb_success, cb_error):
logger.debug(f"Inhibit({handle}, {app_id}, {window}, {flags}, {options})")
params = self.inhibit_params
request = ImplRequest(
self,
BUS_NAME,
handle,
logger,
cb_success,
cb_error,
)
if params.expect_close:
request.wait_for_close()
else:
request.respond(Response(params.response, {}), delay=params.delay)
@dbus.service.method(
MOCK_IFACE,
in_signature="s",
out_signature="",
)
def ArmTimer(self, session_handle):
self.EmitSignal(
MAIN_IFACE,
"StateChanged",
"oa{sv}",
[
session_handle,
{
"screensaver-active": False,
"session-state": SessionState.QUERY_END.value,
},
],
)
def close_session():
session = self.sessions[session_handle]
session.close()
self.sessions[session_handle] = None
if session_handle in self.session_timers:
GLib.source_remove(self.session_timers[session_handle])
self.session_timers[session_handle] = GLib.timeout_add(700, close_session)
@dbus.service.method(
MAIN_IFACE,
in_signature="ooss",
out_signature="u",
async_callbacks=("cb_success", "cb_error"),
)
def CreateMonitor(self, handle, session_handle, app_id, window, cb_success, cb_error):
logger.debug(f"CreateMonitor({handle}, {session_handle}, {app_id}, {window})")
params = self.inhibit_params
session = ImplSession(self, BUS_NAME, session_handle, app_id).export()
self.sessions[session_handle] = session
# This is irregular: the backend doesn't return the results vardict
def internal_cb_success(response, results):
cb_success(response)
request = ImplRequest(
self,
BUS_NAME,
handle,
logger,
internal_cb_success,
cb_error,
)
if params.expect_close:
request.wait_for_close()
else:
def arm_timer():
self.ArmTimer(session_handle)
request.respond(
Response(params.response, {}), delay=params.delay, done_cb=arm_timer
)
@dbus.service.method(
MAIN_IFACE,
in_signature="o",
out_signature="",
)
def QueryEndResponse(self, session_handle):
try:
logger.debug(f"QueryEndResponse({session_handle})")
self.ArmTimer(session_handle)
except Exception as e:
logger.critical(e)

View File

@@ -0,0 +1,336 @@
# SPDX-License-Identifier: LGPL-2.1-or-later
#
# This file is formatted with Python Black
# mypy: disable-error-code="misc"
from tests.templates import Response, init_logger
from collections import namedtuple
from itertools import count
from gi.repository import GLib
from dataclasses import dataclass
import dbus
import dbus.service
import socket
BUS_NAME = "org.freedesktop.impl.portal.Test"
MAIN_OBJ = "/org/freedesktop/portal/desktop"
SYSTEM_BUS = False
MAIN_IFACE = "org.freedesktop.impl.portal.InputCapture"
VERSION = 1
logger = init_logger(__name__)
serials = count()
Barrier = namedtuple("Barrier", ["id", "position"])
@dataclass
class InputcaptureParameters:
delay: int
supported_capabilities: int
capabilities: int
default_zone: list
disable_delay: int
activated_delay: int
deactivated_delay: int
zones_changed_delay: int
def load(mock, parameters={}):
logger.debug(f"Loading parameters: {parameters}")
assert not hasattr(mock, "inputcapture_params")
mock.inputcapture_params = InputcaptureParameters(
delay=parameters.get("delay", 0),
supported_capabilities=parameters.get("supported_capabilities", 0xF),
capabilities=parameters.get("capabilities", None),
default_zone=parameters.get("default-zone", [(1920, 1080, 0, 0)]),
disable_delay=parameters.get("disable-delay", 0),
activated_delay=parameters.get("activated-delay", 0),
deactivated_delay=parameters.get("deactivated-delay", 0),
zones_changed_delay=parameters.get("zones-changed-delay", 0),
)
mock.current_zones = mock.inputcapture_params.default_zone
mock.current_zone_set = next(serials)
mock.AddProperties(
MAIN_IFACE,
dbus.Dictionary(
{
"version": dbus.UInt32(parameters.get("version", VERSION)),
"SupportedCapabilities": dbus.UInt32(
mock.inputcapture_params.supported_capabilities
),
}
),
)
mock.active_session_handles = []
@dbus.service.method(
MAIN_IFACE,
in_signature="oossa{sv}",
out_signature="ua{sv}",
)
def CreateSession(self, handle, session_handle, app_id, parent_window, options):
try:
logger.debug(f"CreateSession({parent_window}, {options})")
params = self.inputcapture_params
assert "capabilities" in options
# Filter to the subset of supported capabilities
if params.capabilities is None:
capabilities = options["capabilities"]
else:
capabilities = params.capabilities
capabilities &= params.supported_capabilities
response = Response(0, {})
response.results["capabilities"] = dbus.UInt32(capabilities)
self.active_session_handles.append(session_handle)
logger.debug(f"CreateSession with response {response}")
return response.response, response.results
except Exception as e:
logger.critical(e)
return (2, {})
@dbus.service.method(
MAIN_IFACE,
in_signature="oosa{sv}",
out_signature="ua{sv}",
)
def GetZones(self, handle, session_handle, app_id, options):
try:
logger.debug(f"GetZones({session_handle}, {options})")
params = self.inputcapture_params
assert session_handle in self.active_session_handles
response = Response(0, {})
response.results["zones"] = params.default_zone
response.results["zone_set"] = dbus.UInt32(
self.current_zone_set, variant_level=1
)
logger.debug(f"GetZones with response {response}")
if response.response == 0:
self.current_zones = response.results["zones"]
return response.response, response.results
except Exception as e:
logger.critical(e)
return (2, {})
@dbus.service.method(
MAIN_IFACE,
in_signature="oosa{sv}aa{sv}u",
out_signature="ua{sv}",
)
def SetPointerBarriers(
self, handle, session_handle, app_id, options, barriers, zone_set
):
try:
logger.debug(
f"SetPointerBarriers({session_handle}, {options}, {barriers}, {zone_set})"
)
assert session_handle in self.active_session_handles
assert zone_set == self.current_zone_set
self.current_barriers = []
failed_barriers = []
# Barrier sanity checks:
for b in barriers:
id = b["barrier_id"]
x1, y1, x2, y2 = b["position"]
if (x1 != x2 and y1 != y2) or (x1 == x2 and y1 == y2):
logger.debug(f"Barrier {id} is not horizontal or vertical")
failed_barriers.append(id)
continue
for z in self.current_zones:
w, h, x, y = z
if x1 < x or x1 > x + w:
continue
if y1 < y or y1 > y + h:
continue
# x1/y1 fit into our current zone
if x2 < x or x2 > x + w or y2 < y or y2 > y + h:
logger.debug(f"Barrier {id} spans multiple zones")
elif x1 == x2 and (x1 != x and x1 != x + w):
logger.debug(f"Barrier {id} is not on vertical edge")
elif y1 == y2 and (y1 != y and y1 != y + h):
logger.debug(f"Barrier {id} is not on horizontal edge")
else:
self.current_barriers.append(Barrier(id=id, position=b["position"]))
break
failed_barriers.append(id)
break
else:
logger.debug(f"Barrier {id} does not fit into any zone")
failed_barriers.append(id)
continue
response = Response(0, {})
response.results["failed_barriers"] = dbus.Array(
[dbus.UInt32(f) for f in failed_barriers],
signature="u",
variant_level=1,
)
logger.debug(f"SetPointerBarriers with response {response}")
return response.response, response.results
except Exception as e:
logger.critical(e)
return (2, {})
@dbus.service.method(
MAIN_IFACE,
in_signature="osa{sv}",
out_signature="ua{sv}",
)
def Enable(self, session_handle, app_id, options):
try:
logger.debug(f"Enable({session_handle}, {options})")
params = self.inputcapture_params
assert session_handle in self.active_session_handles
# for use in the signals
activation_id = next(serials)
barrier = self.current_barriers[0]
pos = (barrier.position[0] + 10, barrier.position[1] + 20)
if params.disable_delay > 0:
def disable():
logger.debug("emitting Disabled")
self.EmitSignal(MAIN_IFACE, "Disabled", "oa{sv}", [session_handle, {}])
GLib.timeout_add(params.disable_delay, disable)
if params.activated_delay > 0:
def activated():
logger.debug("emitting Activated")
options = {
"activation_id": dbus.UInt32(activation_id, variant_level=1),
"barrier_id": dbus.UInt32(barrier.id, variant_level=1),
"cursor_position": dbus.Struct(
pos, signature="dd", variant_level=1
),
}
self.EmitSignal(
MAIN_IFACE, "Activated", "oa{sv}", [session_handle, options]
)
GLib.timeout_add(params.activated_delay, activated)
if params.deactivated_delay > 0:
def deactivated():
logger.debug("emitting Deactivated")
options = {
"activation_id": dbus.UInt32(activation_id, variant_level=1),
"cursor_position": dbus.Struct(
pos, signature="dd", variant_level=1
),
}
self.EmitSignal(
MAIN_IFACE, "Deactivated", "oa{sv}", [session_handle, options]
)
GLib.timeout_add(params.deactivated_delay, deactivated)
if params.zones_changed_delay > 0:
def zones_changed():
logger.debug("emitting ZonesChanged")
options = {
"zone_set": dbus.UInt32(activation_id, variant_level=1),
}
self.EmitSignal(
MAIN_IFACE, "ZonesChanged", "oa{sv}", [session_handle, options]
)
GLib.timeout_add(params.zones_changed_delay, zones_changed)
except Exception as e:
logger.critical(e)
return (2, {})
@dbus.service.method(
MAIN_IFACE,
in_signature="osa{sv}",
out_signature="ua{sv}",
)
def Disable(self, session_handle, app_id, options):
try:
logger.debug(f"Disable({session_handle}, {options})")
assert session_handle in self.active_session_handles
except Exception as e:
logger.critical(e)
return (2, {})
@dbus.service.method(
MAIN_IFACE,
in_signature="osa{sv}",
out_signature="ua{sv}",
)
def Release(self, session_handle, app_id, options):
try:
logger.debug(f"Release({session_handle}, {options})")
assert session_handle in self.active_session_handles
except Exception as e:
logger.critical(e)
return (2, {})
@dbus.service.method(
MAIN_IFACE,
in_signature="osa{sv}",
out_signature="h",
)
def ConnectToEIS(self, session_handle, app_id, options):
try:
logger.debug(f"ConnectToEIS({session_handle}, {options})")
assert session_handle in self.active_session_handles
sockets = socket.socketpair()
self.eis_socket = sockets[0]
assert self.eis_socket.send(b"HELLO") == 5
fd = sockets[1]
logger.debug(f"ConnectToEis with fd {fd.fileno()}")
return dbus.types.UnixFd(fd)
except Exception as e:
logger.critical(e)
return -1

View File

@@ -0,0 +1,48 @@
# SPDX-License-Identifier: LGPL-2.1-or-later
#
# This file is formatted with Python Black
# mypy: disable-error-code="misc"
from tests.templates import init_logger
import dbus.service
BUS_NAME = "org.freedesktop.impl.portal.Test"
MAIN_OBJ = "/org/freedesktop/portal/desktop"
SYSTEM_BUS = False
MAIN_IFACE = "org.freedesktop.impl.portal.Lockdown"
logger = init_logger(__name__)
def load(mock, parameters={}):
logger.debug(f"Loading parameters: {parameters}")
mock.AddProperties(
MAIN_IFACE,
dbus.Dictionary(
{
"disable-printing": dbus.Boolean(
parameters.get("disable-printing", False)
),
"disable-save-to-disk": dbus.Boolean(
parameters.get("disable-save-to-disk", False)
),
"disable-application-handlers": dbus.Boolean(
parameters.get("disable-application-handlers", False)
),
"disable-location": dbus.Boolean(
parameters.get("disable-location", False)
),
"disable-camera": dbus.Boolean(parameters.get("disable-camera", False)),
"disable-microphone": dbus.Boolean(
parameters.get("disable-microphone", False)
),
"disable-sound-output": dbus.Boolean(
parameters.get("disable-sound-output", False)
),
}
),
)

View File

@@ -0,0 +1,82 @@
# SPDX-License-Identifier: LGPL-2.1-or-later
#
# This file is formatted with Python Black
# mypy: disable-error-code="misc"
from tests.templates import init_logger
import dbus.service
from dbusmock import MOCK_IFACE
BUS_NAME = "org.freedesktop.impl.portal.Test"
MAIN_OBJ = "/org/freedesktop/portal/desktop"
SYSTEM_BUS = False
MAIN_IFACE = "org.freedesktop.impl.portal.Notification"
VERSION = 2
logger = init_logger(__name__)
def load(mock, parameters={}):
logger.debug(f"Loading parameters: {parameters}")
mock.AddProperties(
MAIN_IFACE,
dbus.Dictionary(
{
"version": dbus.UInt32(parameters.get("version", VERSION)),
"SupportedOptions": dbus.Dictionary(
parameters.get("SupportedOptions", {}), signature="sv"
),
},
),
)
mock.notifications = {}
@dbus.service.method(
MAIN_IFACE,
in_signature="ssa{sv}",
out_signature="",
)
def AddNotification(self, app_id, id, notification):
logger.debug(f"AddNotification({app_id}, {id}, {notification})")
self.notifications.setdefault(app_id, {})[id] = notification
@dbus.service.method(
MAIN_IFACE,
in_signature="ss",
out_signature="",
)
def RemoveNotification(self, app_id, id):
logger.debug(f"AddNotification({app_id}, {id})")
del self.notifications[app_id][id]
@dbus.service.method(
MOCK_IFACE,
in_signature="sssav",
out_signature="",
)
def EmitActionInvoked(self, app_id, id, action, parameter):
logger.debug(f"EmitActionInvoked({app_id}, {id}, {action}, {parameter})")
# n = self.notifications[app_id][id]
# FIXME check action is in n
self.EmitSignal(
MAIN_IFACE,
"ActionInvoked",
"sssav",
[
app_id,
id,
action,
dbus.Array(parameter, signature="v"),
],
)

View File

@@ -0,0 +1,119 @@
# SPDX-License-Identifier: LGPL-2.1-or-later
#
# This file is formatted with Python Black
# mypy: disable-error-code="misc"
from tests.templates import Response, init_logger, ImplRequest
import dbus.service
from dataclasses import dataclass
BUS_NAME = "org.freedesktop.impl.portal.Test"
MAIN_OBJ = "/org/freedesktop/portal/desktop"
SYSTEM_BUS = False
MAIN_IFACE = "org.freedesktop.impl.portal.Print"
VERSION = 3
logger = init_logger(__name__)
@dataclass
class PrintParameters:
delay: int
response: int
results: dict
expect_close: bool
prepare_results: dict
def load(mock, parameters={}):
logger.debug(f"Loading parameters: {parameters}")
assert not hasattr(mock, "print_params")
mock.print_params = PrintParameters(
delay=parameters.get("delay", 200),
response=parameters.get("response", 0),
results=parameters.get("results", {}),
expect_close=parameters.get("expect-close", False),
prepare_results=parameters.get("prepare-results", {}),
)
mock.AddProperties(
MAIN_IFACE,
dbus.Dictionary(
{
"version": dbus.UInt32(parameters.get("version", VERSION)),
}
),
)
@dbus.service.method(
MAIN_IFACE,
in_signature="osssa{sv}a{sv}a{sv}",
out_signature="ua{sv}",
async_callbacks=("cb_success", "cb_error"),
)
def PreparePrint(
self,
handle,
app_id,
parent_window,
title,
settings,
page_setup,
options,
cb_success,
cb_error,
):
logger.debug(
f"PreparePrint({handle}, {app_id}, {parent_window}, {title}, {settings}, {page_setup}, {options})"
)
params = self.print_params
request = ImplRequest(
self,
BUS_NAME,
handle,
logger,
cb_success,
cb_error,
)
if params.expect_close:
request.wait_for_close()
else:
request.respond(
Response(params.response, params.prepare_results), delay=params.delay
)
@dbus.service.method(
MAIN_IFACE,
in_signature="osssha{sv}",
out_signature="ua{sv}",
async_callbacks=("cb_success", "cb_error"),
)
def Print(
self, handle, app_id, parent_window, title, fd, options, cb_success, cb_error
):
logger.debug(
f"Print({handle}, {app_id}, {parent_window}, {title}, {fd}, {options})"
)
params = self.print_params
request = ImplRequest(
self,
BUS_NAME,
handle,
logger,
cb_success,
cb_error,
)
if params.expect_close:
request.wait_for_close()
else:
request.respond(Response(params.response, params.results), delay=params.delay)

View File

@@ -0,0 +1,185 @@
# SPDX-License-Identifier: LGPL-2.1-or-later
#
# This file is formatted with Python Black
# mypy: disable-error-code="misc"
from tests.templates import Response, init_logger, ImplRequest, ImplSession
from dbusmock import MOCK_IFACE
import dbus
import dbus.service
import socket
from gi.repository import GLib
from dataclasses import dataclass
BUS_NAME = "org.freedesktop.impl.portal.Test"
MAIN_OBJ = "/org/freedesktop/portal/desktop"
SYSTEM_BUS = False
MAIN_IFACE = "org.freedesktop.impl.portal.RemoteDesktop"
VERSION = 2
logger = init_logger(__name__)
@dataclass
class RemotedesktopParameters:
delay: int
response: int
expect_close: bool
force_close: int
force_clipoboard_enabled: bool
fail_connect_to_eis: bool
def load(mock, parameters={}):
logger.debug(f"Loading parameters: {parameters}")
assert not hasattr(mock, "remotedesktop_params")
mock.remotedesktop_params = RemotedesktopParameters(
delay=parameters.get("delay", 200),
response=parameters.get("response", 0),
expect_close=parameters.get("expect-close", False),
force_close=parameters.get("force-close", 0),
force_clipoboard_enabled=parameters.get("force-clipboard-enabled", False),
fail_connect_to_eis=parameters.get("fail-connect-to-eis", False),
)
mock.AddProperties(
MAIN_IFACE,
dbus.Dictionary(
{
"version": dbus.UInt32(parameters.get("version", VERSION)),
}
),
)
mock.sessions: dict[str, ImplSession] = {}
@dbus.service.method(
MAIN_IFACE,
in_signature="oosa{sv}",
out_signature="ua{sv}",
async_callbacks=("cb_success", "cb_error"),
)
def CreateSession(self, handle, session_handle, app_id, options, cb_success, cb_error):
logger.debug(f"CreateSession({handle}, {session_handle}, {app_id}, {options})")
params = self.remotedesktop_params
session = ImplSession(self, BUS_NAME, session_handle, app_id).export()
self.sessions[session_handle] = session
request = ImplRequest(
self,
BUS_NAME,
handle,
logger,
cb_success,
cb_error,
)
if params.expect_close:
request.wait_for_close()
else:
request.respond(
Response(params.response, {"session_handle": session.handle}),
delay=params.delay,
)
if params.force_close > 0:
GLib.timeout_add(params.force_close, session.close)
@dbus.service.method(
MAIN_IFACE,
in_signature="oosa{sv}",
out_signature="ua{sv}",
async_callbacks=("cb_success", "cb_error"),
)
def SelectDevices(self, handle, session_handle, app_id, options, cb_success, cb_error):
logger.debug(f"SelectDevices({handle}, {session_handle}, {app_id}, {options})")
params = self.remotedesktop_params
assert session_handle in self.sessions
request = ImplRequest(
self,
BUS_NAME,
handle,
logger,
cb_success,
cb_error,
)
if params.expect_close:
request.wait_for_close()
else:
request.respond(Response(params.response, {}), delay=params.delay)
@dbus.service.method(
MAIN_IFACE,
in_signature="oossa{sv}",
out_signature="ua{sv}",
async_callbacks=("cb_success", "cb_error"),
)
def Start(
self, handle, session_handle, app_id, parent_window, options, cb_success, cb_error
):
logger.debug(
f"Start({handle}, {session_handle}, {parent_window}, {app_id}, {options})"
)
params = self.remotedesktop_params
assert session_handle in self.sessions
request = ImplRequest(
self,
BUS_NAME,
handle,
logger,
cb_success,
cb_error,
)
response = Response(params.response, {})
if params.force_clipoboard_enabled:
response.results["clipboard_enabled"] = True
if params.expect_close:
request.wait_for_close()
else:
request.respond(response, delay=params.delay)
@dbus.service.method(
MAIN_IFACE,
in_signature="osa{sv}",
out_signature="h",
)
def ConnectToEIS(self, session_handle, app_id, options):
try:
logger.debug(f"ConnectToEIS({session_handle}, {app_id}, {options})")
params = self.remotedesktop_params
assert session_handle in self.sessions
if params.fail_connect_to_eis:
raise dbus.exceptions.DBusException("Purposely failing ConnectToEIS")
sockets = socket.socketpair()
self.eis_socket = sockets[0]
assert self.eis_socket.send(b"HELLO") == 5
return dbus.types.UnixFd(sockets[1])
except Exception as e:
logger.critical(e)
raise e
@dbus.service.method(MOCK_IFACE, in_signature="s", out_signature="s")
def GetSessionAppId(self, session_handle):
logger.debug(f"GetSessionAppId({session_handle})")
assert session_handle in self.sessions
return self.sessions[session_handle].app_id

View File

@@ -0,0 +1,98 @@
# SPDX-License-Identifier: LGPL-2.1-or-later
#
# This file is formatted with Python Black
# mypy: disable-error-code="misc"
from tests.templates import Response, init_logger, ImplRequest
import dbus.service
from dataclasses import dataclass
BUS_NAME = "org.freedesktop.impl.portal.Test"
MAIN_OBJ = "/org/freedesktop/portal/desktop"
SYSTEM_BUS = False
MAIN_IFACE = "org.freedesktop.impl.portal.Screenshot"
VERSION = 2
logger = init_logger(__name__)
@dataclass
class ScreenshotParameters:
delay: int
response: int
results: dict
expect_close: bool
def load(mock, parameters={}):
logger.debug(f"Loading parameters: {parameters}")
assert not hasattr(mock, "screenshot_params")
mock.screenshot_params = ScreenshotParameters(
delay=parameters.get("delay", 200),
response=parameters.get("response", 0),
results=parameters.get("results", {}),
expect_close=parameters.get("expect-close", False),
)
mock.AddProperties(
MAIN_IFACE,
dbus.Dictionary(
{
"version": dbus.UInt32(parameters.get("version", VERSION)),
}
),
)
@dbus.service.method(
MAIN_IFACE,
in_signature="ossa{sv}",
out_signature="ua{sv}",
async_callbacks=("cb_success", "cb_error"),
)
def Screenshot(self, handle, app_id, parent_window, options, cb_success, cb_error):
logger.debug(f"Screenshot({handle}, {app_id}, {parent_window}, {options})")
params = self.screenshot_params
request = ImplRequest(
self,
BUS_NAME,
handle,
logger,
cb_success,
cb_error,
)
if params.expect_close:
request.wait_for_close()
else:
request.respond(Response(params.response, params.results), delay=params.delay)
@dbus.service.method(
MAIN_IFACE,
in_signature="ossa{sv}",
out_signature="ua{sv}",
async_callbacks=("cb_success", "cb_error"),
)
def PickColor(self, handle, app_id, parent_window, options, cb_success, cb_error):
logger.debug(f"PickColor({handle}, {app_id}, {parent_window}, {options})")
params = self.screenshot_params
request = ImplRequest(
self,
BUS_NAME,
handle,
logger,
cb_success,
cb_error,
)
if params.expect_close:
request.wait_for_close()
else:
request.respond(Response(params.response, params.results), delay=params.delay)

View File

@@ -0,0 +1,107 @@
# SPDX-License-Identifier: LGPL-2.1-or-later
#
# This file is formatted with Python Black
# mypy: disable-error-code="misc"
from tests.templates import init_logger
import dbus.service
from dbusmock import MOCK_IFACE
from dataclasses import dataclass
BUS_NAME = "org.freedesktop.impl.portal.Test"
MAIN_OBJ = "/org/freedesktop/portal/desktop"
SYSTEM_BUS = False
MAIN_IFACE = "org.freedesktop.impl.portal.Settings"
VERSION = 2
logger = init_logger(__name__)
@dataclass
class SettingsParameters:
settings: dict
def load(mock, parameters={}):
logger.debug(f"Loading parameters: {parameters}")
assert not hasattr(mock, "settings_params")
mock.settings_params = SettingsParameters(
settings=parameters.get("settings", {}),
)
mock.AddProperties(
MAIN_IFACE,
dbus.Dictionary(
{
"version": dbus.UInt32(parameters.get("version", VERSION)),
}
),
)
@dbus.service.method(
MAIN_IFACE,
in_signature="as",
out_signature="a{sa{sv}}",
)
def ReadAll(self, namespaces):
logger.debug(f"ReadAll({namespaces})")
settings = self.settings_params.settings
if len(namespaces) == 0 or (len(namespaces) == 1 and namespaces[0] == ""):
return settings
def find_matching(namespace):
if len(namespace) >= 3 and namespace[-2:] == ".*":
ns_prefix = namespace[:-2]
matches = {}
for ns in settings:
if ns.startswith(ns_prefix):
matches[ns] = settings[ns]
return matches
if namespace in settings:
return {namespace: settings[namespace]}
return {}
result = dbus.Dictionary({}, signature="sa{sv}")
for ns in namespaces:
result |= find_matching(ns)
return result
@dbus.service.method(
MAIN_IFACE,
in_signature="ss",
out_signature="v",
)
def Read(self, namespace, key):
logger.debug(f"Read({namespace}, {key})")
settings = self.settings_params.settings
return settings[namespace][key]
@dbus.service.method(
MOCK_IFACE,
in_signature="ssv",
out_signature="",
)
def SetSetting(self, namespace, key, value):
logger.debug(f"SetSetting({namespace}, {key}, {value})")
settings = self.settings_params.settings
settings.setdefault(namespace, {})[key] = value
self.EmitSignal(
MAIN_IFACE,
"SettingChanged",
"ssv",
[namespace, key, value],
)

View File

@@ -0,0 +1,146 @@
# SPDX-License-Identifier: LGPL-2.1-or-later
#
# This file is formatted with Python Black
# mypy: disable-error-code="misc"
from tests.templates import Response, init_logger, ImplRequest
import dbus
import dbus.service
from dbusmock import MOCK_IFACE
from dataclasses import dataclass
BUS_NAME = "org.freedesktop.impl.portal.Test"
MAIN_OBJ = "/org/freedesktop/portal/desktop"
SYSTEM_BUS = False
MAIN_IFACE = "org.freedesktop.impl.portal.Usb"
VERSION = 1
logger = init_logger(__name__)
@dataclass
class UsbParameters:
delay: int
response: int
expect_close: bool
filters: dict
def load(mock, parameters={}):
logger.debug(f"Loading parameters: {parameters}")
assert not hasattr(mock, "usb_params")
mock.usb_params = UsbParameters(
delay=parameters.get("delay", 200),
response=parameters.get("response", 0),
expect_close=parameters.get("expect-close", False),
filters=parameters.get("filters", {}),
)
mock.AddProperties(
MAIN_IFACE,
dbus.Dictionary(
{
"version": dbus.UInt32(parameters.get("version", VERSION)),
}
),
)
@dbus.service.method(
MAIN_IFACE,
in_signature="ossa(sa{sv}a{sv})a{sv}",
out_signature="ua{sv}",
async_callbacks=("cb_success", "cb_error"),
)
def AcquireDevices(
self,
handle,
parent_window,
app_id,
devices,
options,
cb_success,
cb_error,
):
logger.debug(
f"AcquireDevices({handle}, {parent_window}, {app_id}, {devices}, {options})"
)
params = self.usb_params
request = ImplRequest(
self,
BUS_NAME,
handle,
logger,
cb_success,
cb_error,
)
def reply():
# no options supported
assert not options
devices_out = []
for device in devices:
(id, info, access_options) = device
props = info["properties"]
allows_writable = params.filters.get("writable", True)
needs_writable = access_options.get("writable", False)
if needs_writable and not allows_writable:
logger.debug(f"Skipping device {id} because it requires writable")
continue
needs_vendor = params.filters.get("vendor", None)
needs_vendor = int(needs_vendor, 16) if needs_vendor else None
vendor = props.get("ID_VENDOR_ID", None)
vendor = int(vendor, 16) if vendor else None
if needs_vendor is not None and needs_vendor != vendor:
logger.debug(
f"Skipping device {id} because it does not belong to vendor {needs_vendor:02x}"
)
continue
needs_model = params.filters.get("model", None)
needs_model = int(needs_model, 16) if needs_model else None
model = props.get("ID_MODEL_ID", None)
model = int(model, 16) if model else None
if needs_model is not None and needs_model != model:
logger.debug(
f"Skipping device {id} because it is not a model {needs_model:02x}"
)
continue
devices_out.append(
dbus.Struct([id, access_options], signature="sa{sv}", variant_level=1)
)
return Response(
params.response,
{"devices": dbus.Array(devices_out, signature="(sa{sv})", variant_level=1)},
)
if params.expect_close:
request.wait_for_close()
else:
request.respond(reply, delay=params.delay)
@dbus.service.method(
MOCK_IFACE,
in_signature="a{sv}",
out_signature="",
)
def SetSelectionFilters(self, filters):
logger.debug(f"SetSelectionFilters({filters})")
params = self.usb_params
params.filters = filters

View File

@@ -0,0 +1,70 @@
# SPDX-License-Identifier: LGPL-2.1-or-later
#
# This file is formatted with Python Black
# mypy: disable-error-code="misc"
from tests.templates import Response, init_logger, ImplRequest
import dbus
import dbus.service
from dataclasses import dataclass
BUS_NAME = "org.freedesktop.impl.portal.Test"
MAIN_OBJ = "/org/freedesktop/portal/desktop"
SYSTEM_BUS = False
MAIN_IFACE = "org.freedesktop.impl.portal.Wallpaper"
logger = init_logger(__name__)
@dataclass
class WallpaperParameters:
delay: int
response: int
expect_close: bool
def load(mock, parameters={}):
logger.debug(f"Loading parameters: {parameters}")
assert not hasattr(mock, "wallpaper_params")
mock.wallpaper_params = WallpaperParameters(
delay=parameters.get("delay", 200),
response=parameters.get("response", 0),
expect_close=parameters.get("expect-close", False),
)
@dbus.service.method(
MAIN_IFACE,
in_signature="osssa{sv}",
out_signature="u",
async_callbacks=("cb_success", "cb_error"),
)
def SetWallpaperURI(
self, handle, app_id, parent_window, uri, options, cb_success, cb_error
):
logger.debug(
f"SetWallpaperURI({handle}, {app_id}, {parent_window}, {uri}, {options})"
)
params = self.wallpaper_params
# This is irregular: the backend doesn't return the results vardict
def internal_cb_success(response, results):
cb_success(response)
request = ImplRequest(
self,
BUS_NAME,
handle,
logger,
internal_cb_success,
cb_error,
)
if params.expect_close:
request.wait_for_close()
else:
request.respond(Response(params.response, {}), delay=params.delay)

View File

@@ -0,0 +1,360 @@
#include "config.h"
#include <glib.h>
#include <document-portal/permission-db.h>
/*
static void
dump_db (PermissionDb *db)
{
g_autofree char *s = permission_db_print (db);
g_printerr ("\n%s\n", s);
}
*/
static PermissionDb *
create_test_db (gboolean serialized)
{
PermissionDb *db;
g_autoptr(PermissionDbEntry) entry1 = NULL;
g_autoptr(PermissionDbEntry) entry2 = NULL;
g_autoptr(PermissionDbEntry) entry3 = NULL;
g_autoptr(PermissionDbEntry) entry4 = NULL;
g_autoptr(PermissionDbEntry) entry5 = NULL;
g_autoptr(PermissionDbEntry) entry6 = NULL;
g_autoptr(PermissionDbEntry) entry7 = NULL;
GError *error = NULL;
const char *permissions1[] = { "read", "write", NULL };
const char *permissions2[] = { "read", NULL };
const char *permissions3[] = { "write", NULL };
db = permission_db_new (NULL, FALSE, &error);
g_assert_no_error (error);
g_assert (db != NULL);
{
g_auto(GStrv) ids = permission_db_list_ids (db);
g_assert (ids != NULL);
g_assert (ids[0] == NULL);
}
{
g_auto(GStrv) apps = permission_db_list_apps (db);
g_assert (apps != NULL);
g_assert (apps[0] == NULL);
}
entry1 = permission_db_entry_new (g_variant_new_string ("foo-data"));
entry2 = permission_db_entry_set_app_permissions (entry1, "org.test.bapp", permissions2);
entry3 = permission_db_entry_set_app_permissions (entry2, "org.test.app", permissions1);
entry4 = permission_db_entry_set_app_permissions (entry3, "org.test.capp", permissions1);
permission_db_set_entry (db, "foo", entry4);
entry5 = permission_db_entry_new (g_variant_new_string ("bar-data"));
entry6 = permission_db_entry_set_app_permissions (entry5, "org.test.app", permissions2);
entry7 = permission_db_entry_set_app_permissions (entry6, "org.test.dapp", permissions3);
permission_db_set_entry (db, "bar", entry7);
if (serialized)
permission_db_update (db);
return db;
}
static void
verify_test_db (PermissionDb *db)
{
g_auto(GStrv) ids = NULL;
g_autofree const char **apps1 = NULL;
g_autofree const char **apps2 = NULL;
g_auto(GStrv) all_apps = NULL;
ids = permission_db_list_ids (db);
g_assert (g_strv_length (ids) == 2);
g_assert (g_strv_contains ((const char **) ids, "foo"));
g_assert (g_strv_contains ((const char **) ids, "bar"));
{
g_autoptr(PermissionDbEntry) entry = NULL;
g_autofree const char **permissions1 = NULL;
g_autofree const char **permissions2 = NULL;
g_autofree const char **permissions3 = NULL;
g_autofree const char **permissions4 = NULL;
g_autoptr(GVariant) data1 = NULL;
entry = permission_db_lookup (db, "foo");
g_assert (entry != NULL);
data1 = permission_db_entry_get_data (entry);
g_assert (data1 != NULL);
g_assert_cmpstr (g_variant_get_type_string (data1), ==, "s");
g_assert_cmpstr (g_variant_get_string (data1, NULL), ==, "foo-data");
apps1 = permission_db_entry_list_apps (entry);
g_assert (g_strv_length ((char **) apps1) == 3);
g_assert (g_strv_contains (apps1, "org.test.app"));
g_assert (g_strv_contains (apps1, "org.test.bapp"));
g_assert (g_strv_contains (apps1, "org.test.capp"));
permissions1 = permission_db_entry_list_permissions (entry, "org.test.app");
g_assert (g_strv_length ((char **) permissions1) == 2);
g_assert (g_strv_contains (permissions1, "read"));
g_assert (g_strv_contains (permissions1, "write"));
permissions2 = permission_db_entry_list_permissions (entry, "org.test.bapp");
g_assert (g_strv_length ((char **) permissions2) == 1);
g_assert (g_strv_contains (permissions2, "read"));
permissions3 = permission_db_entry_list_permissions (entry, "org.test.capp");
g_assert (g_strv_length ((char **) permissions3) == 2);
g_assert (g_strv_contains (permissions3, "read"));
g_assert (g_strv_contains (permissions3, "write"));
permissions4 = permission_db_entry_list_permissions (entry, "org.test.noapp");
g_assert (permissions4 != NULL);
g_assert (g_strv_length ((char **) permissions4) == 0);
}
{
g_autoptr(PermissionDbEntry) entry = NULL;
g_autofree const char **permissions5 = NULL;
g_autofree const char **permissions6 = NULL;
g_autoptr(GVariant) data2 = NULL;
entry = permission_db_lookup (db, "bar");
g_assert (entry != NULL);
data2 = permission_db_entry_get_data (entry);
g_assert (data2 != NULL);
g_assert_cmpstr (g_variant_get_type_string (data2), ==, "s");
g_assert_cmpstr (g_variant_get_string (data2, NULL), ==, "bar-data");
apps2 = permission_db_entry_list_apps (entry);
g_assert (g_strv_length ((char **) apps2) == 2);
g_assert (g_strv_contains (apps2, "org.test.app"));
g_assert (g_strv_contains (apps2, "org.test.dapp"));
permissions5 = permission_db_entry_list_permissions (entry, "org.test.app");
g_assert (g_strv_length ((char **) permissions5) == 1);
g_assert (g_strv_contains (permissions5, "read"));
permissions6 = permission_db_entry_list_permissions (entry, "org.test.dapp");
g_assert (g_strv_length ((char **) permissions6) == 1);
g_assert (g_strv_contains (permissions6, "write"));
}
{
g_autoptr(PermissionDbEntry) entry = NULL;
entry = permission_db_lookup (db, "gazonk");
g_assert (entry == NULL);
}
all_apps = permission_db_list_apps (db);
g_assert (g_strv_length (all_apps) == 4);
g_assert (g_strv_contains ((const char **) all_apps, "org.test.app"));
g_assert (g_strv_contains ((const char **) all_apps, "org.test.bapp"));
g_assert (g_strv_contains ((const char **) all_apps, "org.test.capp"));
g_assert (g_strv_contains ((const char **) all_apps, "org.test.dapp"));
}
static void
test_db_open (void)
{
GError *error = NULL;
PermissionDb *db;
db = permission_db_new (g_test_get_filename (G_TEST_DIST, "dbs", "does_not_exist", NULL), TRUE, &error);
g_assert (g_error_matches (error, G_FILE_ERROR, G_FILE_ERROR_NOENT) ||
g_error_matches (error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND));
g_assert (db == NULL);
g_clear_error (&error);
db = permission_db_new (g_test_get_filename (G_TEST_DIST, "dbs", "does_not_exist", NULL), FALSE, &error);
g_assert_no_error (error);
g_assert (db != NULL);
g_clear_error (&error);
g_object_unref (db);
db = permission_db_new (g_test_get_filename (G_TEST_DIST, "dbs", "no_tables", NULL), TRUE, &error);
g_assert_error (error, G_FILE_ERROR, G_FILE_ERROR_INVAL);
g_assert (db == NULL);
g_clear_error (&error);
}
static void
test_serialize (void)
{
g_autoptr(PermissionDb) db = NULL;
g_autoptr(PermissionDb) db2 = NULL;
g_autofree char *dump1 = NULL;
g_autofree char *dump2 = NULL;
g_autofree char *dump3 = NULL;
GError *error = NULL;
char tmpfile[] = "/tmp/test-permission-db-XXXXXX";
int fd;
db = create_test_db (FALSE);
verify_test_db (db);
dump1 = permission_db_print (db);
g_assert (permission_db_is_dirty (db));
permission_db_update (db);
verify_test_db (db);
g_assert (!permission_db_is_dirty (db));
dump2 = permission_db_print (db);
g_assert_cmpstr (dump1, ==, dump2);
fd = g_mkstemp (tmpfile);
close (fd);
permission_db_set_path (db, tmpfile);
permission_db_save_content (db, &error);
g_assert_no_error (error);
db2 = permission_db_new (tmpfile, TRUE, &error);
g_assert_no_error (error);
g_assert (db2 != NULL);
dump3 = permission_db_print (db2);
g_assert_cmpstr (dump1, ==, dump3);
unlink (tmpfile);
}
static void
test_modify (void)
{
g_autoptr(PermissionDb) db = NULL;
const char *permissions[] = { "read", "write", "execute", NULL };
const char *no_permissions[] = { NULL };
db = create_test_db (FALSE);
/* Add permission */
{
g_autoptr(PermissionDbEntry) entry1 = NULL;
g_autoptr(PermissionDbEntry) entry2 = NULL;
entry1 = permission_db_lookup (db, "foo");
entry2 = permission_db_entry_set_app_permissions (entry1, "org.test.app", permissions);
permission_db_set_entry (db, "foo", entry2);
}
/* Add entry */
{
g_autoptr(PermissionDbEntry) entry1 = NULL;
g_autoptr(PermissionDbEntry) entry2 = NULL;
entry1 = permission_db_entry_new (g_variant_new_string ("gazonk-data"));
entry2 = permission_db_entry_set_app_permissions (entry1, "org.test.eapp", permissions);
permission_db_set_entry (db, "gazonk", entry2);
}
/* Remove permission */
{
g_autoptr(PermissionDbEntry) entry1 = NULL;
g_autoptr(PermissionDbEntry) entry2 = NULL;
entry1 = permission_db_lookup (db, "bar");
entry2 = permission_db_entry_set_app_permissions (entry1, "org.test.dapp", no_permissions);
permission_db_set_entry (db, "bar", entry2);
}
/* Verify */
{
g_autoptr(PermissionDbEntry) entry5 = NULL;
g_autoptr(PermissionDbEntry) entry6 = NULL;
g_autoptr(PermissionDbEntry) entry7 = NULL;
g_autofree const char **apps2 = NULL;
g_auto(GStrv) apps3 = NULL;
g_autofree const char **permissions1 = NULL;
g_autofree const char **permissions2 = NULL;
g_autofree const char **permissions3 = NULL;
entry5 = permission_db_lookup (db, "foo");
permissions1 = permission_db_entry_list_permissions (entry5, "org.test.app");
g_assert (g_strv_length ((char **) permissions1) == 3);
g_assert (g_strv_contains (permissions1, "read"));
g_assert (g_strv_contains (permissions1, "write"));
g_assert (g_strv_contains (permissions1, "execute"));
entry6 = permission_db_lookup (db, "bar");
permissions2 = permission_db_entry_list_permissions (entry6, "org.test.dapp");
g_assert (g_strv_length ((char **) permissions2) == 0);
entry7 = permission_db_lookup (db, "gazonk");
permissions3 = permission_db_entry_list_permissions (entry7, "org.test.eapp");
g_assert (g_strv_length ((char **) permissions3) == 3);
g_assert (g_strv_contains (permissions3, "read"));
g_assert (g_strv_contains (permissions3, "write"));
g_assert (g_strv_contains (permissions3, "execute"));
apps2 = permission_db_entry_list_apps (entry6);
g_assert_cmpint (g_strv_length ((char **) apps2), ==, 1);
g_assert (g_strv_contains (apps2, "org.test.app"));
apps3 = permission_db_list_apps (db);
g_assert_cmpint (g_strv_length (apps3), ==, 4);
g_assert (g_strv_contains ((const char **) apps3, "org.test.app"));
g_assert (g_strv_contains ((const char **) apps3, "org.test.bapp"));
g_assert (g_strv_contains ((const char **) apps3, "org.test.capp"));
g_assert (g_strv_contains ((const char **) apps3, "org.test.eapp"));
}
permission_db_update (db);
/* Verify after serialize */
{
g_autoptr(PermissionDbEntry) entry5 = NULL;
g_autoptr(PermissionDbEntry) entry6 = NULL;
g_autoptr(PermissionDbEntry) entry7 = NULL;
g_autofree const char **apps2 = NULL;
g_auto(GStrv) apps3 = NULL;
g_autofree const char **permissions1 = NULL;
g_autofree const char **permissions2 = NULL;
g_autofree const char **permissions3 = NULL;
entry5 = permission_db_lookup (db, "foo");
permissions1 = permission_db_entry_list_permissions (entry5, "org.test.app");
g_assert (g_strv_length ((char **) permissions1) == 3);
g_assert (g_strv_contains (permissions1, "read"));
g_assert (g_strv_contains (permissions1, "write"));
g_assert (g_strv_contains (permissions1, "execute"));
entry6 = permission_db_lookup (db, "bar");
permissions2 = permission_db_entry_list_permissions (entry6, "org.test.dapp");
g_assert (g_strv_length ((char **) permissions2) == 0);
entry7 = permission_db_lookup (db, "gazonk");
permissions3 = permission_db_entry_list_permissions (entry7, "org.test.eapp");
g_assert (g_strv_length ((char **) permissions3) == 3);
g_assert (g_strv_contains (permissions3, "read"));
g_assert (g_strv_contains (permissions3, "write"));
g_assert (g_strv_contains (permissions3, "execute"));
apps2 = permission_db_entry_list_apps (entry6);
g_assert_cmpint (g_strv_length ((char **) apps2), ==, 1);
g_assert (g_strv_contains (apps2, "org.test.app"));
apps3 = permission_db_list_apps (db);
g_assert_cmpint (g_strv_length (apps3), ==, 4);
g_assert (g_strv_contains ((const char **) apps3, "org.test.app"));
g_assert (g_strv_contains ((const char **) apps3, "org.test.bapp"));
g_assert (g_strv_contains ((const char **) apps3, "org.test.capp"));
g_assert (g_strv_contains ((const char **) apps3, "org.test.eapp"));
}
}
int
main (int argc, char **argv)
{
g_test_init (&argc, &argv, NULL);
g_test_add_func ("/db/open", test_db_open);
g_test_add_func ("/db/serialize", test_serialize);
g_test_add_func ("/db/modify", test_modify);
return g_test_run ();
}

View File

@@ -0,0 +1,69 @@
#include "config.h"
#include <glib.h>
#include "xdp-method-info.h"
static void
test_method_info_all (void)
{
unsigned int i;
unsigned int count = xdp_method_info_get_count ();
const XdpMethodInfo *method_info = xdp_method_info_get_all ();
g_assert_cmpint (count, >, 100);
g_assert_nonnull (method_info);
for (i = 0; i < count + 1; i++)
{
if (method_info->interface == NULL)
return;
g_assert_nonnull (method_info->method);
method_info++;
}
g_assert_not_reached();
}
static void
test_method_info_find (void)
{
const XdpMethodInfo *method_info;
method_info = xdp_method_info_find ("org.freedesktop.portal.Notification", "AddNotification");
g_assert_nonnull (method_info);
g_assert_cmpint (method_info->option_arg, ==, -1);
g_assert_false (method_info->uses_request);
method_info = xdp_method_info_find ("org.freedesktop.portal.Inhibit", "Inhibit");
g_assert_nonnull (method_info);
g_assert_cmpint (method_info->option_arg, ==, 2);
g_assert_true (method_info->uses_request);
method_info = xdp_method_info_find ("org.freedesktop.portal.Inhibit", "QueryEndResponse");
g_assert_nonnull (method_info);
g_assert_cmpint (method_info->option_arg, ==, -1);
g_assert_false (method_info->uses_request);
/* Prefix is required */
method_info = xdp_method_info_find ("Inhibit", "QueryEndResponse");
g_assert_null (method_info);
method_info = xdp_method_info_find ("DoesNotExist", "DoesNotExist");
g_assert_null (method_info);
method_info = xdp_method_info_find ("DoesNotExist", "DoesNotExist");
g_assert_null (method_info);
method_info = xdp_method_info_find ("org.freedesktop.portal.Inhibit", "DoesNotExist");
g_assert_null (method_info);
}
int main (int argc, char **argv)
{
g_test_init (&argc, &argv, NULL);
g_test_add_func ("/method-info/all", test_method_info_all);
g_test_add_func ("/method-info/find", test_method_info_find);
return g_test_run ();
}

View File

@@ -0,0 +1,205 @@
#include "config.h"
#include <glib.h>
#include "xdp-app-info-private.h"
#include "xdp-app-info-snap-private.h"
#include "xdp-app-info-host-private.h"
#include "xdp-utils.h"
#define snap_parse_cgroup _xdp_app_info_snap_parse_cgroup_file
#define host_parse_app_id _xdp_app_info_host_parse_app_id_from_unit_name
static void
test_parse_cgroup_unified (void)
{
char data[] = "0::/user.slice/user-1000.slice/user@1000.service/apps.slice/snap.something.scope\n";
FILE *f;
int res;
gboolean is_snap = FALSE;
f = fmemopen(data, sizeof(data), "r");
res = snap_parse_cgroup (f, &is_snap);
g_assert_cmpint (res, ==, 0);
g_assert_true (is_snap);
fclose(f);
}
static void
test_parse_cgroup_freezer (void)
{
char data[] =
"12:pids:/user.slice/user-1000.slice/user@1000.service\n"
"11:perf_event:/\n"
"10:net_cls,net_prio:/\n"
"9:cpuset:/\n"
"8:memory:/user.slice/user-1000.slice/user@1000.service/apps.slice/apps-org.gnome.Terminal.slice/vte-spawn-228ae109-a869-4533-8988-65ea4c10b492.scope\n"
"7:rdma:/\n"
"6:devices:/user.slice\n"
"5:blkio:/user.slice\n"
"4:hugetlb:/\n"
"3:freezer:/snap.portal-test\n"
"2:cpu,cpuacct:/user.slice\n"
"1:name=systemd:/user.slice/user-1000.slice/user@1000.service/apps.slice/apps-org.gnome.Terminal.slice/vte-spawn-228ae109-a869-4533-8988-65ea4c10b492.scope\n"
"0::/user.slice/user-1000.slice/user@1000.service/apps.slice/apps-org.gnome.Terminal.slice/vte-spawn-228ae109-a869-4533-8988-65ea4c10b492.scope\n";
FILE *f;
int res;
gboolean is_snap = FALSE;
f = fmemopen(data, sizeof(data), "r");
res = snap_parse_cgroup (f, &is_snap);
g_assert_cmpint (res, ==, 0);
g_assert_true (is_snap);
fclose(f);
}
static void
test_parse_cgroup_systemd (void)
{
char data[] = "1:name=systemd:/user.slice/user-1000.slice/user@1000.service/apps.slice/snap.something.scope\n";
FILE *f;
int res;
gboolean is_snap = FALSE;
f = fmemopen(data, sizeof(data), "r");
res = snap_parse_cgroup (f, &is_snap);
g_assert_cmpint (res, ==, 0);
g_assert_true (is_snap);
fclose(f);
}
static void
test_parse_cgroup_not_snap (void)
{
char data[] =
"12:pids:/\n"
"11:perf_event:/\n"
"10:net_cls,net_prio:/\n"
"9:cpuset:/\n"
"8:memory:/\n"
"7:rdma:/\n"
"6:devices:/\n"
"5:blkio:/\n"
"4:hugetlb:/\n"
"3:freezer:/\n"
"2:cpu,cpuacct:/\n"
"1:name=systemd:/\n"
"0::/\n";
FILE *f;
int res;
gboolean is_snap = FALSE;
f = fmemopen(data, sizeof(data), "r");
res = snap_parse_cgroup (f, &is_snap);
g_assert_cmpint (res, ==, 0);
g_assert_false (is_snap);
fclose(f);
}
static void
test_alternate_doc_path (void)
{
g_autofree char *path = NULL;
xdp_set_documents_mountpoint (NULL);
/* If no documents mount point is set, there is no alternate path */
path = xdp_get_alternate_document_path ("/whatever", "app-id");
g_assert_cmpstr (path, ==, NULL);
xdp_set_documents_mountpoint ("/doc/portal");
/* Paths outside of the document portal do not have an alternate path */
path = xdp_get_alternate_document_path ("/whatever", "app-id");
g_assert_cmpstr (path, ==, NULL);
/* The doc portal mount point itself does not have an alternate path */
path = xdp_get_alternate_document_path ("/doc/portal", "app-id");
g_assert_cmpstr (path, ==, NULL);
/* Paths under the doc portal mount point have an alternate path */
path = xdp_get_alternate_document_path ("/doc/portal/foo/bar", "app-id");
g_assert_cmpstr (path, ==, "/doc/portal/by-app/app-id/foo/bar");
g_clear_pointer (&path, g_free);
path = xdp_get_alternate_document_path ("/doc/portal/foo/bar", "second-app");
g_assert_cmpstr (path, ==, "/doc/portal/by-app/second-app/foo/bar");
xdp_set_documents_mountpoint (NULL);
}
#ifdef HAVE_LIBSYSTEMD
static void
test_app_id_via_systemd_unit (void)
{
g_autofree char *app_id = NULL;
app_id = host_parse_app_id ("app-not-a-well-formed-unit-name");
g_assert_cmpstr (app_id, ==, "");
g_clear_pointer (&app_id, g_free);
app_id = host_parse_app_id ("app-gnome-org.gnome.Evolution\\x2dalarm\\x2dnotify-2437.scope");
/* Note, this is not Evolution's app ID, because the scope is for a background service */
g_assert_cmpstr (app_id, ==, "org.gnome.Evolution-alarm-notify");
g_clear_pointer (&app_id, g_free);
app_id = host_parse_app_id ("app-gnome-org.gnome.Epiphany-182352.scope");
g_assert_cmpstr (app_id, ==, "org.gnome.Epiphany");
g_clear_pointer (&app_id, g_free);
app_id = host_parse_app_id ("app-glib-spice\\x2dvdagent-1839.scope");
g_assert_cmpstr (app_id, ==, "spice-vdagent");
g_clear_pointer (&app_id, g_free);
app_id = host_parse_app_id ("app-KDE-org.kde.okular@12345.service");
g_assert_cmpstr (app_id, ==, "org.kde.okular");
g_clear_pointer (&app_id, g_free);
app_id = host_parse_app_id ("app-firefox.service");
g_assert_cmpstr (app_id, ==, "firefox");
g_clear_pointer (&app_id, g_free);
app_id = host_parse_app_id ("app-org.kde.amarok.service");
g_assert_cmpstr (app_id, ==, "org.kde.amarok");
g_clear_pointer (&app_id, g_free);
app_id = host_parse_app_id ("app-gnome-org.gnome.SettingsDaemon.DiskUtilityNotify-autostart.service");
g_assert_cmpstr (app_id, ==, "org.gnome.SettingsDaemon.DiskUtilityNotify");
g_clear_pointer (&app_id, g_free);
app_id = host_parse_app_id ("app-gnome-org.gnome.Terminal-92502.slice");
g_assert_cmpstr (app_id, ==, "org.gnome.Terminal");
g_clear_pointer (&app_id, g_free);
app_id = host_parse_app_id ("app-com.obsproject.Studio-d70acc38b5154a3a8b4a60accc4b15f4.scope");
g_assert_cmpstr (app_id, ==, "com.obsproject.Studio");
g_clear_pointer (&app_id, g_free);
app_id = host_parse_app_id ("app-firefox-jcfppqx.scope");
g_assert_cmpstr (app_id, ==, "firefox");
g_clear_pointer (&app_id, g_free);
app_id = host_parse_app_id ("app-gnome-firefox.service");
g_assert_cmpstr (app_id, ==, "firefox");
g_clear_pointer (&app_id, g_free);
}
#endif /* HAVE_LIBSYSTEMD */
int main (int argc, char **argv)
{
g_test_init (&argc, &argv, NULL);
g_test_add_func ("/parse-cgroup/unified", test_parse_cgroup_unified);
g_test_add_func ("/parse-cgroup/freezer", test_parse_cgroup_freezer);
g_test_add_func ("/parse-cgroup/systemd", test_parse_cgroup_systemd);
g_test_add_func ("/parse-cgroup/not-snap", test_parse_cgroup_not_snap);
g_test_add_func ("/alternate-doc-path", test_alternate_doc_path);
#ifdef HAVE_LIBSYSTEMD
g_test_add_func ("/app-id-via-systemd-unit", test_app_id_via_systemd_unit);
#endif
return g_test_run ();
}

View File

@@ -0,0 +1,120 @@
# SPDX-License-Identifier: LGPL-2.1-or-later
#
# This file is formatted with Python Black
import tests as xdp
import pytest
ACCOUNT_DATA = {
"id": "test",
"name": "Test Name",
"image": "file:///image.png",
}
@pytest.fixture
def required_templates():
return {
"account": {
"results": ACCOUNT_DATA,
},
}
class TestAccount:
def set_permission(self, dbus_con, app_id, permission):
perm_store_intf = xdp.get_permission_store_iface(dbus_con)
perm_store_intf.SetPermission(
"wallpaper",
True,
"wallpaper",
app_id,
[permission],
)
def test_version(self, portals, dbus_con):
xdp.check_version(dbus_con, "Account", 1)
def test_basic1(self, portals, dbus_con, app_id):
account_intf = xdp.get_portal_iface(dbus_con, "Account")
mock_intf = xdp.get_mock_iface(dbus_con)
reason = "reason"
request = xdp.Request(dbus_con, account_intf)
options = {
"reason": reason,
}
response = request.call(
"GetUserInformation",
window="",
options=options,
)
assert response
assert response.response == 0
assert response.results["id"] == ACCOUNT_DATA["id"]
assert response.results["name"] == ACCOUNT_DATA["name"]
assert response.results["image"] == ACCOUNT_DATA["image"]
# Check the impl portal was called with the right args
method_calls = mock_intf.GetMethodCalls("GetUserInformation")
assert len(method_calls) > 0
_, args = method_calls[-1]
assert args[1] == app_id
assert args[2] == "" # window
assert args[3]["reason"] == reason
def test_reason(self, portals, dbus_con):
account_intf = xdp.get_portal_iface(dbus_con, "Account")
mock_intf = xdp.get_mock_iface(dbus_con)
reason = """This reason is unreasonably long, it stretches over
more than twohundredfiftysix characters, which is really quite
long. Excessively so. The portal frontend will silently drop
reasons of this magnitude. If you can't express your reasons
concisely, you probably have no good reason in the first place
and are just waffling around."""
assert len(reason) - 1 > 256
request = xdp.Request(dbus_con, account_intf)
options = {
"reason": reason,
}
response = request.call(
"GetUserInformation",
window="",
options=options,
)
assert response
assert response.response == 0
# Check the impl portal was called with the right args
method_calls = mock_intf.GetMethodCalls("GetUserInformation")
assert len(method_calls) > 0
_, args = method_calls[-1]
assert "reason" not in args[3]
@pytest.mark.parametrize("template_params", ({"account": {"expect-close": True}},))
def test_close(self, portals, dbus_con):
account_intf = xdp.get_portal_iface(dbus_con, "Account")
reason = "reason"
request = xdp.Request(dbus_con, account_intf)
request.schedule_close(1000)
options = {
"reason": reason,
}
request.call(
"GetUserInformation",
window="",
options=options,
)
# Only true if the impl.Request was closed too
assert request.closed

View File

@@ -0,0 +1,167 @@
# SPDX-License-Identifier: LGPL-2.1-or-later
#
# This file is formatted with Python Black
import tests as xdp
import dbus
import pytest
import os
from pathlib import Path
from gi.repository import GLib
@pytest.fixture
def required_templates():
return {"background": {}}
class TestBackground:
def get_autostart_path(self, app_id):
return Path(os.environ["XDG_CONFIG_HOME"]) / "autostart" / f"{app_id}.desktop"
def get_autostart_keyfile(self, app_id):
keyfile = GLib.KeyFile.new()
desktop_file_path = self.get_autostart_path(app_id)
with open(str(desktop_file_path.absolute())) as desktop_file:
desktop_file_contents = desktop_file.read()
assert keyfile.load_from_data(
desktop_file_contents,
len(desktop_file_contents),
GLib.KeyFileFlags.NONE,
)
return keyfile
def test_version(self, portals, dbus_con):
xdp.check_version(dbus_con, "Background", 2)
def test_request_background(self, portals, dbus_con, app_id):
background_intf = xdp.get_portal_iface(dbus_con, "Background")
desktop_file = self.get_autostart_path(app_id)
reason = "Testing portals"
request = xdp.Request(dbus_con, background_intf)
options = {
"reason": reason,
}
response = request.call(
"RequestBackground",
parent_window="",
options=options,
)
assert response
assert response.response == 0
assert response.results["background"]
assert not response.results["autostart"]
assert not desktop_file.exists()
def test_autostart_desktopfile(self, portals, dbus_con, app_id):
background_intf = xdp.get_portal_iface(dbus_con, "Background")
reason = "Testing portals"
autostart = True
commandline = ["/bin/true", "test"]
dbus_activatable = True
request = xdp.Request(dbus_con, background_intf)
options = {
"reason": reason,
"autostart": autostart,
"commandline": commandline,
"dbus-activatable": dbus_activatable,
}
response = request.call(
"RequestBackground",
parent_window="",
options=options,
)
assert response
assert response.response == 0
assert response.results["background"]
assert response.results["autostart"]
keyfile = self.get_autostart_keyfile(app_id)
assert keyfile.get_string("Desktop Entry", "Type") == "Application"
assert keyfile.get_string("Desktop Entry", "Name") == app_id
assert keyfile.get_string("Desktop Entry", "X-XDP-Autostart") == app_id
assert keyfile.get_string("Desktop Entry", "Exec") == "/bin/true test"
assert keyfile.get_boolean("Desktop Entry", "DBusActivatable")
def test_autostart_disable(self, portals, dbus_con, app_id):
background_intf = xdp.get_portal_iface(dbus_con, "Background")
desktop_file = self.get_autostart_path(app_id)
reason = "Testing portals"
autostart = True
request = xdp.Request(dbus_con, background_intf)
options = {
"reason": reason,
"autostart": autostart,
}
response = request.call(
"RequestBackground",
parent_window="",
options=options,
)
assert response
assert response.response == 0
assert response.results["background"]
assert response.results["autostart"]
assert desktop_file.exists()
request = xdp.Request(dbus_con, background_intf)
options = {
"reason": reason,
}
response = request.call(
"RequestBackground",
parent_window="",
options=options,
)
assert response
assert response.response == 0
assert response.results["background"]
assert not response.results["autostart"]
assert not desktop_file.exists()
def test_long_reason(self, portals, dbus_con, app_id):
background_intf = xdp.get_portal_iface(dbus_con, "Background")
reason = (
"012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789"
+ "012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789"
+ "012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789"
)
autostart = True
commandline = ["/bin/true", "test"]
dbus_activatable = True
request = xdp.Request(dbus_con, background_intf)
options = {
"reason": reason,
"autostart": autostart,
"commandline": commandline,
"dbus-activatable": dbus_activatable,
}
with pytest.raises(dbus.exceptions.DBusException) as excinfo:
request.call(
"RequestBackground",
parent_window="",
options=options,
)
assert (
excinfo.value.get_dbus_name()
== "org.freedesktop.portal.Error.InvalidArgument"
)

View File

@@ -0,0 +1,139 @@
# SPDX-License-Identifier: LGPL-2.1-or-later
#
# This file is formatted with Python Black
import tests as xdp
import dbus
import pytest
@pytest.fixture
def required_templates():
return {
"access": {},
"lockdown": {},
}
@pytest.fixture
def app_id():
# x-d-p currently defaults to the empty app id for the camera portal for
# host XdpAppInfos (which the XdpAppInfoTest is). So use the empty app_id
# for now.
return ""
class TestCamera:
def set_permissions(self, dbus_con, appid, permissions):
perm_store_intf = xdp.get_permission_store_iface(dbus_con)
perm_store_intf.SetPermission(
"devices",
True,
"camera",
appid,
permissions,
)
def test_version(self, portals, dbus_con):
xdp.check_version(dbus_con, "Camera", 1)
def test_access(self, portals, dbus_con, app_id):
camera_intf = xdp.get_portal_iface(dbus_con, "Camera")
mock_intf = xdp.get_mock_iface(dbus_con)
request = xdp.Request(dbus_con, camera_intf)
response = request.call(
"AccessCamera",
options={},
)
assert response
assert response.response == 0
# Check the impl portal was called with the right args
method_calls = mock_intf.GetMethodCalls("AccessDialog")
assert len(method_calls) == 1
_, args = method_calls[-1]
assert args[1] == app_id
@pytest.mark.parametrize("template_params", ({"access": {"response": 1}},))
def test_access_cancel(self, portals, dbus_con, app_id):
camera_intf = xdp.get_portal_iface(dbus_con, "Camera")
mock_intf = xdp.get_mock_iface(dbus_con)
request = xdp.Request(dbus_con, camera_intf)
response = request.call(
"AccessCamera",
options={},
)
assert response
assert response.response == 1
# Check the impl portal was called with the right args
method_calls = mock_intf.GetMethodCalls("AccessDialog")
assert len(method_calls) == 1
_, args = method_calls[-1]
assert args[1] == app_id
@pytest.mark.parametrize("template_params", ({"access": {"expect-close": True}},))
def test_access_close(self, portals, dbus_con, app_id):
camera_intf = xdp.get_portal_iface(dbus_con, "Camera")
mock_intf = xdp.get_mock_iface(dbus_con)
request = xdp.Request(dbus_con, camera_intf)
request.schedule_close(1000)
request.call(
"AccessCamera",
options={},
)
# Only true if the impl.Request was closed too
assert request.closed
# Check the impl portal was called with the right args
method_calls = mock_intf.GetMethodCalls("AccessDialog")
assert len(method_calls) == 1
_, args = method_calls[-1]
assert args[1] == app_id
@pytest.mark.parametrize(
"template_params", ({"lockdown": {"disable-camera": True}},)
)
def test_access_lockdown(self, portals, dbus_con, app_id):
camera_intf = xdp.get_portal_iface(dbus_con, "Camera")
mock_intf = xdp.get_mock_iface(dbus_con)
request = xdp.Request(dbus_con, camera_intf)
with pytest.raises(dbus.exceptions.DBusException) as excinfo:
request.call(
"AccessCamera",
options={},
)
assert (
excinfo.value.get_dbus_name() == "org.freedesktop.portal.Error.NotAllowed"
)
# Check the impl portal was called with the right args
method_calls = mock_intf.GetMethodCalls("AccessDialog")
assert len(method_calls) == 0
def test_access_denied(self, portals, dbus_con, app_id):
camera_intf = xdp.get_portal_iface(dbus_con, "Camera")
mock_intf = xdp.get_mock_iface(dbus_con)
self.set_permissions(dbus_con, app_id, ["no"])
request = xdp.Request(dbus_con, camera_intf)
response = request.call(
"AccessCamera",
options={},
)
assert response
assert response.response == 1
# Check the impl portal was called with the right args
method_calls = mock_intf.GetMethodCalls("AccessDialog")
assert len(method_calls) == 0

View File

@@ -0,0 +1,103 @@
# SPDX-License-Identifier: LGPL-2.1-or-later
#
# This file is formatted with Python Black
import tests as xdp
import dbus
import pytest
import os
@pytest.fixture
def required_templates():
return {
"Clipboard": {},
"RemoteDesktop": {"force-clipboard-enabled": True},
}
class TestClipboard:
def test_version(self, portals, dbus_con):
xdp.check_version(dbus_con, "Clipboard", 1)
def start_session(self, dbus_con):
clipboard_intf = xdp.get_portal_iface(dbus_con, "Clipboard")
remotedesktop_intf = xdp.get_portal_iface(dbus_con, "RemoteDesktop")
create_session_request = xdp.Request(dbus_con, remotedesktop_intf)
create_session_response = create_session_request.call(
"CreateSession", options={"session_handle_token": "1234"}
)
assert create_session_response
assert create_session_response.response == 0
assert str(create_session_response.results["session_handle"])
session = xdp.Session.from_response(dbus_con, create_session_response)
clipboard_intf.RequestClipboard(session.handle, {})
start_session_request = xdp.Request(dbus_con, remotedesktop_intf)
start_session_response = start_session_request.call(
"Start", session_handle=session.handle, parent_window="", options={}
)
assert start_session_response
assert start_session_response.response == 0
return (session, start_session_response.results.get("clipboard_enabled"))
def test_request_clipboard_and_start_session(self, portals, dbus_con):
_, clipboard_enabled = self.start_session(dbus_con)
assert clipboard_enabled
@pytest.mark.parametrize(
"template_params", ({"RemoteDesktop": {"force-clipboard-enabled": False}},)
)
def test_checks_clipboard_enabled(self, portals, dbus_con):
clipboard_intf = xdp.get_portal_iface(dbus_con, "Clipboard")
session, clipboard_enabled = self.start_session(dbus_con)
assert not clipboard_enabled
with pytest.raises(dbus.exceptions.DBusException):
clipboard_intf.SetSelection(session.handle, {})
def test_set_selection(self, portals, dbus_con):
clipboard_intf = xdp.get_portal_iface(dbus_con, "Clipboard")
session, _ = self.start_session(dbus_con)
clipboard_intf.SetSelection(session.handle, {})
def test_selection_write(self, portals, dbus_con):
clipboard_intf = xdp.get_portal_iface(dbus_con, "Clipboard")
session, _ = self.start_session(dbus_con)
fd_object: dbus.types.UnixFd = clipboard_intf.SelectionWrite(
session.handle, 1234
)
assert fd_object
fd = fd_object.take()
assert fd
bytes_written = os.write(fd, b"Clipboard")
assert bytes_written > 0
clipboard_intf.SelectionWriteDone(session.handle, 1234, True)
def test_selection_read(self, portals, dbus_con):
clipboard_intf = xdp.get_portal_iface(dbus_con, "Clipboard")
session, _ = self.start_session(dbus_con)
fd_object: dbus.types.UnixFd = clipboard_intf.SelectionRead(
session.handle, "mimetype"
)
assert fd_object
fd = fd_object.take()
assert fd
clipboard_contents = os.read(fd, 1000)
assert str(clipboard_contents)

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,421 @@
# SPDX-License-Identifier: LGPL-2.1-or-later
#
# This file is formatted with Python Black
import tests as xdp
import pytest
import dbus
from pathlib import Path
import os
from gi.repository import GLib, Gio
EXPORT_FILES_FLAG_EXPORT_DIR = 8
def path_from_null_term_bytes(bytes):
path_bytes, rest = bytes.split(b"\x00")
assert rest == b""
return Path(os.fsdecode(path_bytes))
def get_mountpoint(documents_intf):
mountpoint = documents_intf.GetMountPoint(byte_arrays=True)
mountpoint = path_from_null_term_bytes(mountpoint)
assert mountpoint.exists()
return mountpoint
def write_bytes_atomic(file_path, bytes):
GLib.file_set_contents(file_path.absolute().as_posix(), bytes)
def write_bytes_trunc(file_path, bytes):
try:
fd = os.open(
file_path.absolute().as_posix(), os.O_RDWR | os.O_TRUNC | os.O_CREAT
)
os.write(fd, bytes)
finally:
os.close(fd)
def get_host_path_attr(path):
xattr = "xattr::document-portal.host-path"
file = Gio.file_new_for_path(path.absolute().as_posix())
info = file.query_info(xattr, Gio.FileQueryInfoFlags.NONE)
host_path = info.get_attribute_as_string(xattr)
if not host_path:
return None
return Path(os.fsdecode(host_path))
def export_file(documents_intf, file_path, unique=False):
assert file_path.exists()
with open(file_path.absolute().as_posix(), "r") as file:
doc_id = documents_intf.Add(file.fileno(), not unique, False)
assert doc_id
return doc_id
def export_file_named(documents_intf, folder_path, name, unique=False):
assert folder_path.exists()
# bytestring convention is zero terminated
name_nt = os.fsencode(name) + b"\x00"
try:
fd = os.open(folder_path.absolute().as_posix(), os.O_PATH | os.O_CLOEXEC)
doc_id = documents_intf.AddNamed(fd, name_nt, not unique, False)
assert doc_id
finally:
os.close(fd)
return doc_id
def export_files(documents_intf, file_paths, perms, flags=0, app_id=""):
fds = []
try:
for file_path in file_paths:
fds.append(
os.open(file_path.absolute().as_posix(), os.O_PATH | os.O_CLOEXEC)
)
result = documents_intf.AddFull(
fds,
flags,
app_id,
perms,
byte_arrays=True,
)
finally:
for fd in fds:
os.close(fd)
assert result
return result
class TestDocuments:
def test_version(self, xdg_document_portal, dbus_con):
documents = dbus_con.get_object(
"org.freedesktop.portal.Documents",
"/org/freedesktop/portal/documents",
)
properties_intf = dbus.Interface(
documents,
"org.freedesktop.DBus.Properties",
)
portal_version = properties_intf.Get(
"org.freedesktop.portal.Documents",
"version",
)
assert int(portal_version) == 5
def test_mount_point(self, xdg_document_portal, dbus_con):
documents_intf = xdp.get_document_portal_iface(dbus_con)
get_mountpoint(documents_intf)
def test_create_doc(self, xdg_document_portal, dbus_con):
documents_intf = xdp.get_document_portal_iface(dbus_con)
mountpoint = get_mountpoint(documents_intf)
content = b"content"
file_name = "a-file"
file_path = Path(os.environ["TMPDIR"]) / file_name
write_bytes_atomic(file_path, content)
doc_id = export_file(documents_intf, file_path)
doc_path = mountpoint / doc_id
doc_app1_path = mountpoint / "by-app" / "com.test.App1" / doc_id
doc_app2_path = mountpoint / "by-app" / "com.test.App2" / doc_id
# Make sure it got exported
assert (doc_path / file_name).read_bytes() == content
assert not (doc_path / "another-file").exists()
assert not (mountpoint / "anotherid" / file_name).exists()
# Make sure it is not viewable by apps
assert not doc_app1_path.exists()
assert not doc_app2_path.exists()
# Create a tmp file in same dir, ensure it works and can't be seen by other apps
write_bytes_atomic(doc_path / "tmp1", b"tmpdata1")
assert (doc_path / "tmp1").read_bytes() == b"tmpdata1"
assert not (doc_app1_path / "tmp1").exists()
# Ensure App 1 and only it can see the document and tmpfile
documents_intf.GrantPermissions(doc_id, "com.test.App1", ["read"])
assert (doc_app1_path / file_name).read_bytes() == content
assert not (doc_app2_path / file_name).exists()
# Make sure App 1 can't create a tmpfile
assert not (doc_app1_path / "tmp2").exists()
with pytest.raises(PermissionError):
(doc_app1_path / "tmp2").write_bytes(b"tmpdata1")
assert not (doc_app1_path / "tmp2").exists()
assert not (doc_path / "tmp2").exists()
# Update the document contents, ensure this is propagated
content = b"content2"
write_bytes_atomic(doc_path / file_name, content)
assert (doc_path / file_name).read_bytes() == content
assert (doc_app1_path / file_name).read_bytes() == content
assert file_path.read_bytes() == content
assert not (doc_app2_path / file_name).exists()
assert not (doc_app2_path / "tmp1").exists()
# Update the document contents outside fuse fd, ensure this is propagated
content = b"content3"
write_bytes_atomic(file_path, content)
assert (doc_path / file_name).read_bytes() == content
assert (doc_app1_path / file_name).read_bytes() == content
assert file_path.read_bytes() == content
assert not (doc_app2_path / file_name).exists()
assert not (doc_app2_path / "tmp1").exists()
# Try to update the doc from an app that can't write to it
with pytest.raises(PermissionError):
(doc_app1_path / file_name).write_bytes(b"content4")
# Update the doc from an app with write access
documents_intf.GrantPermissions(doc_id, "com.test.App1", ["write"])
content = b"content5"
write_bytes_atomic(doc_app1_path / file_name, content)
assert (doc_path / file_name).read_bytes() == content
assert (doc_app1_path / file_name).read_bytes() == content
assert file_path.read_bytes() == content
assert not (doc_app2_path / file_name).exists()
# Try to create a tmp file for an app
assert not (doc_app1_path / "tmp3").exists()
write_bytes_atomic(doc_app1_path / "tmp3", b"tmpdata2")
(doc_app1_path / "tmp3").read_bytes() == b"tmpdata2"
assert not (doc_path / "tmp3").exists()
# Re-Create a file from a fuse document file, in various ways
doc_id2 = export_file(documents_intf, (doc_path / file_name))
assert doc_id2 == doc_id
doc_id3 = export_file(documents_intf, (doc_app1_path / file_name))
assert doc_id3 == doc_id
doc_id4 = export_file(documents_intf, file_path)
assert doc_id4 == doc_id
# Ensure we can make a unique document
doc_id5 = export_file(documents_intf, file_path, unique=True)
assert doc_id5 != doc_id
def test_recursive_doc(self, xdg_document_portal, dbus_con):
documents_intf = xdp.get_document_portal_iface(dbus_con)
mountpoint = get_mountpoint(documents_intf)
content = b"content"
file_name = "recursive-file"
file_path = Path(os.environ["TMPDIR"]) / file_name
write_bytes_atomic(file_path, content)
doc_id = export_file(documents_intf, file_path)
doc_path = mountpoint / doc_id
doc_app1_path = mountpoint / "by-app" / "com.test.App1" / doc_id
assert (doc_path / file_name).read_bytes() == content
doc_id2 = export_file(documents_intf, doc_path / file_name)
assert doc_id2 == doc_id
documents_intf.GrantPermissions(doc_id, "com.test.App1", ["read"])
doc_id3 = export_file(documents_intf, doc_app1_path / file_name)
assert doc_id3 == doc_id
def test_create_docs(self, xdg_document_portal, dbus_con):
documents_intf = xdp.get_document_portal_iface(dbus_con)
mountpoint = get_mountpoint(documents_intf)
files = {
"doc1": b"doc1-content",
"doc2": b"doc2-content",
}
file_paths = []
for file_name, file_content in files.items():
file_path = Path(os.environ["TMPDIR"]) / file_name
write_bytes_atomic(file_path, file_content)
file_paths.append(file_path)
doc_ids, extra = export_files(
documents_intf, file_paths, ["read"], app_id="org.other.App"
)
assert extra
out_mountpoint = path_from_null_term_bytes(extra["mountpoint"])
assert out_mountpoint == mountpoint
assert doc_ids
for doc_id, (file_name, file_content) in zip(doc_ids, files.items()):
assert (mountpoint / doc_id / file_name).read_bytes() == file_content
assert (Path(os.environ["TMPDIR"]) / file_name).read_bytes() == file_content
app1_path = mountpoint / "by-app" / "com.test.App1" / doc_id / file_name
app2_path = mountpoint / "by-app" / "com.test.App2" / doc_id / file_name
assert not app1_path.exists()
assert not app2_path.exists()
assert not (mountpoint / doc_id / "another-file").exists()
assert not (mountpoint / "anotherid" / file_name).exists()
other_app_path = (
mountpoint / "by-app" / "org.other.App" / doc_id / file_name
)
assert other_app_path.read_bytes() == file_content
with pytest.raises(PermissionError):
other_app_path.write_bytes(b"new-content")
def test_add_named(self, xdg_document_portal, dbus_con):
documents_intf = xdp.get_document_portal_iface(dbus_con)
mountpoint = get_mountpoint(documents_intf)
content = b"content"
file_name = "add-named-1"
folder_path = Path(os.environ["TMPDIR"])
doc_id = export_file_named(documents_intf, folder_path, file_name)
assert doc_id
doc_path = mountpoint / doc_id
doc_app1_path = mountpoint / "by-app" / "com.test.App1" / doc_id
doc_app2_path = mountpoint / "by-app" / "com.test.App2" / doc_id
assert doc_path.exists()
assert not doc_app1_path.exists()
assert not (doc_path / file_name).exists()
assert not (doc_app1_path / file_name).exists()
documents_intf.GrantPermissions(doc_id, "com.test.App1", ["read", "write"])
assert doc_path.exists()
assert doc_app1_path.exists()
assert not (doc_path / file_name).exists()
assert not (doc_app1_path / file_name).exists()
# Update truncating with no previous file
write_bytes_trunc(doc_path / file_name, content)
assert (doc_path / file_name).read_bytes() == content
assert (doc_app1_path / file_name).read_bytes() == content
assert not (doc_app2_path / file_name).exists()
# Update truncating with previous file
content = b"content2"
write_bytes_trunc(doc_path / file_name, content)
assert (doc_path / file_name).read_bytes() == content
assert (doc_app1_path / file_name).read_bytes() == content
assert not (doc_app2_path / file_name).exists()
# Update atomic with previous file
content = b"content3"
write_bytes_atomic(doc_path / file_name, content)
assert (doc_path / file_name).read_bytes() == content
assert (doc_app1_path / file_name).read_bytes() == content
assert not (doc_app2_path / file_name).exists()
# Update from host
content = b"content4"
write_bytes_atomic(folder_path / file_name, content)
assert (doc_path / file_name).read_bytes() == content
assert (doc_app1_path / file_name).read_bytes() == content
assert not (doc_app2_path / file_name).exists()
# Unlink doc
(doc_path / file_name).unlink()
assert doc_path.exists()
assert doc_app1_path.exists()
assert not (doc_path / file_name).exists()
assert not (doc_app1_path / file_name).exists()
# Update atomic with no previous file
content = b"content5"
write_bytes_atomic(doc_path / file_name, content)
assert (doc_path / file_name).read_bytes() == content
assert (doc_app1_path / file_name).read_bytes() == content
assert not (doc_app2_path / file_name).exists()
# Unlink doc on host
(folder_path / file_name).unlink()
assert doc_path.exists()
assert doc_app1_path.exists()
assert not (doc_path / file_name).exists()
assert not (doc_app1_path / file_name).exists()
# Update atomic with unexpected no previous file
content = b"content6"
write_bytes_atomic(doc_path / file_name, content)
assert (doc_path / file_name).read_bytes() == content
assert (doc_app1_path / file_name).read_bytes() == content
assert not (doc_app2_path / file_name).exists()
# Unlink doc on host again
(folder_path / file_name).unlink()
assert doc_path.exists()
assert doc_app1_path.exists()
assert not (doc_path / file_name).exists()
assert not (doc_app1_path / file_name).exists()
# Update truncating with unexpected no previous file
content = b"content7"
write_bytes_trunc(doc_path / file_name, content)
assert (doc_path / file_name).read_bytes() == content
assert (doc_app1_path / file_name).read_bytes() == content
assert not (doc_app2_path / file_name).exists()
def test_get_host_paths(self, xdg_document_portal, dbus_con):
documents_intf = xdp.get_document_portal_iface(dbus_con)
content = b"content"
file_name = "host-path"
file_path = Path(os.environ["TMPDIR"]) / file_name
write_bytes_atomic(file_path, content)
doc_id = export_file(documents_intf, file_path)
host_paths = documents_intf.GetHostPaths([doc_id], byte_arrays=True)
assert doc_id in host_paths
doc_host_path = path_from_null_term_bytes(host_paths[doc_id])
assert doc_host_path == file_path
def test_host_paths_xattr(self, xdg_document_portal, dbus_con):
documents_intf = xdp.get_document_portal_iface(dbus_con)
mountpoint = get_mountpoint(documents_intf)
base_path = Path(os.environ["TMPDIR"]) / "a"
file_path = base_path / "b" / "c"
file_path.parent.mkdir(parents=True, exist_ok=True)
file_path.write_bytes(b"test")
doc_ids, extra = export_files(
documents_intf, [base_path], ["read"], flags=EXPORT_FILES_FLAG_EXPORT_DIR
)
doc_id = doc_ids[0]
host_path = get_host_path_attr(mountpoint / doc_id)
assert not host_path
host_path = get_host_path_attr(mountpoint / doc_id / "a")
assert host_path == base_path
host_path = get_host_path_attr(mountpoint / doc_id / "a" / "b")
assert host_path == base_path / "b"
host_path = get_host_path_attr(mountpoint / doc_id / "a" / "b" / "c")
assert host_path == base_path / "b" / "c"
try:
xdp.ensure_fuse_supported()
except xdp.FuseNotSupportedException as e:
pytest.skip(f"No fuse support: {e}", allow_module_level=True)

View File

@@ -0,0 +1,84 @@
# SPDX-License-Identifier: LGPL-2.1-or-later
#
# This file is formatted with Python Black
import tests as xdp
import pytest
import dbus
import os
from pathlib import Path
SVG_IMAGE_DATA = """<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" height="16px" width="16px"/>
"""
DESKTOP_FILE = b"""[Desktop Entry]
Version=1.0
Name=Dynamic Launcher Example
Exec=true %u
Type=Application
"""
@pytest.fixture
def required_templates():
return {"dynamiclauncher": {}}
class TestDynamicLauncher:
def test_version(self, portals, dbus_con):
"""tests the version of the interface"""
xdp.check_version(dbus_con, "DynamicLauncher", 1)
def test_basic(self, portals, dbus_con, app_id):
"""test that the backend receives the expected data"""
dynlauncher_intf = xdp.get_portal_iface(dbus_con, "DynamicLauncher")
mock_intf = xdp.get_mock_iface(dbus_con)
app_name = "App Name"
bytes = SVG_IMAGE_DATA.encode("utf-8")
request = xdp.Request(dbus_con, dynlauncher_intf)
options = {
"modal": False,
}
response = request.call(
"PrepareInstall",
parent_window="",
name=app_name,
icon_v=dbus.Struct(
("bytes", dbus.ByteArray(bytes, variant_level=1)),
signature="sv",
variant_level=1,
),
options=options,
)
assert response
assert response.response == 0
assert response.results["name"] == app_name
token = response.results["token"]
# Check the impl portal was called with the right args
method_calls = mock_intf.GetMethodCalls("PrepareInstall")
assert len(method_calls) > 0
_, args = method_calls[-1]
assert args[2] == "" # parent window
assert args[3] == app_name # name
# args[4] == icon
assert not args[5]["modal"]
desktop_file_name = app_id + ".ExampleApp.desktop"
dynlauncher_intf.Install(
token,
desktop_file_name,
DESKTOP_FILE,
{},
)
file = Path(os.environ["XDG_DATA_HOME"]) / "applications" / desktop_file_name
assert file.exists()

View File

@@ -0,0 +1,293 @@
# SPDX-License-Identifier: LGPL-2.1-or-later
#
# This file is formatted with Python Black
import tests as xdp
import dbus
import pytest
import time
@pytest.fixture
def required_templates():
return {"email": {}}
class TestEmail:
def test_version(self, portals, dbus_con):
"""tests the version of the interface"""
xdp.check_version(dbus_con, "Email", 4)
def test_basic(self, portals, dbus_con):
"""test that the backend receives the expected data"""
email_intf = xdp.get_portal_iface(dbus_con, "Email")
mock_intf = xdp.get_mock_iface(dbus_con)
addresses = ["mclasen@redhat.com"]
subject = "Re: portal tests"
body = "You have to see this"
request = xdp.Request(dbus_con, email_intf)
options = {
"addresses": addresses,
"subject": subject,
"body": body,
}
response = request.call(
"ComposeEmail",
parent_window="",
options=options,
)
assert response
assert response.response == 0
# Check the impl portal was called with the right args
method_calls = mock_intf.GetMethodCalls("ComposeEmail")
assert len(method_calls) > 0
_, args = method_calls[-1]
assert args[2] == "" # parent window
assert args[3]["addresses"] == addresses
assert args[3]["subject"] == subject
assert args[3]["body"] == body
def test_address(self, portals, dbus_con):
"""test that an invalid address triggers an error"""
email_intf = xdp.get_portal_iface(dbus_con, "Email")
mock_intf = xdp.get_mock_iface(dbus_con)
addresses = ["gibberish! not an email address\n%Q"]
request = xdp.Request(dbus_con, email_intf)
options = {
"addresses": addresses,
}
with pytest.raises(dbus.exceptions.DBusException) as excinfo:
request.call(
"ComposeEmail",
parent_window="",
options=options,
)
assert (
excinfo.value.get_dbus_name()
== "org.freedesktop.portal.Error.InvalidArgument"
)
# Check the impl portal was never called
method_calls = mock_intf.GetMethodCalls("ComposeEmail")
assert len(method_calls) == 0
def test_punycode_address(self, portals, dbus_con):
"""test email address containing punycode"""
email_intf = xdp.get_portal_iface(dbus_con, "Email")
mock_intf = xdp.get_mock_iface(dbus_con)
addresses = ["xn--franais-xxa@exemple.fr"]
subject = "Re: portal tests"
body = "To ASCII and beyond"
request = xdp.Request(dbus_con, email_intf)
options = {
"addresses": addresses,
"subject": subject,
"body": body,
}
response = request.call(
"ComposeEmail",
parent_window="",
options=options,
)
assert response
assert response.response == 0
# Check the impl portal was called with the right args
method_calls = mock_intf.GetMethodCalls("ComposeEmail")
assert len(method_calls) > 0
_, args = method_calls[-1]
assert args[2] == "" # parent window
assert args[3]["addresses"] == addresses
assert args[3]["subject"] == subject
assert args[3]["body"] == body
def test_subject_multiline(self, portals, dbus_con):
"""test that an multiline subject triggers an error"""
email_intf = xdp.get_portal_iface(dbus_con, "Email")
mock_intf = xdp.get_mock_iface(dbus_con)
subject = "not\na\nvalid\nsubject line"
request = xdp.Request(dbus_con, email_intf)
options = {
"subject": subject,
}
with pytest.raises(dbus.exceptions.DBusException) as excinfo:
request.call(
"ComposeEmail",
parent_window="",
options=options,
)
assert (
excinfo.value.get_dbus_name()
== "org.freedesktop.portal.Error.InvalidArgument"
)
# Check the impl portal was never called
method_calls = mock_intf.GetMethodCalls("ComposeEmail")
assert len(method_calls) == 0
def test_subject_too_long(self, portals, dbus_con):
"""test that a subject line over 200 chars triggers an error"""
email_intf = xdp.get_portal_iface(dbus_con, "Email")
mock_intf = xdp.get_mock_iface(dbus_con)
subject = "This subject line is too long" + "abc" * 60
assert len(subject) > 200
request = xdp.Request(dbus_con, email_intf)
options = {
"subject": subject,
}
with pytest.raises(dbus.exceptions.DBusException) as excinfo:
request.call(
"ComposeEmail",
parent_window="",
options=options,
)
assert (
excinfo.value.get_dbus_name()
== "org.freedesktop.portal.Error.InvalidArgument"
)
# Check the impl portal was never called
method_calls = mock_intf.GetMethodCalls("ComposeEmail")
assert len(method_calls) == 0
@pytest.mark.parametrize("template_params", ({"email": {"delay": 2000}},))
def test_delay(self, portals, dbus_con):
"""
Test that everything works as expected when the backend takes some
time to send its response, as * is to be expected from a real backend
that presents dialogs to the user.
"""
email_intf = xdp.get_portal_iface(dbus_con, "Email")
mock_intf = xdp.get_mock_iface(dbus_con)
subject = "delay test"
addresses = ["mclasen@redhat.com"]
request = xdp.Request(dbus_con, email_intf)
options = {
"addresses": addresses,
"subject": subject,
}
start_time = time.perf_counter()
response = request.call(
"ComposeEmail",
parent_window="",
options=options,
)
assert response
assert response.response == 0
end_time = time.perf_counter()
assert end_time - start_time > 2
# Check the impl portal was called with the right args
method_calls = mock_intf.GetMethodCalls("ComposeEmail")
assert len(method_calls) > 0
_, args = method_calls[-1]
assert args[2] == "" # parent window
assert args[3]["addresses"] == addresses
assert args[3]["subject"] == subject
@pytest.mark.parametrize("template_params", ({"email": {"response": 1}},))
def test_cancel(self, portals, dbus_con):
"""
Test that user cancellation works as expected.
We simulate that the user cancels a hypothetical dialog,
by telling the backend to return 1 as response code.
And we check that we get the expected G_IO_ERROR_CANCELLED.
"""
email_intf = xdp.get_portal_iface(dbus_con, "Email")
mock_intf = xdp.get_mock_iface(dbus_con)
subject = "cancel test"
addresses = ["mclasen@redhat.com"]
request = xdp.Request(dbus_con, email_intf)
options = {
"addresses": addresses,
"subject": subject,
}
response = request.call(
"ComposeEmail",
parent_window="",
options=options,
)
assert response
assert response.response == 1
# Check the impl portal was called with the right args
method_calls = mock_intf.GetMethodCalls("ComposeEmail")
assert len(method_calls) > 0
_, args = method_calls[-1]
assert args[2] == "" # parent window
assert args[3]["addresses"] == addresses
assert args[3]["subject"] == subject
@pytest.mark.parametrize("template_params", ({"email": {"expect-close": True}},))
def test_close(self, portals, dbus_con):
"""
Test that app-side cancellation works as expected.
We cancel the cancellable while while the hypothetical
dialog is up, and tell the backend that it should
expect a Close call. We rely on the backend to
verify that that call actually happened.
"""
email_intf = xdp.get_portal_iface(dbus_con, "Email")
mock_intf = xdp.get_mock_iface(dbus_con)
subject = "close test"
addresses = ["mclasen@redhat.com"]
request = xdp.Request(dbus_con, email_intf)
request.schedule_close(1000)
options = {
"addresses": addresses,
"subject": subject,
}
request.call(
"ComposeEmail",
parent_window="",
options=options,
)
# Only true if the impl.Request was closed too
assert request.closed
# Check the impl portal was called with the right args
method_calls = mock_intf.GetMethodCalls("ComposeEmail")
assert len(method_calls) > 0
_, args = method_calls[-1]
assert args[2] == "" # parent window
assert args[3]["addresses"] == addresses
assert args[3]["subject"] == subject

View File

@@ -0,0 +1,591 @@
# SPDX-License-Identifier: LGPL-2.1-or-later
#
# This file is formatted with Python Black
import tests as xdp
import dbus
import pytest
FILECHOOSER_RESULTS = {
"uris": ["file:///test.txt", "file:///example/test2.txt"],
"choices": [("encoding", "utf8"), ("reencode", "true"), ("third", "a")],
}
@pytest.fixture
def required_templates():
return {
"filechooser": {
"results": dbus.Dictionary(FILECHOOSER_RESULTS, signature="sv"),
},
"lockdown": {},
}
class TestFilechooser:
def test_version(self, portals, dbus_con):
xdp.check_version(dbus_con, "FileChooser", 4)
def test_open_file_basic(self, portals, dbus_con, app_id):
filechooser_intf = xdp.get_portal_iface(dbus_con, "FileChooser")
mock_intf = xdp.get_mock_iface(dbus_con)
title = "Test"
accept_label = "Accept"
multiple = True
options = {
"accept_label": accept_label,
"multiple": multiple,
}
request = xdp.Request(dbus_con, filechooser_intf)
response = request.call(
"OpenFile",
parent_window="",
title=title,
options=options,
)
assert response
assert response.response == 0
assert response.results["uris"] == FILECHOOSER_RESULTS["uris"]
# Check the impl portal was called with the right args
method_calls = mock_intf.GetMethodCalls("OpenFile")
assert len(method_calls) == 1
_, args = method_calls.pop()
assert args[1] == app_id
assert args[2] == "" # parent window
assert args[3] == title
assert args[4]["accept_label"] == accept_label
assert args[4]["multiple"] == multiple
@pytest.mark.parametrize("template_params", ({"filechooser": {"response": 1}},))
def test_open_file_cancel(self, portals, dbus_con, app_id):
filechooser_intf = xdp.get_portal_iface(dbus_con, "FileChooser")
mock_intf = xdp.get_mock_iface(dbus_con)
title = "Test"
accept_label = "Accept"
multiple = True
options = {
"accept_label": accept_label,
"multiple": multiple,
}
request = xdp.Request(dbus_con, filechooser_intf)
response = request.call(
"OpenFile",
parent_window="",
title=title,
options=options,
)
assert response
assert response.response == 1
# Check the impl portal was called with the right args
method_calls = mock_intf.GetMethodCalls("OpenFile")
assert len(method_calls) == 1
_, args = method_calls.pop()
assert args[1] == app_id
assert args[2] == "" # parent window
assert args[3] == title
assert args[4]["accept_label"] == accept_label
assert args[4]["multiple"] == multiple
@pytest.mark.parametrize(
"template_params", ({"filechooser": {"expect-close": True}},)
)
def test_open_file_close(self, portals, dbus_con, app_id):
filechooser_intf = xdp.get_portal_iface(dbus_con, "FileChooser")
mock_intf = xdp.get_mock_iface(dbus_con)
title = "Test"
accept_label = "Accept"
multiple = True
options = {
"accept_label": accept_label,
"multiple": multiple,
}
request = xdp.Request(dbus_con, filechooser_intf)
request.schedule_close(1000)
request.call(
"OpenFile",
parent_window="",
title=title,
options=options,
)
# Only true if the impl.Request was closed too
assert request.closed
# Check the impl portal was called with the right args
method_calls = mock_intf.GetMethodCalls("OpenFile")
assert len(method_calls) == 1
_, args = method_calls.pop()
assert args[1] == app_id
assert args[2] == "" # parent window
assert args[3] == title
assert args[4]["accept_label"] == accept_label
assert args[4]["multiple"] == multiple
def test_open_file_filter1(self, portals, dbus_con):
filechooser_intf = xdp.get_portal_iface(dbus_con, "FileChooser")
mock_intf = xdp.get_mock_iface(dbus_con)
options = {
"filters": [
(
"Images",
[
(dbus.UInt32(0), "*ico"),
(dbus.UInt32(1), "image/png"),
],
),
(
"Text",
[
(dbus.UInt32(0), "*.txt"),
],
),
],
}
request = xdp.Request(dbus_con, filechooser_intf)
response = request.call(
"OpenFile",
parent_window="",
title="Test",
options=options,
)
assert response
assert response.response == 0
assert response.results["uris"] == FILECHOOSER_RESULTS["uris"]
method_calls = mock_intf.GetMethodCalls("OpenFile")
assert len(method_calls) == 1
def test_open_file_filter2(self, portals, dbus_con):
filechooser_intf = xdp.get_portal_iface(dbus_con, "FileChooser")
options = {
"filters": [
(
"Text",
[
# Invalid filter type
(dbus.UInt32(4), "*.txt"),
],
),
],
}
request = xdp.Request(dbus_con, filechooser_intf)
with pytest.raises(dbus.exceptions.DBusException) as excinfo:
request.call(
"OpenFile",
parent_window="",
title="Test",
options=options,
)
assert (
excinfo.value.get_dbus_name()
== "org.freedesktop.portal.Error.InvalidArgument"
)
def test_open_file_current_filter1(self, portals, dbus_con):
filechooser_intf = xdp.get_portal_iface(dbus_con, "FileChooser")
mock_intf = xdp.get_mock_iface(dbus_con)
options = {
"filters": [
(
"Images",
[
(dbus.UInt32(0), "*ico"),
(dbus.UInt32(1), "image/png"),
],
),
(
"Text",
[
(dbus.UInt32(0), "*.txt"),
],
),
],
"current_filter": (
"Text",
[
(dbus.UInt32(0), "*.txt"),
],
),
}
request = xdp.Request(dbus_con, filechooser_intf)
response = request.call(
"OpenFile",
parent_window="",
title="Test",
options=options,
)
assert response
assert response.response == 0
assert response.results["uris"] == FILECHOOSER_RESULTS["uris"]
method_calls = mock_intf.GetMethodCalls("OpenFile")
assert len(method_calls) == 1
_, args = method_calls.pop()
assert args[4]["current_filter"] == options["current_filter"]
def test_open_file_current_filter2(self, portals, dbus_con):
filechooser_intf = xdp.get_portal_iface(dbus_con, "FileChooser")
mock_intf = xdp.get_mock_iface(dbus_con)
options = {
"current_filter": (
"Text",
[
(dbus.UInt32(0), "*.txt"),
],
),
}
request = xdp.Request(dbus_con, filechooser_intf)
response = request.call(
"OpenFile",
parent_window="",
title="Test",
options=options,
)
assert response
assert response.response == 0
assert response.results["uris"] == FILECHOOSER_RESULTS["uris"]
method_calls = mock_intf.GetMethodCalls("OpenFile")
assert len(method_calls) == 1
_, args = method_calls.pop()
assert args[4]["current_filter"] == options["current_filter"]
def test_open_file_current_filter3(self, portals, dbus_con):
filechooser_intf = xdp.get_portal_iface(dbus_con, "FileChooser")
options = {
"current_filter": (
"Text",
[
# Invalid filter type
(dbus.UInt32(6), "*.txt"),
],
),
}
request = xdp.Request(dbus_con, filechooser_intf)
with pytest.raises(dbus.exceptions.DBusException) as excinfo:
request.call(
"OpenFile",
parent_window="",
title="Test",
options=options,
)
assert (
excinfo.value.get_dbus_name()
== "org.freedesktop.portal.Error.InvalidArgument"
)
def test_open_file_current_filter4(self, portals, dbus_con):
filechooser_intf = xdp.get_portal_iface(dbus_con, "FileChooser")
options = {
"filters": [
(
"Images",
[
(dbus.UInt32(0), "*ico"),
(dbus.UInt32(1), "image/png"),
],
),
(
"Text",
[
(dbus.UInt32(0), "*.txt"),
],
),
],
"current_filter": (
"Something else",
[
(dbus.UInt32(0), "*.sth.else"),
],
),
}
request = xdp.Request(dbus_con, filechooser_intf)
with pytest.raises(dbus.exceptions.DBusException) as excinfo:
request.call(
"OpenFile",
parent_window="",
title="Test",
options=options,
)
assert (
excinfo.value.get_dbus_name()
== "org.freedesktop.portal.Error.InvalidArgument"
)
def test_open_file_choices1(self, portals, dbus_con):
filechooser_intf = xdp.get_portal_iface(dbus_con, "FileChooser")
mock_intf = xdp.get_mock_iface(dbus_con)
options = {
"choices": [
(
"encoding",
"Encoding",
[
("utf8", "Unicode"),
("latin15", "Western"),
],
"latin15",
),
(
"reencode",
"Reencode",
[],
"false",
),
(
"third",
"Third",
[("a", "A"), ("b", "B")],
"",
),
],
}
request = xdp.Request(dbus_con, filechooser_intf)
response = request.call(
"OpenFile",
parent_window="",
title="Test",
options=options,
)
assert response
assert response.response == 0
assert response.results["uris"] == FILECHOOSER_RESULTS["uris"]
method_calls = mock_intf.GetMethodCalls("OpenFile")
assert len(method_calls) == 1
_, args = method_calls.pop()
assert args[4]["choices"] == options["choices"]
def test_open_file_choices_invalid(self, portals, dbus_con):
filechooser_intf = xdp.get_portal_iface(dbus_con, "FileChooser")
invalid_choices = [
(
"encoding",
"Encoding",
[
("utf8", ""),
("latin15", "Western"),
],
"latin15",
),
(
"encoding",
"Encoding",
[
("", "Unicode"),
("latin15", "Western"),
],
"latin15",
),
(
"",
"Encoding",
[
("utf8", "Unicode"),
("latin15", "Western"),
],
"latin15",
),
]
for choice in invalid_choices:
request = xdp.Request(dbus_con, filechooser_intf)
with pytest.raises(dbus.exceptions.DBusException) as excinfo:
options = {
"choices": [choice],
}
request.call(
"OpenFile",
parent_window="",
title="Test",
options=options,
)
assert (
excinfo.value.get_dbus_name()
== "org.freedesktop.portal.Error.InvalidArgument"
)
def test_save_file_basic(self, portals, dbus_con, app_id):
filechooser_intf = xdp.get_portal_iface(dbus_con, "FileChooser")
mock_intf = xdp.get_mock_iface(dbus_con)
title = "Test"
accept_label = "Accept"
current_name = "File Name"
options = {
"accept_label": accept_label,
"current_name": current_name,
}
request = xdp.Request(dbus_con, filechooser_intf)
response = request.call(
"SaveFile",
parent_window="",
title=title,
options=options,
)
assert response
assert response.response == 0
assert response.results["uris"] == FILECHOOSER_RESULTS["uris"]
# Check the impl portal was called with the right args
method_calls = mock_intf.GetMethodCalls("SaveFile")
assert len(method_calls) == 1
_, args = method_calls.pop()
assert args[1] == app_id
assert args[2] == "" # parent window
assert args[3] == title
assert args[4]["accept_label"] == accept_label
assert args[4]["current_name"] == current_name
@pytest.mark.parametrize("template_params", ({"filechooser": {"response": 1}},))
def test_save_file_cancel(self, portals, dbus_con, app_id):
filechooser_intf = xdp.get_portal_iface(dbus_con, "FileChooser")
mock_intf = xdp.get_mock_iface(dbus_con)
title = "Test"
accept_label = "Accept"
current_name = "File Name"
options = {
"accept_label": accept_label,
"current_name": current_name,
}
request = xdp.Request(dbus_con, filechooser_intf)
response = request.call(
"SaveFile",
parent_window="",
title=title,
options=options,
)
assert response
assert response.response == 1
# Check the impl portal was called with the right args
method_calls = mock_intf.GetMethodCalls("SaveFile")
assert len(method_calls) == 1
_, args = method_calls.pop()
assert args[1] == app_id
assert args[2] == "" # parent window
assert args[3] == title
assert args[4]["accept_label"] == accept_label
assert args[4]["current_name"] == current_name
@pytest.mark.parametrize(
"template_params", ({"filechooser": {"expect-close": True}},)
)
def test_save_file_close(self, portals, dbus_con, app_id):
filechooser_intf = xdp.get_portal_iface(dbus_con, "FileChooser")
mock_intf = xdp.get_mock_iface(dbus_con)
title = "Test"
accept_label = "Accept"
current_name = "File Name"
options = {
"accept_label": accept_label,
"current_name": current_name,
}
request = xdp.Request(dbus_con, filechooser_intf)
request.schedule_close(1000)
request.call(
"SaveFile",
parent_window="",
title=title,
options=options,
)
# Only true if the impl.Request was closed too
assert request.closed
# Check the impl portal was called with the right args
method_calls = mock_intf.GetMethodCalls("SaveFile")
assert len(method_calls) == 1
_, args = method_calls.pop()
assert args[1] == app_id
assert args[2] == "" # parent window
assert args[3] == title
assert args[4]["accept_label"] == accept_label
assert args[4]["current_name"] == current_name
def test_save_file_filters(self, portals, dbus_con, app_id):
filechooser_intf = xdp.get_portal_iface(dbus_con, "FileChooser")
mock_intf = xdp.get_mock_iface(dbus_con)
options = {
"filters": [
(
"Images",
[
(dbus.UInt32(0), "*ico"),
(dbus.UInt32(1), "image/png"),
],
),
(
"Text",
[
(dbus.UInt32(0), "*.txt"),
],
),
],
}
request = xdp.Request(dbus_con, filechooser_intf)
response = request.call(
"SaveFile",
parent_window="",
title="Title",
options=options,
)
assert response
assert response.response == 0
assert response.results["uris"] == FILECHOOSER_RESULTS["uris"]
# Check the impl portal was called with the right args
method_calls = mock_intf.GetMethodCalls("SaveFile")
assert len(method_calls) == 1
_, args = method_calls.pop()
assert args[4]["filters"] == options["filters"]
@pytest.mark.parametrize(
"template_params", ({"lockdown": {"disable-save-to-disk": True}},)
)
def test_save_file_lockdown(self, portals, dbus_con, app_id):
filechooser_intf = xdp.get_portal_iface(dbus_con, "FileChooser")
mock_intf = xdp.get_mock_iface(dbus_con)
request = xdp.Request(dbus_con, filechooser_intf)
with pytest.raises(dbus.exceptions.DBusException) as excinfo:
request.call(
"SaveFile",
parent_window="",
title="Title",
options={},
)
assert (
excinfo.value.get_dbus_name() == "org.freedesktop.portal.Error.NotAllowed"
)
# Check the impl portal was not called
method_calls = mock_intf.GetMethodCalls("FileChooser")
assert len(method_calls) == 0

View File

@@ -0,0 +1,216 @@
# SPDX-License-Identifier: LGPL-2.1-or-later
#
# This file is formatted with Python Black
import tests as xdp
import dbus
import pytest
import time
@pytest.fixture
def required_templates():
return {"globalshortcuts": {}}
class TestGlobalShortcuts:
def test_version(self, portals, dbus_con):
xdp.check_version(dbus_con, "GlobalShortcuts", 1)
def test_create_close_session(self, portals, dbus_con, app_id):
globalshortcuts_intf = xdp.get_portal_iface(dbus_con, "GlobalShortcuts")
mock_intf = xdp.get_mock_iface(dbus_con)
request = xdp.Request(dbus_con, globalshortcuts_intf)
options = {
"session_handle_token": "session_token0",
}
response = request.call(
"CreateSession",
options=options,
)
assert response
assert response.response == 0
session = xdp.Session.from_response(dbus_con, response)
# Check the impl portal was called with the right args
method_calls = mock_intf.GetMethodCalls("CreateSession")
assert len(method_calls) > 0
_, args = method_calls[-1]
assert args[1] == session.handle
assert args[2] == app_id
session.close()
xdp.wait_for(lambda: session.closed)
@pytest.mark.parametrize(
"template_params", ({"globalshortcuts": {"force-close": 500}},)
)
def test_create_session_signal_closed(self, portals, dbus_con, app_id):
globalshortcuts_intf = xdp.get_portal_iface(dbus_con, "GlobalShortcuts")
mock_intf = xdp.get_mock_iface(dbus_con)
request = xdp.Request(dbus_con, globalshortcuts_intf)
options = {
"session_handle_token": "session_token0",
}
response = request.call(
"CreateSession",
options=options,
)
assert response
assert response.response == 0
session = xdp.Session.from_response(dbus_con, response)
# Check the impl portal was called with the right args
method_calls = mock_intf.GetMethodCalls("CreateSession")
assert len(method_calls) > 0
_, args = method_calls[-1]
assert args[1] == session.handle
assert args[2] == app_id
# Now expect the backend to close it
xdp.wait_for(lambda: session.closed)
def test_bind_list_shortcuts(self, portals, dbus_con):
globalshortcuts_intf = xdp.get_portal_iface(dbus_con, "GlobalShortcuts")
request = xdp.Request(dbus_con, globalshortcuts_intf)
options = {
"session_handle_token": "session_token0",
}
response = request.call(
"CreateSession",
options=options,
)
assert response
assert response.response == 0
session = xdp.Session.from_response(dbus_con, response)
shortcuts = [
(
"binding1",
{
"description": dbus.String("Binding #1", variant_level=1),
"preferred-trigger": dbus.String("CTRL+a", variant_level=1),
},
),
(
"binding2",
{
"description": dbus.String("Binding #2", variant_level=1),
"preferred-trigger": dbus.String("CTRL+b", variant_level=1),
},
),
]
request = xdp.Request(dbus_con, globalshortcuts_intf)
response = request.call(
"BindShortcuts",
session_handle=session.handle,
shortcuts=shortcuts,
parent_window="",
options={},
)
assert response
assert response.response == 0
request = xdp.Request(dbus_con, globalshortcuts_intf)
options = {}
response = request.call(
"ListShortcuts",
session_handle=session.handle,
options=options,
)
assert response
assert response.response == 0
assert len(list(response.results["shortcuts"])) == len(list(shortcuts))
session.close()
xdp.wait_for(lambda: session.closed)
def test_trigger(self, portals, dbus_con):
globalshortcuts_intf = xdp.get_portal_iface(dbus_con, "GlobalShortcuts")
mock_intf = xdp.get_mock_iface(dbus_con)
request = xdp.Request(dbus_con, globalshortcuts_intf)
options = {
"session_handle_token": "session_token0",
}
response = request.call(
"CreateSession",
options=options,
)
assert response
assert response.response == 0
session = xdp.Session.from_response(dbus_con, response)
shortcuts = [
(
"binding1",
{
"description": dbus.String("Binding #1", variant_level=1),
"preferred-trigger": dbus.String("CTRL+a", variant_level=1),
},
),
]
request = xdp.Request(dbus_con, globalshortcuts_intf)
response = request.call(
"BindShortcuts",
session_handle=session.handle,
shortcuts=shortcuts,
parent_window="",
options={},
)
assert response
assert response.response == 0
activated_count = 0
deactivated_count = 0
def cb_activated(session_handle, shortcut_id, timestamp, options):
nonlocal activated_count
now_since_epoch = int(time.time() * 1000000)
# This assert will race twice a year on systems configured with
# summer time timezone changes
assert (
now_since_epoch > timestamp
and (now_since_epoch - 10 * 10001000) < timestamp
)
assert shortcut_id == "binding1"
activated_count += 1
def cb_deactivated(session_handle, shortcut_id, timestamp, options):
nonlocal deactivated_count
now_since_epoch = int(time.time() * 1000000)
# This assert will race twice a year on systems configured with
# summer time timezone changes
assert (
now_since_epoch > timestamp
and (now_since_epoch - 10 * 10001000) < timestamp
)
assert shortcut_id == "binding1"
deactivated_count += 1
globalshortcuts_intf.connect_to_signal("Activated", cb_activated)
globalshortcuts_intf.connect_to_signal("Deactivated", cb_deactivated)
mock_intf.Trigger(session.handle, "binding1")
xdp.wait_for(lambda: activated_count == 1 and deactivated_count == 1)
assert not session.closed
session.close()
xdp.wait_for(lambda: session.closed)

View File

@@ -0,0 +1,242 @@
# SPDX-License-Identifier: LGPL-2.1-or-later
#
# This file is formatted with Python Black
import tests as xdp
import pytest
from enum import Enum, Flag
class InhibitFlags(Flag):
LOGOUT = 1
USER_SWITCH = 2
SUSPEND = 4
IDLE = 8
ALL = 16 - 1
class SessionState(Enum):
RUNNING = 1
QUERY_END = 2
ENDING = 3
@pytest.fixture
def required_templates():
return {"inhibit": {}}
class TestInhibit:
def set_permissions(self, dbus_con, app_id, permissions):
perm_store_intf = xdp.get_permission_store_iface(dbus_con)
perm_store_intf.SetPermission(
"inhibit",
True,
"inhibit",
app_id,
permissions,
)
def test_version(self, portals, dbus_con):
xdp.check_version(dbus_con, "Inhibit", 3)
def test_basic(self, portals, dbus_con, app_id):
inhibit_intf = xdp.get_portal_iface(dbus_con, "Inhibit")
mock_intf = xdp.get_mock_iface(dbus_con)
reason = "reason"
flags = InhibitFlags.ALL
request = xdp.Request(dbus_con, inhibit_intf)
options = {
"reason": reason,
}
response = request.call(
"Inhibit",
window="",
flags=flags.value,
options=options,
)
assert response
assert response.response == 0
# Check the impl portal was called with the right args
method_calls = mock_intf.GetMethodCalls("Inhibit")
assert len(method_calls) > 0
_, args = method_calls[-1]
assert args[1] == app_id
assert args[2] == "" # parent window
assert args[3] == flags.value
assert args[4]["reason"] == reason
@pytest.mark.parametrize("template_params", ({"inhibit": {"response": 1}},))
def test_cancel(self, portals, dbus_con, app_id):
inhibit_intf = xdp.get_portal_iface(dbus_con, "Inhibit")
mock_intf = xdp.get_mock_iface(dbus_con)
reason = "reason"
flags = InhibitFlags.ALL
request = xdp.Request(dbus_con, inhibit_intf)
options = {
"reason": reason,
}
response = request.call(
"Inhibit",
window="",
flags=flags.value,
options=options,
)
# for some reason, the backend failing is still considered a success
assert response
assert response.response == 0
# Check the impl portal was called with the right args
method_calls = mock_intf.GetMethodCalls("Inhibit")
assert len(method_calls) > 0
_, args = method_calls[-1]
assert args[1] == app_id
assert args[2] == "" # parent window
assert args[3] == flags.value
assert args[4]["reason"] == reason
@pytest.mark.parametrize("template_params", ({"inhibit": {"expect-close": True}},))
def test_close(self, portals, dbus_con, app_id):
inhibit_intf = xdp.get_portal_iface(dbus_con, "Inhibit")
mock_intf = xdp.get_mock_iface(dbus_con)
reason = "reason"
flags = InhibitFlags.ALL
request = xdp.Request(dbus_con, inhibit_intf)
request.schedule_close(1000)
options = {
"reason": reason,
}
request.call(
"Inhibit",
window="",
flags=flags.value,
options=options,
)
# Only true if the impl.Request was closed too
assert request.closed
# Check the impl portal was called with the right args
method_calls = mock_intf.GetMethodCalls("Inhibit")
assert len(method_calls) > 0
_, args = method_calls[-1]
assert args[1] == app_id
assert args[2] == "" # parent window
assert args[3] == flags.value
assert args[4]["reason"] == reason
def test_permission(self, portals, dbus_con, app_id):
inhibit_intf = xdp.get_portal_iface(dbus_con, "Inhibit")
mock_intf = xdp.get_mock_iface(dbus_con)
self.set_permissions(dbus_con, app_id, ["logout", "suspend"])
reason = "reason"
flags = InhibitFlags.LOGOUT | InhibitFlags.SUSPEND | InhibitFlags.IDLE
allowed_flags = InhibitFlags.LOGOUT | InhibitFlags.SUSPEND
request = xdp.Request(dbus_con, inhibit_intf)
options = {
"reason": reason,
}
response = request.call(
"Inhibit",
window="",
flags=flags.value,
options=options,
)
assert response
assert response.response == 0
method_calls = mock_intf.GetMethodCalls("Inhibit")
_, args = method_calls[-1]
assert args[3] == allowed_flags.value
self.set_permissions(dbus_con, app_id, ["suspend"])
flags = InhibitFlags.LOGOUT | InhibitFlags.SUSPEND | InhibitFlags.IDLE
allowed_flags = InhibitFlags.SUSPEND
request = xdp.Request(dbus_con, inhibit_intf)
options = {
"reason": reason,
}
response = request.call(
"Inhibit",
window="",
flags=flags.value,
options=options,
)
assert response
assert response.response == 0
method_calls = mock_intf.GetMethodCalls("Inhibit")
_, args = method_calls[-1]
assert args[3] == allowed_flags.value
def test_monitor(self, portals, dbus_con, app_id):
inhibit_intf = xdp.get_portal_iface(dbus_con, "Inhibit")
mock_intf = xdp.get_mock_iface(dbus_con)
changed_count = 0
request = xdp.Request(dbus_con, inhibit_intf)
options = {
"session_handle_token": "session_token0",
}
response = request.call(
"CreateMonitor",
window="",
options=options,
)
assert response
assert response.response == 0
session = xdp.Session.from_response(dbus_con, response)
# Check the impl portal was called with the right args
method_calls = mock_intf.GetMethodCalls("CreateMonitor")
assert len(method_calls) > 0
_, args = method_calls[-1]
assert args[1] == session.handle
assert args[2] == app_id
assert args[3] == "" # parent window
def state_changed_cb(session_handle, state):
nonlocal changed_count
assert not state["screensaver-active"]
assert state["session-state"] == SessionState.QUERY_END.value
changed_count += 1
inhibit_intf.connect_to_signal("StateChanged", state_changed_cb)
# wait for a Query End state change
xdp.wait_for(lambda: changed_count == 1)
assert not session.closed
# and respond with QueryEndResponse
inhibit_intf.QueryEndResponse(session.handle)
# wait for another Query End state change
xdp.wait_for(lambda: changed_count == 2)
assert not session.closed
# do not respond with QueryEndResponse and instead wait for >1s
xdp.wait(1500)
# the session should have gotten closed by now
assert session.closed

View File

@@ -0,0 +1,674 @@
# SPDX-License-Identifier: LGPL-2.1-or-later
#
# This file is formatted with Python Black
import tests as xdp
import dbus
import pytest
import socket
from gi.repository import GLib
from itertools import count
from typing import Any
counter = count()
def default_zones():
return [(1024, 768, 0, 0), (640, 480, 1024, 0)]
@pytest.fixture
def required_templates():
return {"inputcapture": {}}
@pytest.fixture
def zones():
return default_zones()
class TestInputCapture:
def create_session(self, dbus_con, capabilities=0xF):
"""
Call CreateSession for the given capabilities and return the
(response, results) tuple.
"""
inputcapture_intf = xdp.get_portal_iface(dbus_con, "InputCapture")
mock_intf = xdp.get_mock_iface(dbus_con)
request = xdp.Request(dbus_con, inputcapture_intf)
capabilities = dbus.UInt32(capabilities, variant_level=1)
session_handle_token = dbus.String(f"session{next(counter)}", variant_level=1)
options = dbus.Dictionary(
{
"capabilities": capabilities,
"session_handle_token": session_handle_token,
},
signature="sv",
)
response = request.call("CreateSession", parent_window="", options=options)
assert response
assert response.response == 0
assert "session_handle" in response.results
assert "capabilities" in response.results
caps = response.results["capabilities"]
# Returned capabilities must be a subset of the requested ones
assert caps & ~capabilities == 0
self.current_session_handle = response.results["session_handle"]
# Check the impl portal was called with the right args
method_calls = mock_intf.GetMethodCalls("CreateSession")
assert len(method_calls) > 0
_, args = method_calls[-1]
assert args[3] == "" # parent window
assert args[4]["capabilities"] == capabilities
return response
def get_zones(self, dbus_con):
"""
Call GetZones and return the (response, results) tuple.
"""
inputcapture_intf = xdp.get_portal_iface(dbus_con, "InputCapture")
mock_intf = xdp.get_mock_iface(dbus_con)
request = xdp.Request(dbus_con, inputcapture_intf)
options: Any = {}
response = request.call(
"GetZones", session_handle=self.current_session_handle, options=options
)
assert response
assert response.response == 0
assert "zones" in response.results
assert "zone_set" in response.results
self.current_zone_set = response.results["zone_set"]
# Check the impl portal was called with the right args
method_calls = mock_intf.GetMethodCalls("GetZones")
assert len(method_calls) > 0
_, args = method_calls[-1]
assert args[0] == request.handle
assert args[1] == self.current_session_handle
return response
def set_pointer_barriers(self, dbus_con, barriers):
inputcapture_intf = xdp.get_portal_iface(dbus_con, "InputCapture")
mock_intf = xdp.get_mock_iface(dbus_con)
request = xdp.Request(dbus_con, inputcapture_intf)
options: Any = {}
response = request.call(
"SetPointerBarriers",
session_handle=self.current_session_handle,
options=options,
barriers=barriers,
zone_set=self.current_zone_set,
)
assert response
assert response.response == 0
assert "failed_barriers" in response.results
# Check the impl portal was called with the right args
method_calls = mock_intf.GetMethodCalls("SetPointerBarriers")
assert len(method_calls) > 0
_, args = method_calls[-1]
assert args[0] == request.handle
assert args[1] == self.current_session_handle
assert args[4] == barriers
assert args[5] == self.current_zone_set
return response
def connect_to_eis(self, dbus_con):
inputcapture_intf = xdp.get_portal_iface(dbus_con, "InputCapture")
mock_intf = xdp.get_mock_iface(dbus_con)
fd = inputcapture_intf.ConnectToEIS(
self.current_session_handle, dbus.Dictionary({}, signature="sv")
)
# Our dbusmock template sends HELLO
eis_socket = socket.fromfd(fd.take(), socket.AF_UNIX, socket.SOCK_STREAM)
hello = eis_socket.recv(10)
assert hello == b"HELLO"
method_calls = mock_intf.GetMethodCalls("ConnectToEIS")
assert len(method_calls) > 0
_, args = method_calls[-1]
assert args[0] == self.current_session_handle
return eis_socket
def enable(self, dbus_con):
inputcapture_intf = xdp.get_portal_iface(dbus_con, "InputCapture")
mock_intf = xdp.get_mock_iface(dbus_con)
inputcapture_intf.Enable(
self.current_session_handle, dbus.Dictionary({}, signature="sv")
)
method_calls = mock_intf.GetMethodCalls("Enable")
assert len(method_calls) > 0
_, args = method_calls[-1]
assert args[0] == self.current_session_handle
def disable(self, dbus_con):
inputcapture_intf = xdp.get_portal_iface(dbus_con, "InputCapture")
mock_intf = xdp.get_mock_iface(dbus_con)
inputcapture_intf.Disable(
self.current_session_handle, dbus.Dictionary({}, signature="sv")
)
method_calls = mock_intf.GetMethodCalls("Disable")
assert len(method_calls) > 0
_, args = method_calls[-1]
assert args[0] == self.current_session_handle
def release(self, dbus_con, activation_id: int, cursor_position=None):
inputcapture_intf = xdp.get_portal_iface(dbus_con, "InputCapture")
mock_intf = xdp.get_mock_iface(dbus_con)
options = {"activation_id": dbus.UInt32(activation_id)}
if cursor_position:
options["cursor_position"] = dbus.Struct(
list(cursor_position), signature="dd", variant_level=1
)
inputcapture_intf.Release(
self.current_session_handle, dbus.Dictionary(options, signature="sv")
)
method_calls = mock_intf.GetMethodCalls("Release")
assert len(method_calls) > 0
_, args = method_calls[-1]
assert args[0] == self.current_session_handle
assert "activation_id" in args[2]
aid = args[2]["activation_id"]
assert aid == activation_id
if cursor_position:
assert "cursor_position" in args[2]
pos = args[2]["cursor_position"]
assert pos == cursor_position
def test_version(self, portals, dbus_con):
xdp.check_version(dbus_con, "InputCapture", 1)
@pytest.mark.parametrize(
"template_params",
(
{
"inputcapture": {
"supported_capabilities": 0b101, # KEYBOARD, POINTER, TOUCH
},
},
),
)
def test_supported_capabilities(self, portals, dbus_con):
properties_intf = xdp.get_iface(dbus_con, "org.freedesktop.DBus.Properties")
caps = properties_intf.Get(
"org.freedesktop.portal.InputCapture", "SupportedCapabilities"
)
assert caps == 0b101
def test_create_session(self, portals, dbus_con):
mock_intf = xdp.get_mock_iface(dbus_con)
self.create_session(dbus_con, capabilities=0b1) # KEYBOARD
# Check the impl portal was called with the right args
method_calls = mock_intf.GetMethodCalls("CreateSession")
assert len(method_calls) == 1
_, args = method_calls.pop(0)
assert args[3] == "" # parent window
assert args[4]["capabilities"] == 0b1
@pytest.mark.parametrize(
"template_params",
(
{
"inputcapture": {
"capabilities": 0b110, # TOUCH, POINTER
"supported_capabilities": 0b111, # TOUCH, POINTER, KEYBOARD
},
},
),
)
def test_create_session_limited_caps(self, portals, dbus_con):
mock_intf = xdp.get_mock_iface(dbus_con)
# Request more caps than are supported
response, results = self.create_session(dbus_con, capabilities=0b111)
caps = results["capabilities"]
# Returned capabilities must the ones we set up in the params
assert caps == 0b110
# Check the impl portal was called with the right args
method_calls = mock_intf.GetMethodCalls("CreateSession")
assert len(method_calls) == 1
_, args = method_calls.pop(0)
assert args[3] == "" # parent window
assert args[4]["capabilities"] == 0b111
@pytest.mark.parametrize(
"template_params",
(
{
"inputcapture": {
"default-zone": dbus.Array(
[dbus.Struct(z, signature="uuii") for z in default_zones()],
signature="(uuii)",
variant_level=1,
)
},
},
),
)
def test_get_zones(self, portals, dbus_con, zones):
mock_intf = xdp.get_mock_iface(dbus_con)
response, results = self.create_session(dbus_con)
response, results = self.get_zones(dbus_con)
for z1, z2 in zip(results["zones"], zones):
assert z1 == z2
# Check the impl portal was called with the right args
method_calls = mock_intf.GetMethodCalls("CreateSession")
assert len(method_calls) == 1
method_calls = mock_intf.GetMethodCalls("GetZones")
assert len(method_calls) == 1
@pytest.mark.parametrize(
"template_params",
(
{
"inputcapture": {
"default-zone": dbus.Array(
[dbus.Struct(z, signature="uuii") for z in default_zones()],
signature="(uuii)",
variant_level=1,
)
},
},
),
)
def test_set_pointer_barriers(self, portals, dbus_con, zones):
mock_intf = xdp.get_mock_iface(dbus_con)
response, results = self.create_session(dbus_con)
response, results = self.get_zones(dbus_con)
barriers = [
{
"barrier_id": dbus.UInt32(10, variant_level=1),
"position": dbus.Struct(
[0, 0, 0, 768], signature="iiii", variant_level=1
),
},
{
"barrier_id": dbus.UInt32(11, variant_level=1),
"position": dbus.Struct(
[0, 0, 1024, 0], signature="iiii", variant_level=1
),
},
{
"barrier_id": dbus.UInt32(12, variant_level=1),
"position": dbus.Struct(
[1024, 0, 1024, 768], signature="iiii", variant_level=1
),
},
{
"barrier_id": dbus.UInt32(13, variant_level=1),
"position": dbus.Struct(
[0, 768, 1024, 768], signature="iiii", variant_level=1
),
},
{
"barrier_id": dbus.UInt32(14, variant_level=1),
"position": dbus.Struct(
[100, 768, 500, 768], signature="iiii", variant_level=1
),
},
{
"barrier_id": dbus.UInt32(15, variant_level=1),
"position": dbus.Struct(
[1024, 0, 1024, 480], signature="iiii", variant_level=1
),
},
{
"barrier_id": dbus.UInt32(16, variant_level=1),
"position": dbus.Struct(
[1024 + 640, 0, 1024 + 640, 480], signature="iiii", variant_level=1
),
},
# invalid ones
{
"barrier_id": dbus.UInt32(20, variant_level=1),
"position": dbus.Struct(
[0, 1, 3, 4], signature="iiii", variant_level=1
),
},
{
"barrier_id": dbus.UInt32(21, variant_level=1),
"position": dbus.Struct(
[0, 1, 1024, 1], signature="iiii", variant_level=1
),
},
{
"barrier_id": dbus.UInt32(22, variant_level=1),
"position": dbus.Struct(
[1, 0, 1, 768], signature="iiii", variant_level=1
),
},
{
"barrier_id": dbus.UInt32(23, variant_level=1),
"position": dbus.Struct(
[1023, 0, 1023, 768], signature="iiii", variant_level=1
),
},
{
"barrier_id": dbus.UInt32(24, variant_level=1),
"position": dbus.Struct(
[0, 0, 1050, 0], signature="iiii", variant_level=1
),
},
]
response, results = self.set_pointer_barriers(dbus_con, barriers=barriers)
failed_barriers = results["failed_barriers"]
assert all([id >= 20 for id in failed_barriers])
for id in [b["barrier_id"] for b in barriers if b["barrier_id"] >= 20]:
assert id in failed_barriers
# Check the impl portal was called with the right args
method_calls = mock_intf.GetMethodCalls("CreateSession")
assert len(method_calls) == 1
method_calls = mock_intf.GetMethodCalls("GetZones")
assert len(method_calls) == 1
method_calls = mock_intf.GetMethodCalls("SetPointerBarriers")
assert len(method_calls) == 1
_, args = method_calls.pop(0)
assert args[4] == barriers
assert args[5] == self.current_zone_set
def test_connect_to_eis(self, portals, dbus_con):
self.create_session(dbus_con)
self.get_zones(dbus_con)
# The default zone is 1920x1080
barriers = [
{
"barrier_id": dbus.UInt32(10, variant_level=1),
"position": dbus.Struct(
[0, 0, 1920, 0], signature="iiii", variant_level=1
),
},
]
self.set_pointer_barriers(dbus_con, barriers)
self.connect_to_eis(dbus_con)
def test_enable_disable(self, portals, dbus_con):
mock_intf = xdp.get_mock_iface(dbus_con)
self.create_session(dbus_con)
self.get_zones(dbus_con)
# The default zone is 1920x1080
barriers = [
{
"barrier_id": dbus.UInt32(10, variant_level=1),
"position": dbus.Struct(
[0, 0, 1920, 0], signature="iiii", variant_level=1
),
},
]
self.set_pointer_barriers(dbus_con, barriers)
self.connect_to_eis(dbus_con)
# Disable before enable should be a noop
self.disable(dbus_con)
method_calls = mock_intf.GetMethodCalls("Disable")
assert len(method_calls) == 1
self.enable(dbus_con)
method_calls = mock_intf.GetMethodCalls("Enable")
assert len(method_calls) == 1
self.disable(dbus_con)
method_calls = mock_intf.GetMethodCalls("Disable")
assert len(method_calls) == 2
@pytest.mark.parametrize(
"template_params",
(
{
"inputcapture": {
"disable-delay": 200,
},
},
),
)
def test_disable_signal(self, portals, dbus_con):
inputcapture_intf = xdp.get_portal_iface(dbus_con, "InputCapture")
self.create_session(dbus_con)
self.get_zones(dbus_con)
# The default zone is 1920x1080
barriers = [
{
"barrier_id": dbus.UInt32(10, variant_level=1),
"position": dbus.Struct(
[0, 0, 1920, 0], signature="iiii", variant_level=1
),
},
]
self.set_pointer_barriers(dbus_con, barriers)
self.connect_to_eis(dbus_con)
disabled_signal_received = False
def cb_disabled(session_handle, options):
nonlocal disabled_signal_received
disabled_signal_received = True
assert session_handle == session_handle
inputcapture_intf.connect_to_signal("Disabled", cb_disabled)
self.enable(dbus_con)
xdp.wait_for(lambda: disabled_signal_received)
@pytest.mark.parametrize(
"template_params",
(
{
"inputcapture": {
"activated-delay": 200,
"deactivated-delay": 300,
},
},
),
)
def test_activated_signal(self, portals, dbus_con):
inputcapture_intf = xdp.get_portal_iface(dbus_con, "InputCapture")
self.create_session(dbus_con)
self.get_zones(dbus_con)
# The default zone is 1920x1080
barriers = [
{
"barrier_id": dbus.UInt32(10, variant_level=1),
"position": dbus.Struct(
[0, 0, 1920, 0], signature="iiii", variant_level=1
),
},
]
self.set_pointer_barriers(dbus_con, barriers)
self.connect_to_eis(dbus_con)
disabled_signal_received = False
activated_signal_received = False
deactivated_signal_received = False
def cb_disabled(session_handle, options):
nonlocal disabled_signal_received
disabled_signal_received = True
def cb_activated(session_handle, options):
nonlocal activated_signal_received
activated_signal_received = True
assert session_handle == session_handle
assert "activation_id" in options
assert "barrier_id" in options
assert options["barrier_id"] == 10 # template uses first barrier
assert "cursor_position" in options
assert options["cursor_position"] == (
10.0,
20.0,
) # template uses x+10, y+20 of first barrier
def cb_deactivated(session_handle, options):
nonlocal deactivated_signal_received
deactivated_signal_received = True
assert session_handle == session_handle
assert "activation_id" in options
assert "cursor_position" in options
assert options["cursor_position"] == (
10.0,
20.0,
) # template uses x+10, y+20 of first barrier
inputcapture_intf.connect_to_signal("Activated", cb_activated)
inputcapture_intf.connect_to_signal("Deactivated", cb_deactivated)
inputcapture_intf.connect_to_signal("Disabled", cb_disabled)
self.enable(dbus_con)
xdp.wait_for(lambda: activated_signal_received and deactivated_signal_received)
assert not disabled_signal_received
# Disabling should not trigger the signal
self.disable(dbus_con)
assert not disabled_signal_received
@pytest.mark.parametrize(
"template_params",
(
{
"inputcapture": {
"zones-changed-delay": 200,
},
},
),
)
def test_zones_changed_signal(self, portals, dbus_con):
inputcapture_intf = xdp.get_portal_iface(dbus_con, "InputCapture")
self.create_session(dbus_con)
self.get_zones(dbus_con)
# The default zone is 1920x1080
barriers = [
{
"barrier_id": dbus.UInt32(10, variant_level=1),
"position": dbus.Struct(
[0, 0, 1920, 0], signature="iiii", variant_level=1
),
},
]
self.set_pointer_barriers(dbus_con, barriers)
self.connect_to_eis(dbus_con)
zones_changed_signal_received = False
def cb_zones_changed(session_handle, options):
nonlocal zones_changed_signal_received
zones_changed_signal_received = True
assert session_handle == session_handle
inputcapture_intf.connect_to_signal("ZonesChanged", cb_zones_changed)
self.enable(dbus_con)
xdp.wait_for(lambda: zones_changed_signal_received)
@pytest.mark.parametrize(
"template_params",
(
{
"inputcapture": {
"activated-delay": 200,
"deactivated-delay": 1000,
"disabled-delay": 1200,
},
},
),
)
def test_release(self, portals, dbus_con):
inputcapture_intf = xdp.get_portal_iface(dbus_con, "InputCapture")
self.create_session(dbus_con)
self.get_zones(dbus_con)
# The default zone is 1920x1080
barriers = [
{
"barrier_id": dbus.UInt32(10, variant_level=1),
"position": dbus.Struct(
[0, 0, 1920, 0], signature="iiii", variant_level=1
),
},
]
self.set_pointer_barriers(dbus_con, barriers)
self.connect_to_eis(dbus_con)
disabled_signal_received = False
activated_signal_received = False
deactivated_signal_received = False
activation_id = None
def cb_disabled(session_handle, options):
nonlocal disabled_signal_received
disabled_signal_received = True
def cb_activated(session_handle, options):
nonlocal activated_signal_received, activation_id
activated_signal_received = True
activation_id = options["activation_id"]
def cb_deactivated(session_handle, options):
nonlocal deactivated_signal_received
deactivated_signal_received = True
inputcapture_intf.connect_to_signal("Disabled", cb_activated)
inputcapture_intf.connect_to_signal("Activated", cb_activated)
inputcapture_intf.connect_to_signal("Deactivated", cb_deactivated)
self.enable(dbus_con)
xdp.wait_for(lambda: activated_signal_received)
assert activation_id is not None
assert not deactivated_signal_received
assert not disabled_signal_received
self.release(
dbus_con, cursor_position=(10.0, 50.0), activation_id=activation_id
)
# XDP should filter any signals the implementation may
# send after Release().
mainloop = GLib.MainLoop()
GLib.timeout_add(1000, mainloop.quit)
mainloop.run()
# Release() implies deactivated
assert not deactivated_signal_received
assert not disabled_signal_received

View File

@@ -0,0 +1,125 @@
# SPDX-License-Identifier: LGPL-2.1-or-later
import tests as xdp
import pytest
import dbus
@pytest.fixture
def required_templates():
return {"geoclue2": {}}
class TestLocation:
def test_version(self, portals, dbus_con):
xdp.check_version(dbus_con, "Location", 1)
def get_geoclue_mock(self, dbus_con_sys):
geoclue_manager_proxy = dbus_con_sys.get_object(
"org.freedesktop.GeoClue2",
"/org/freedesktop/GeoClue2/Manager",
)
geoclue_manager = dbus.Interface(
geoclue_manager_proxy, "org.freedesktop.GeoClue2.Manager"
)
geoclue_client_proxy = dbus_con_sys.get_object(
"org.freedesktop.GeoClue2", geoclue_manager.GetClient()
)
geoclue_mock = dbus.Interface(
geoclue_client_proxy, "org.freedesktop.GeoClue2.Mock"
)
return geoclue_mock
@pytest.mark.parametrize("required_templates", ({},))
def test_no_geoclue(self, portals, dbus_con):
location_intf = xdp.get_portal_iface(dbus_con, "Location")
session = xdp.Session(
dbus_con,
location_intf.CreateSession({"session_handle_token": "session_token0"}),
)
start_session_request = xdp.Request(dbus_con, location_intf)
start_session_response = start_session_request.call(
"Start",
session_handle=session.handle,
parent_window="window-hndl",
options={},
)
assert start_session_response
assert start_session_response.response == 2
def test_session_update(self, portals, dbus_con, dbus_con_sys):
location_intf = xdp.get_portal_iface(dbus_con, "Location")
geoclue_mock_intf = self.get_geoclue_mock(dbus_con_sys)
location_updated = False
updated_count = 0
session = xdp.Session(
dbus_con,
location_intf.CreateSession({"session_handle_token": "session_token0"}),
)
def cb_location_updated(session_handle, location):
nonlocal location_updated
nonlocal updated_count
if updated_count == 0:
assert location["Latitude"] == 0
assert location["Longitude"] == 0
assert location["Accuracy"] == 0
elif updated_count == 1:
assert location["Latitude"] == 11
assert location["Longitude"] == 22
assert location["Accuracy"] == 3
updated_count += 1
location_updated = True
location_intf.connect_to_signal("LocationUpdated", cb_location_updated)
start_session_request = xdp.Request(dbus_con, location_intf)
start_session_response = start_session_request.call(
"Start",
session_handle=session.handle,
parent_window="window-hndl",
options={},
)
assert start_session_response
assert start_session_response.response == 0
xdp.wait_for(lambda: location_updated)
location_updated = False
assert updated_count == 1
geoclue_mock_intf.ChangeLocation(
{
"Latitude": dbus.UInt32(11),
"Longitude": dbus.UInt32(22),
"Accuracy": dbus.UInt32(3),
}
)
xdp.wait_for(lambda: location_updated)
location_updated = False
assert updated_count == 2
def test_bad_accuracy(self, portals, dbus_con):
location_intf = xdp.get_portal_iface(dbus_con, "Location")
with pytest.raises(dbus.exceptions.DBusException) as excinfo:
location_intf.CreateSession(
{
"session_handle_token": "session_token0",
"accuracy": dbus.UInt32(22),
}
)
assert (
excinfo.value.get_dbus_name()
== "org.freedesktop.portal.Error.InvalidArgument"
)

View File

@@ -0,0 +1,531 @@
# 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>',
),
(
"&lt;html&gt;",
"&lt;html&gt;",
),
(
'<a href="https://xkcd.com/327/#&quot;&gt;&lt;html&gt;"></a>',
'<a href="https://xkcd.com/327/#&quot;&gt;&lt;html&gt;"></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

View File

@@ -0,0 +1,352 @@
# SPDX-License-Identifier: LGPL-2.1-or-later
#
# This file is formatted with Python Black
import tests as xdp
import dbus
import pytest
import os
import tempfile
from pathlib import Path
from typing import Any
defaults_list = b"""[Default Applications]
x-scheme-handler/http=furrfix.desktop;
text/plain=furrfix.desktop
"""
furrfix_desktop = b"""[Desktop Entry]
Version=1.0
Name=Furrfix
GenericName=Not a Web Browser
Comment=Don't Browse the Web
Exec=true %u
Icon=furrfix
Terminal=false
Type=Application
MimeType=text/plain;text/html;text/xml;application/xhtml+xml;application/vnd.mozilla.xul+xml;text/mml;x-scheme-handler/http;x-scheme-handler/https;x-scheme-handler/xdg-desktop-portal-test;
StartupNotify=true
Categories=Network;WebBrowser;
Keywords=web;browser;internet;
"""
mimeinfo_cache = b"""[MIME Cache]
application/vnd.mozilla.xul+xml=furrfix.desktop;
application/xhtml+xml=furrfix.desktop;
text/plain=furrfix.desktop;
text/html=furrfix.desktop;
text/mml=furrfix.desktop;
text/xml=furrfix.desktop;
x-scheme-handler/http=furrfix.desktop;
x-scheme-handler/https=furrfix.desktop;
x-scheme-handler/xdg-desktop-portal-test=furrfix.desktop;
"""
@pytest.fixture
def xdg_data_home_files():
return {
"applications/defaults.list": defaults_list,
"applications/furrfix.desktop": furrfix_desktop,
"applications/mimeinfo.cache": mimeinfo_cache,
}
@pytest.fixture
def required_templates():
return {
"appchooser": {},
"lockdown": {},
}
class TestOpenURI:
def set_permissions(self, dbus_con, type, permissions):
perm_store_intf = xdp.get_permission_store_iface(dbus_con)
perm_store_intf.SetPermission(
"desktop-used-apps",
True,
"inhibit",
type,
permissions,
)
def enable_paranoid_mode(self, dbus_con, type):
# turn on paranoid mode to ensure we get a backend call
perm_store_intf = xdp.get_permission_store_iface(dbus_con)
perm_store_intf.SetValue(
"desktop-used-apps",
True,
type,
dbus.Dictionary(
{
"always-ask": True,
},
signature="sv",
),
)
def test_version(self, portals, dbus_con):
xdp.check_version(dbus_con, "OpenURI", 5)
def test_http1(self, portals, dbus_con, app_id):
openuri_intf = xdp.get_portal_iface(dbus_con, "OpenURI")
mock_intf = xdp.get_mock_iface(dbus_con)
scheme_handler = "x-scheme-handler/http"
self.enable_paranoid_mode(dbus_con, scheme_handler)
uri = "http://www.flatpak.org"
writable = False
activation_token = "token"
request = xdp.Request(dbus_con, openuri_intf)
options = {
"writable": writable,
"activation_token": activation_token,
}
response = request.call(
"OpenURI",
parent_window="",
uri=uri,
options=options,
)
assert response
assert response.response == 0
# Check the impl portal was called with the right args
method_calls = mock_intf.GetMethodCalls("ChooseApplication")
assert len(method_calls) > 0
_, args = method_calls[-1]
assert args[1] == app_id
assert args[2] == "" # parent window
assert "furrfix" in args[3]
assert args[4]["uri"] == uri
assert args[4]["content_type"] == scheme_handler
assert args[4]["activation_token"] == activation_token
def test_http2(self, portals, dbus_con):
openuri_intf = xdp.get_portal_iface(dbus_con, "OpenURI")
mock_intf = xdp.get_mock_iface(dbus_con)
scheme_handler = "x-scheme-handler/http"
self.set_permissions(dbus_con, scheme_handler, ["furrfix", "3", "3"])
uri = "http://www.flatpak.org"
writable = False
activation_token = "token"
request = xdp.Request(dbus_con, openuri_intf)
options = {
"writable": writable,
"activation_token": activation_token,
}
response = request.call(
"OpenURI",
parent_window="",
uri=uri,
options=options,
)
assert response
assert response.response == 0
# Check the impl portal was not called because the choice thresold
# has been reached
method_calls = mock_intf.GetMethodCalls("ChooseApplication")
assert len(method_calls) == 0
def test_file(self, portals, dbus_con, app_id):
openuri_intf = xdp.get_portal_iface(dbus_con, "OpenURI")
mock_intf = xdp.get_mock_iface(dbus_con)
scheme_handler = "text/plain"
self.enable_paranoid_mode(dbus_con, scheme_handler)
fd, _ = tempfile.mkstemp(prefix="openuri_mock_file_", dir=Path.home())
os.write(fd, b"openuri_mock_file")
writable = False
activation_token = "token"
request = xdp.Request(dbus_con, openuri_intf)
options = {
"writable": writable,
"activation_token": activation_token,
}
response = request.call(
"OpenFile",
parent_window="",
fd=fd,
options=options,
)
assert response
assert response.response == 0
# Check the impl portal was called with the right args
method_calls = mock_intf.GetMethodCalls("ChooseApplication")
assert len(method_calls) > 0
_, args = method_calls[-1]
assert args[1] == app_id
assert args[2] == "" # parent window
assert "furrfix" in args[3]
assert args[4]["content_type"] == scheme_handler
assert args[4]["activation_token"] == activation_token
path = args[4]["uri"]
assert path.startswith("file:///")
with open(path[7:]) as file:
openuri_file_contents = file.read()
assert openuri_file_contents == "openuri_mock_file"
@pytest.mark.parametrize("template_params", ({"appchooser": {"response": 1}},))
def test_cancel(self, portals, dbus_con):
openuri_intf = xdp.get_portal_iface(dbus_con, "OpenURI")
scheme_handler = "x-scheme-handler/http"
self.enable_paranoid_mode(dbus_con, scheme_handler)
uri = "http://www.flatpak.org"
request = xdp.Request(dbus_con, openuri_intf)
options: Any = {}
response = request.call(
"OpenURI",
parent_window="",
uri=uri,
options=options,
)
assert response
assert response.response == 1
@pytest.mark.parametrize(
"template_params", ({"appchooser": {"expect-close": True}},)
)
def test_close(self, portals, dbus_con, app_id):
openuri_intf = xdp.get_portal_iface(dbus_con, "OpenURI")
mock_intf = xdp.get_mock_iface(dbus_con)
scheme_handler = "x-scheme-handler/http"
self.enable_paranoid_mode(dbus_con, scheme_handler)
uri = "http://www.flatpak.org"
writable = False
activation_token = "token"
request = xdp.Request(dbus_con, openuri_intf)
request.schedule_close(1000)
options = {
"writable": writable,
"activation_token": activation_token,
}
request.call(
"OpenURI",
parent_window="",
uri=uri,
options=options,
)
# Only true if the impl.Request was closed too
assert request.closed
# Check the impl portal was called with the right args
method_calls = mock_intf.GetMethodCalls("ChooseApplication")
assert len(method_calls) > 0
_, args = method_calls[-1]
assert args[1] == app_id
assert args[2] == "" # parent window
assert "furrfix" in args[3]
assert args[4]["uri"] == uri
assert args[4]["content_type"] == scheme_handler
assert args[4]["activation_token"] == activation_token
@pytest.mark.parametrize(
"template_params", ({"lockdown": {"disable-application-handlers": True}},)
)
def test_lockdown(self, portals, dbus_con, app_id):
openuri_intf = xdp.get_portal_iface(dbus_con, "OpenURI")
scheme_handler = "x-scheme-handler/http"
self.enable_paranoid_mode(dbus_con, scheme_handler)
uri = "http://www.flatpak.org"
writable = False
activation_token = "token"
request = xdp.Request(dbus_con, openuri_intf)
options = {
"writable": writable,
"activation_token": activation_token,
}
with pytest.raises(dbus.exceptions.DBusException) as excinfo:
request.call(
"OpenURI",
parent_window="",
uri=uri,
options=options,
)
assert (
excinfo.value.get_dbus_name() == "org.freedesktop.portal.Error.NotAllowed"
)
def test_dir(self, portals, dbus_con, app_id):
openuri_intf = xdp.get_portal_iface(dbus_con, "OpenURI")
mock_intf = xdp.get_mock_iface(dbus_con)
scheme_handler = "inode/directory"
self.enable_paranoid_mode(dbus_con, scheme_handler)
fd, file_path = tempfile.mkstemp(prefix="openuri_mock_file_", dir=Path.home())
os.write(fd, b"openuri_mock_file")
activation_token = "token"
request = xdp.Request(dbus_con, openuri_intf)
options = {
"activation_token": activation_token,
}
response = request.call(
"OpenDirectory",
parent_window="",
fd=fd,
options=options,
)
assert response
assert response.response == 0
# Check the appchooser portal got called to open the containing dir
method_calls = mock_intf.GetMethodCalls("ChooseApplication")
assert len(method_calls) > 0
_, args = method_calls[-1]
assert args[1] == app_id
assert args[2] == "" # parent window
assert args[4]["content_type"] == scheme_handler
assert args[4]["activation_token"] == activation_token
path = args[4]["uri"]
assert path.startswith("file:///")
assert Path(path[7:]) == Path(file_path).parent
def test_scheme_supported(self, portals, dbus_con):
openuri_intf = xdp.get_portal_iface(dbus_con, "OpenURI")
supported = openuri_intf.SchemeSupported("https", {})
assert supported
supported = openuri_intf.SchemeSupported("bogusnonexistanthandler", {})
assert not supported
with pytest.raises(dbus.exceptions.DBusException) as excinfo:
openuri_intf.SchemeSupported("", {})
assert (
excinfo.value.get_dbus_name()
== "org.freedesktop.portal.Error.InvalidArgument"
)

View File

@@ -0,0 +1,298 @@
# SPDX-License-Identifier: LGPL-2.1-or-later
#
# This file is formatted with Python Black
import tests as xdp
import dbus
from gi.repository import GLib, Gio
class PermissionStore(xdp.GDBusIface):
def __init__(self):
super().__init__(
"org.freedesktop.impl.portal.PermissionStore",
"/org/freedesktop/impl/portal/PermissionStore",
"org.freedesktop.impl.portal.PermissionStore",
)
def Lookup(self, table, id):
return self._call(
"Lookup",
GLib.Variant("(ss)", (table, id)),
)
def Set(self, table, create, id, perm, data):
return self._call(
"Set",
GLib.Variant("(sbsa{sas}v)", (table, create, id, perm, data)),
)
def SetValue(self, table, create, id, data):
return self._call(
"SetValue",
GLib.Variant("(sbsv)", (table, create, id, data)),
)
def SetPermission(self, table, create, id, app, perm):
return self._call(
"SetPermission",
GLib.Variant("(sbssas)", (table, create, id, app, perm)),
)
def SetPermissionAsync(self, table, create, id, app, perm, user_cb):
self._call_async(
"SetPermission",
GLib.Variant("(sbssas)", (table, create, id, app, perm)),
cb=user_cb,
)
def DeletePermissionAsync(self, table, id, app, user_cb):
self._call_async(
"DeletePermission",
GLib.Variant("(sss)", (table, id, app)),
cb=user_cb,
)
def DeleteAsync(self, table, id, user_cb):
self._call_async(
"Delete",
GLib.Variant("(ss)", (table, id)),
cb=user_cb,
)
def Delete(self, table, id):
return self._call(
"Delete",
GLib.Variant("(ss)", (table, id)),
)
def GetPermission(self, table, id, app):
return self._call(
"GetPermission",
GLib.Variant("(sss)", (table, id, app)),
)
class TestPermissionStore:
def test_version(self, portals, dbus_con):
permission_store = dbus_con.get_object(
"org.freedesktop.impl.portal.PermissionStore",
"/org/freedesktop/impl/portal/PermissionStore",
)
properties_intf = dbus.Interface(
permission_store,
"org.freedesktop.DBus.Properties",
)
portal_version = properties_intf.Get(
"org.freedesktop.impl.portal.PermissionStore",
"version",
)
assert int(portal_version) == 2
def test_delete_race(self, portals, dbus_con):
permission_store_intf = PermissionStore()
finished_count = 0
table = "inhibit"
id = "inhibit"
perms = ["logout", "suspend"]
def cb(_):
nonlocal finished_count
finished_count += 1
permission_store_intf.SetPermissionAsync(table, True, id, "a", perms, cb)
permission_store_intf.DeleteAsync(table, id, cb)
xdp.wait_for(lambda: finished_count >= 2)
try:
permission_store_intf.Lookup(table, id)
assert False, "This statement should not be reached"
except GLib.GError as e:
assert "org.freedesktop.portal.Error.NotFound" in e.message
assert e.matches(Gio.io_error_quark(), Gio.IOErrorEnum.DBUS_ERROR)
permission_store_intf.SetPermissionAsync(table, True, id, "a", perms, cb)
permission_store_intf.SetPermissionAsync(table, True, id, "b", perms, cb)
permission_store_intf.DeletePermissionAsync(table, id, "a", cb)
xdp.wait_for(lambda: finished_count >= 4)
result, _ = permission_store_intf.Lookup(table, id)
perms_out = result.unpack()[0]
assert perms_out == {"b": perms}
permission_store_intf.SetPermissionAsync(table, True, id, "a", perms, cb)
permission_store_intf.DeletePermissionAsync(table, id, "b", cb)
permission_store_intf.DeletePermissionAsync(table, id, "a", cb)
xdp.wait_for(lambda: finished_count >= 7)
result, _ = permission_store_intf.Lookup(table, id)
perms_out = result.unpack()[0]
assert perms_out == {}
def test_change(self, portals, dbus_con):
permission_store_intf = PermissionStore()
changed_count = 0
table = "TEST"
id = "test-resource"
app = "one.two.three"
perms = ["one", "two"]
def cb_changed1(results):
nonlocal changed_count
cb_table, cb_id, deleted, _, cb_perms = results.unpack()
assert cb_table == table
assert cb_id == id
assert not deleted
assert cb_perms[app] == perms
changed_count += 1
cs = permission_store_intf.connect_to_signal("Changed", cb_changed1)
permission_store_intf.SetPermissionAsync(table, True, id, app, perms, None)
xdp.wait_for(lambda: changed_count >= 1)
cs.disconnect()
def cb_changed2(results):
nonlocal changed_count
cb_table, cb_id, deleted, _, _ = results.unpack()
assert cb_table == table
assert cb_id == id
assert deleted
changed_count += 1
cs = permission_store_intf.connect_to_signal("Changed", cb_changed2)
permission_store_intf.Delete(table, id)
xdp.wait_for(lambda: changed_count >= 2)
cs.disconnect()
def test_lookup(self, portals, dbus_con):
permission_store_intf = PermissionStore()
table = "TEST"
id = "test-resource"
perms = ["one", "two"]
data = True
try:
permission_store_intf.Lookup(table, id)
assert False, "This statement should not be reached"
except GLib.GError as e:
assert "org.freedesktop.portal.Error.NotFound" in e.message
permissions = [(id, perms)]
permission_store_intf.Set(table, True, id, permissions, GLib.Variant("b", data))
result, _ = permission_store_intf.Lookup(table, id)
perms_out = result.unpack()[0]
data_out = result.unpack()[1]
assert id in perms_out
perms_out = perms_out[id]
assert perms_out == perms
assert data_out == data
def test_set_value(self, portals, dbus_con):
permission_store_intf = PermissionStore()
table = "TEST"
id = "test-resource"
data = True
try:
permission_store_intf.Lookup(table, id)
assert False, "This statement should not be reached"
except GLib.GError as e:
assert "org.freedesktop.portal.Error.NotFound" in e.message
permission_store_intf.SetValue(table, True, id, GLib.Variant("b", data))
result, _ = permission_store_intf.Lookup(table, id)
perms_out = result.unpack()[0]
data_out = result.unpack()[1]
assert perms_out == {}
assert data_out == data
def test_create(self, portals, dbus_con):
permission_store_intf = PermissionStore()
table = "inhibit"
id = "inhibit"
app = ""
perms = ["logout", "suspend"]
try:
permission_store_intf.SetPermission(
table,
# Do not create if it does not exist
False,
id,
app,
perms,
)
assert False, "This statement should not be reached"
except GLib.GError as e:
assert "org.freedesktop.portal.Error.NotFound" in e.message
permission_store_intf.SetPermission(table, True, id, app, perms)
def test_delete(self, portals, dbus_con):
permission_store_intf = PermissionStore()
table = "inhibit"
id = "inhibit"
app = ""
perms = ["logout", "suspend"]
try:
permission_store_intf.Delete(table, id)
assert False, "This statement should not be reached"
except GLib.GError as e:
assert "org.freedesktop.portal.Error.NotFound" in e.message
permission_store_intf.SetPermission(table, True, id, app, perms)
permission_store_intf.Delete(table, id)
try:
permission_store_intf.Lookup(table, id)
assert False, "This statement should not be reached"
except GLib.GError as e:
assert "org.freedesktop.portal.Error.NotFound" in e.message
def test_get_permission(self, portals, dbus_con):
permission_store_intf = PermissionStore()
table = "notifications"
id = "notification"
app = "a"
perms = ["yes"]
try:
permission_store_intf.GetPermission(table, id, app)
assert False, "This statement should not be reached"
except GLib.GError as e:
assert "org.freedesktop.portal.Error.NotFound" in e.message
permission_store_intf.SetPermission(table, True, id, app, perms)
result, _ = permission_store_intf.GetPermission(table, id, app)
permissions = result.unpack()[0]
assert permissions == perms
result, _ = permission_store_intf.GetPermission(table, id, "no-such-app")
permissions = result.unpack()[0]
assert permissions == []

View File

@@ -0,0 +1,345 @@
# SPDX-License-Identifier: LGPL-2.1-or-later
#
# This file is formatted with Python Black
import tests as xdp
import dbus
import pytest
import os
import tempfile
from pathlib import Path
from typing import Any
PRINT_PREPARE_DATA = {
"token": dbus.UInt32(1337),
}
@pytest.fixture
def required_templates():
return {
"print": {
"prepare-results": PRINT_PREPARE_DATA,
},
"lockdown": {},
}
class TestPrint:
def test_version(self, portals, dbus_con):
xdp.check_version(dbus_con, "Print", 3)
def test_prepare_print_basic(self, portals, dbus_con, app_id):
print_intf = xdp.get_portal_iface(dbus_con, "Print")
mock_intf = xdp.get_mock_iface(dbus_con)
title = "Test Title"
settings: Any = {}
page_setup: Any = {}
options = {
"modal": True,
"accept_label": "Accept",
"supported_output_file_formats": ["pdf"],
}
request = xdp.Request(dbus_con, print_intf)
response = request.call(
"PreparePrint",
parent_window="",
title=title,
settings=settings,
page_setup=page_setup,
options=options,
)
assert response
assert response.response == 0
# Check the impl portal was called with the right args
method_calls = mock_intf.GetMethodCalls("PreparePrint")
assert len(method_calls) == 1
_, args = method_calls.pop()
assert args[1] == app_id
assert args[2] == "" # parent window
assert args[3] == title
assert args[4] == settings
assert args[5] == page_setup
assert args[6]["modal"] == options["modal"]
assert args[6]["accept_label"] == options["accept_label"]
assert (
args[6]["supported_output_file_formats"]
== options["supported_output_file_formats"]
)
@pytest.mark.parametrize("template_params", ({"print": {"response": 1}},))
def test_prepare_print_cancel(self, portals, dbus_con, app_id):
print_intf = xdp.get_portal_iface(dbus_con, "Print")
mock_intf = xdp.get_mock_iface(dbus_con)
title = "Test Title"
request = xdp.Request(dbus_con, print_intf)
response = request.call(
"PreparePrint",
parent_window="",
title=title,
settings={},
page_setup={},
options={},
)
assert response
assert response.response == 1
# Check the impl portal was called with the right args
method_calls = mock_intf.GetMethodCalls("PreparePrint")
assert len(method_calls) == 1
_, args = method_calls.pop()
assert args[1] == app_id
assert args[2] == "" # parent window
assert args[3] == title
@pytest.mark.parametrize("template_params", ({"print": {"expect-close": True}},))
def test_prepare_print_close(self, portals, dbus_con, app_id):
print_intf = xdp.get_portal_iface(dbus_con, "Print")
mock_intf = xdp.get_mock_iface(dbus_con)
title = "Test Title"
request = xdp.Request(dbus_con, print_intf)
request.schedule_close(1000)
request.call(
"PreparePrint",
parent_window="",
title=title,
settings={},
page_setup={},
options={},
)
# Only true if the impl.Request was closed too
assert request.closed
# Check the impl portal was called with the right args
method_calls = mock_intf.GetMethodCalls("PreparePrint")
assert len(method_calls) == 1
_, args = method_calls.pop()
assert args[1] == app_id
assert args[2] == "" # parent window
assert args[3] == title
@pytest.mark.parametrize(
"template_params", ({"lockdown": {"disable-printing": True}},)
)
def test_prepare_print_lockdown(self, portals, dbus_con, app_id):
print_intf = xdp.get_portal_iface(dbus_con, "Print")
mock_intf = xdp.get_mock_iface(dbus_con)
title = "Test Title"
request = xdp.Request(dbus_con, print_intf)
with pytest.raises(dbus.exceptions.DBusException) as excinfo:
request.call(
"PreparePrint",
parent_window="",
title=title,
settings={},
page_setup={},
options={},
)
assert (
excinfo.value.get_dbus_name() == "org.freedesktop.portal.Error.NotAllowed"
)
# Check the impl portal was not called
method_calls = mock_intf.GetMethodCalls("PreparePrint")
assert len(method_calls) == 0
def test_print_basic(self, portals, dbus_con, app_id):
print_intf = xdp.get_portal_iface(dbus_con, "Print")
mock_intf = xdp.get_mock_iface(dbus_con)
fd, file_path = tempfile.mkstemp(prefix="print_mock_file_", dir=Path.home())
os.write(fd, b"print_mock_file")
title = "Test Title"
options = {
"modal": True,
"token": "token",
"supported_output_file_formats": ["svg"],
}
request = xdp.Request(dbus_con, print_intf)
response = request.call(
"Print",
parent_window="",
title=title,
fd=fd,
options=options,
)
assert response
assert response.response == 0
# Check the impl portal was called with the right args
method_calls = mock_intf.GetMethodCalls("Print")
assert len(method_calls) == 1
_, args = method_calls.pop()
assert args[1] == app_id
assert args[2] == "" # parent window
assert args[3] == title
assert args[5]["modal"] == options["modal"]
assert (
args[5]["supported_output_file_formats"]
== options["supported_output_file_formats"]
)
backend_fd = args[4].take()
ino = os.stat(f"/proc/self/fd/{fd}").st_ino
ino_backend = os.stat(f"/proc/self/fd/{backend_fd}").st_ino
os.close(fd)
assert ino == ino_backend
@pytest.mark.parametrize("template_params", ({"print": {"response": 1}},))
def test_print_cancel(self, portals, dbus_con, app_id):
print_intf = xdp.get_portal_iface(dbus_con, "Print")
mock_intf = xdp.get_mock_iface(dbus_con)
fd, _ = tempfile.mkstemp(prefix="print_mock_file_", dir=Path.home())
os.write(fd, b"print_mock_file")
title = "Test Title"
request = xdp.Request(dbus_con, print_intf)
response = request.call(
"Print",
parent_window="",
title=title,
fd=fd,
options={},
)
assert response
assert response.response == 1
# Check the impl portal was called with the right args
method_calls = mock_intf.GetMethodCalls("Print")
assert len(method_calls) == 1
_, args = method_calls.pop()
assert args[1] == app_id
@pytest.mark.parametrize("template_params", ({"print": {"expect-close": True}},))
def test_print_close(self, portals, dbus_con, app_id):
print_intf = xdp.get_portal_iface(dbus_con, "Print")
mock_intf = xdp.get_mock_iface(dbus_con)
fd, _ = tempfile.mkstemp(prefix="print_mock_file_", dir=Path.home())
os.write(fd, b"print_mock_file")
title = "Test Title"
request = xdp.Request(dbus_con, print_intf)
request.schedule_close(1000)
request.call(
"Print",
parent_window="",
title=title,
fd=fd,
options={},
)
# Only true if the impl.Request was closed too
assert request.closed
# Check the impl portal was called with the right args
method_calls = mock_intf.GetMethodCalls("Print")
assert len(method_calls) == 1
_, args = method_calls.pop()
assert args[1] == app_id
@pytest.mark.parametrize(
"template_params", ({"lockdown": {"disable-printing": True}},)
)
def test_print_lockdown(self, portals, dbus_con, app_id):
print_intf = xdp.get_portal_iface(dbus_con, "Print")
mock_intf = xdp.get_mock_iface(dbus_con)
fd, _ = tempfile.mkstemp(prefix="print_mock_file_", dir=Path.home())
os.write(fd, b"print_mock_file")
title = "Test Title"
request = xdp.Request(dbus_con, print_intf)
with pytest.raises(dbus.exceptions.DBusException) as excinfo:
request.call(
"Print",
parent_window="",
title=title,
fd=fd,
options={},
)
assert (
excinfo.value.get_dbus_name() == "org.freedesktop.portal.Error.NotAllowed"
)
# Check the impl portal was not called
method_calls = mock_intf.GetMethodCalls("Print")
assert len(method_calls) == 0
def test_print_prepare_and_print(self, portals, dbus_con, app_id):
print_intf = xdp.get_portal_iface(dbus_con, "Print")
mock_intf = xdp.get_mock_iface(dbus_con)
title = "Test Title"
fd, file_path = tempfile.mkstemp(prefix="print_mock_file_", dir=Path.home())
os.write(fd, b"print_mock_file")
request = xdp.Request(dbus_con, print_intf)
response = request.call(
"PreparePrint",
parent_window="",
title=title,
settings={},
page_setup={},
options={},
)
assert response
assert response.response == 0
assert response.results["token"] == PRINT_PREPARE_DATA["token"]
# Check the impl portal was called with the right args
method_calls = mock_intf.GetMethodCalls("PreparePrint")
assert len(method_calls) == 1
options = {
"token": response.results["token"],
}
request = xdp.Request(dbus_con, print_intf)
response = request.call(
"Print",
parent_window="",
title=title,
fd=fd,
options=options,
)
assert response
assert response.response == 0
# Check the impl portal was called with the right args
method_calls = mock_intf.GetMethodCalls("Print")
assert len(method_calls) == 1
_, args = method_calls.pop()
assert args[5]["token"] == PRINT_PREPARE_DATA["token"]
backend_fd = args[4].take()
ino = os.stat(f"/proc/self/fd/{fd}").st_ino
ino_backend = os.stat(f"/proc/self/fd/{backend_fd}").st_ino
os.close(fd)
assert ino == ino_backend

View File

@@ -0,0 +1,142 @@
# SPDX-License-Identifier: LGPL-2.1-or-later
import tests as xdp
import dbus
import pytest
@pytest.fixture
def app_id():
return "org.example.WrongAppId"
@pytest.fixture
def required_templates():
return {"remotedesktop": {}}
class TestRegistry:
def test_version(self, portals, dbus_con):
documents = dbus_con.get_object(
"org.freedesktop.portal.Desktop",
"/org/freedesktop/portal/desktop",
)
properties_intf = dbus.Interface(
documents,
"org.freedesktop.DBus.Properties",
)
portal_version = properties_intf.Get(
"org.freedesktop.host.portal.Registry",
"version",
)
assert int(portal_version) == 1
def create_dummy_session(self, dbus_con):
remotedesktop_intf = xdp.get_portal_iface(dbus_con, "RemoteDesktop")
request = xdp.Request(dbus_con, remotedesktop_intf)
session_counter_attr_name = "session_counter"
session_counter = getattr(self, session_counter_attr_name, 0)
setattr(self, session_counter_attr_name, session_counter + 1)
options = {
"session_handle_token": f"session_token{session_counter}",
}
response = request.call(
"CreateSession",
options=options,
)
assert response
assert response.response == 0
return xdp.Session.from_response(dbus_con, response)
def test_registerless(self, portals, dbus_con, app_id):
mock_intf = xdp.get_mock_iface(dbus_con)
expected_app_id = app_id
session = self.create_dummy_session(dbus_con)
app_id = mock_intf.GetSessionAppId(session.handle)
assert app_id == expected_app_id
def test_register(self, portals, dbus_con):
registry_intf = xdp.get_portal_iface(dbus_con, "Registry", domain="host")
mock_intf = xdp.get_mock_iface(dbus_con)
expected_app_id = "org.example.CorrectAppId"
registry_intf.Register(expected_app_id, {})
session = self.create_dummy_session(dbus_con)
app_id = mock_intf.GetSessionAppId(session.handle)
assert app_id == expected_app_id
def test_late_register(self, portals, dbus_con, app_id):
registry_intf = xdp.get_portal_iface(dbus_con, "Registry", domain="host")
mock_intf = xdp.get_mock_iface(dbus_con)
expected_app_id = app_id
unexpected_app_id = "org.example.CorrectAppId"
session = self.create_dummy_session(dbus_con)
app_id = mock_intf.GetSessionAppId(session.handle)
assert app_id == expected_app_id
with pytest.raises(dbus.exceptions.DBusException) as exc_info:
registry_intf.Register(unexpected_app_id, {})
exc_info.match(".*Connection already associated with an application ID.*")
new_session = self.create_dummy_session(dbus_con)
new_app_id = mock_intf.GetSessionAppId(new_session.handle)
assert new_app_id == expected_app_id
def test_multiple_connections(self, portals, dbus_con, app_id):
registry_intf = xdp.get_portal_iface(dbus_con, "Registry", domain="host")
mock_intf = xdp.get_mock_iface(dbus_con)
expected_app_id = "org.example.CorrectAppId"
unexpected_app_id = app_id
registry_intf.Register(expected_app_id, {})
session = self.create_dummy_session(dbus_con)
app_id = mock_intf.GetSessionAppId(session.handle)
assert app_id == expected_app_id
dbus_con2 = dbus.bus.BusConnection(dbus.bus.BusConnection.TYPE_SESSION)
dbus_con2.set_exit_on_disconnect(False)
mock_intf2 = xdp.get_mock_iface(dbus_con2)
session2 = self.create_dummy_session(dbus_con2)
app_id2 = mock_intf2.GetSessionAppId(session2.handle)
assert app_id2 == unexpected_app_id
dbus_con2.close()
dbus_con3 = dbus.bus.BusConnection(dbus.bus.BusConnection.TYPE_SESSION)
dbus_con3.set_exit_on_disconnect(False)
mock_intf3 = xdp.get_mock_iface(dbus_con3)
registry_intf3 = xdp.get_portal_iface(dbus_con3, "Registry", domain="host")
registry_intf3.Register(expected_app_id, {})
session3 = self.create_dummy_session(dbus_con3)
app_id3 = mock_intf3.GetSessionAppId(session3.handle)
assert app_id3 == expected_app_id
dbus_con3.close()
def test_no_reregister(self, portals, dbus_con):
registry_intf = xdp.get_portal_iface(dbus_con, "Registry", domain="host")
mock_intf = xdp.get_mock_iface(dbus_con)
expected_app_id = "org.example.CorrectAppId"
registry_intf.Register(expected_app_id, {})
session = self.create_dummy_session(dbus_con)
app_id = mock_intf.GetSessionAppId(session.handle)
assert app_id == expected_app_id
with pytest.raises(dbus.exceptions.DBusException) as exc_info:
registry_intf.Register(expected_app_id, {})
exc_info.match(".*Connection already associated with an application ID.*")

View File

@@ -0,0 +1,251 @@
# SPDX-License-Identifier: LGPL-2.1-or-later
#
# This file is formatted with Python Black
import tests as xdp
import dbus
import pytest
import socket
from typing import List, Dict, Any
@pytest.fixture
def required_templates():
return {"remotedesktop": {}}
class TestRemoteDesktop:
def test_version(self, portals, dbus_con):
xdp.check_version(dbus_con, "RemoteDesktop", 2)
def test_create_close_session(self, portals, dbus_con):
remotedesktop_intf = xdp.get_portal_iface(dbus_con, "RemoteDesktop")
mock_intf = xdp.get_mock_iface(dbus_con)
request = xdp.Request(dbus_con, remotedesktop_intf)
options = {
"session_handle_token": "session_token0",
}
response = request.call(
"CreateSession",
options=options,
)
assert response
assert response.response == 0
session = xdp.Session.from_response(dbus_con, response)
# Check the impl portal was called with the right args
method_calls = mock_intf.GetMethodCalls("CreateSession")
assert len(method_calls) > 0
_, args = method_calls[-1]
assert args[1] == session.handle
# assert args[2] == "" # appid, not necessary empty
session.close()
xdp.wait_for(lambda: session.closed)
@pytest.mark.parametrize("token", ("Invalid-Token&", "", "/foo"))
def test_remote_desktop_create_session_invalid(self, portals, dbus_con, token):
remotedesktop_intf = xdp.get_portal_iface(dbus_con, "RemoteDesktop")
request = xdp.Request(dbus_con, remotedesktop_intf)
options = {"session_handle_token": token}
with pytest.raises(dbus.exceptions.DBusException) as excinfo:
request.call("CreateSession", options=options)
e = excinfo.value
assert e.get_dbus_name() == "org.freedesktop.portal.Error.InvalidArgument"
assert "Invalid token" in e.get_dbus_message()
@pytest.mark.parametrize(
"template_params", ({"remotedesktop": {"force-close": 500}},)
)
def test_create_session_signal_closed(self, portals, dbus_con):
remotedesktop_intf = xdp.get_portal_iface(dbus_con, "RemoteDesktop")
mock_intf = xdp.get_mock_iface(dbus_con)
request = xdp.Request(dbus_con, remotedesktop_intf)
options = {
"session_handle_token": "session_token0",
}
response = request.call(
"CreateSession",
options=options,
)
assert response
assert response.response == 0
session = xdp.Session.from_response(dbus_con, response)
# Check the impl portal was called with the right args
method_calls = mock_intf.GetMethodCalls("CreateSession")
assert len(method_calls) > 0
_, args = method_calls[-1]
assert args[1] == session.handle
# assert args[2] == "" # appid, not necessary empty
# Now expect the backend to close it
xdp.wait_for(lambda: session.closed)
def test_connect_to_eis(self, portals, dbus_con):
remotedesktop_intf = xdp.get_portal_iface(dbus_con, "RemoteDesktop")
request = xdp.Request(dbus_con, remotedesktop_intf)
options = {
"session_handle_token": "session_token0",
}
response = request.call(
"CreateSession",
options=options,
)
assert response
assert response.response == 0
session = xdp.Session.from_response(dbus_con, response)
request = xdp.Request(dbus_con, remotedesktop_intf)
options = {
"types": dbus.UInt32(0x3),
}
response = request.call(
"SelectDevices",
session_handle=session.handle,
options=options,
)
assert response
assert response.response == 0
request = xdp.Request(dbus_con, remotedesktop_intf)
options = {}
response = request.call(
"Start",
session_handle=session.handle,
parent_window="",
options=options,
)
assert response
assert response.response == 0
fd = remotedesktop_intf.ConnectToEIS(
session.handle,
dbus.Dictionary({}, signature="sv"),
)
eis_socket = socket.fromfd(fd.take(), socket.AF_UNIX, socket.SOCK_STREAM)
assert eis_socket.recv(10) == b"HELLO"
@pytest.mark.parametrize(
"template_params", ({"remotedesktop": {"fail-connect-to-eis": True}},)
)
def test_connect_to_eis_fail(self, portals, dbus_con):
remotedesktop_intf = xdp.get_portal_iface(dbus_con, "RemoteDesktop")
request = xdp.Request(dbus_con, remotedesktop_intf)
options = {
"session_handle_token": "session_token0",
}
response = request.call(
"CreateSession",
options=options,
)
assert response
assert response.response == 0
session = xdp.Session.from_response(dbus_con, response)
request = xdp.Request(dbus_con, remotedesktop_intf)
options = {
"types": dbus.UInt32(0x3),
}
response = request.call(
"SelectDevices",
session_handle=session.handle,
options=options,
)
assert response
assert response.response == 0
request = xdp.Request(dbus_con, remotedesktop_intf)
options = {}
response = request.call(
"Start",
session_handle=session.handle,
parent_window="",
options=options,
)
assert response
assert response.response == 0
with pytest.raises(dbus.exceptions.DBusException) as excinfo:
_ = remotedesktop_intf.ConnectToEIS(
session.handle, dbus.Dictionary({}, signature="sv")
)
assert "Purposely failing ConnectToEIS" in excinfo.value.get_dbus_message()
def test_connect_to_eis_fail_notifies(self, portals, dbus_con):
remotedesktop_intf = xdp.get_portal_iface(dbus_con, "RemoteDesktop")
request = xdp.Request(dbus_con, remotedesktop_intf)
options = {
"session_handle_token": "session_token0",
}
response = request.call(
"CreateSession",
options=options,
)
assert response
assert response.response == 0
session = xdp.Session.from_response(dbus_con, response)
request = xdp.Request(dbus_con, remotedesktop_intf)
options = {
"types": dbus.UInt32(0x3),
}
response = request.call(
"SelectDevices",
session_handle=session.handle,
options=options,
)
assert response
assert response.response == 0
request = xdp.Request(dbus_con, remotedesktop_intf)
options = {}
response = request.call(
"Start",
session_handle=session.handle,
parent_window="",
options=options,
)
assert response
assert response.response == 0
notifyfuncs: List[Dict[str, Any]] = [
{"name": "NotifyPointerMotion", "args": (1, 2)},
{"name": "NotifyPointerMotionAbsolute", "args": (0, 1, 2)},
{"name": "NotifyPointerButton", "args": (1, 1)},
{"name": "NotifyPointerAxis", "args": (1, 1)},
{"name": "NotifyPointerAxisDiscrete", "args": (1, 1)},
{"name": "NotifyKeyboardKeycode", "args": (1, 1)},
{"name": "NotifyKeyboardKeysym", "args": (1, 1)},
{"name": "NotifyTouchDown", "args": (0, 0, 1, 1)},
{"name": "NotifyTouchMotion", "args": (0, 0, 1, 1)},
{"name": "NotifyTouchUp", "args": (0,)},
]
for notifyfunc in notifyfuncs:
with pytest.raises(dbus.exceptions.DBusException) as excinfo:
func = getattr(remotedesktop_intf, notifyfunc["name"])
assert func is not None
func(
session.handle,
dbus.Dictionary({}, signature="sv"),
*notifyfunc["args"],
)
# Not the best error message but...
assert (
"Session is not allowed to call Notify"
in excinfo.value.get_dbus_message()
)

View File

@@ -0,0 +1,187 @@
# SPDX-License-Identifier: LGPL-2.1-or-later
#
# This file is formatted with Python Black
import tests as xdp
import dbus
import pytest
from typing import Any
SCREENSHOT_DATA = dbus.Dictionary(
{
"uri": "file:///screenshot.png",
"color": (0.0, 1.0, 0.331),
},
signature="sv",
)
@pytest.fixture
def required_templates():
return {
"access": {},
"screenshot": {
"results": SCREENSHOT_DATA,
},
}
class TestScreenshot:
def test_version(self, portals, dbus_con):
xdp.check_version(dbus_con, "Screenshot", 2)
@pytest.mark.parametrize("modal", [True, False])
@pytest.mark.parametrize("interactive", [True, False])
def test_screenshot_basic(self, portals, dbus_con, app_id, modal, interactive):
screenshot_intf = xdp.get_portal_iface(dbus_con, "Screenshot")
mock_intf = xdp.get_mock_iface(dbus_con)
request = xdp.Request(dbus_con, screenshot_intf)
options = {
"modal": modal,
"interactive": interactive,
}
response = request.call(
"Screenshot",
parent_window="",
options=options,
)
assert response
assert response.response == 0
assert response.results["uri"] == SCREENSHOT_DATA["uri"]
# Check the impl portal was called with the right args
method_calls = mock_intf.GetMethodCalls("Screenshot")
assert len(method_calls) > 0
_, args = method_calls[-1]
assert args[1] == app_id
assert args[2] == "" # parent window
assert args[3]["modal"] == modal
assert args[3]["interactive"] == interactive
# check that args were forwarded to access portal correctly
if not interactive:
method_calls = mock_intf.GetMethodCalls("AccessDialog")
assert len(method_calls) > 0
_, args = method_calls[-1]
assert args[1] == app_id
assert args[2] == "" # parent window
assert args[6]["modal"] == modal
@pytest.mark.parametrize(
"template_params", ({"screenshot": {"expect-close": True}},)
)
def test_screenshot_close(self, portals, dbus_con):
screenshot_intf = xdp.get_portal_iface(dbus_con, "Screenshot")
request = xdp.Request(dbus_con, screenshot_intf)
request.schedule_close(1000)
options = {
"interactive": True,
}
request.call(
"Screenshot",
parent_window="",
options=options,
)
# Only true if the impl.Request was closed too
assert request.closed
@pytest.mark.parametrize("template_params", ({"screenshot": {"response": 1}},))
def test_screenshot_cancel(self, portals, dbus_con, app_id):
screenshot_intf = xdp.get_portal_iface(dbus_con, "Screenshot")
mock_intf = xdp.get_mock_iface(dbus_con)
modal = True
interactive = True
request = xdp.Request(dbus_con, screenshot_intf)
options = {
"modal": modal,
"interactive": interactive,
}
response = request.call(
"Screenshot",
parent_window="",
options=options,
)
assert response
assert response.response == 1
# Check the impl portal was called with the right args
method_calls = mock_intf.GetMethodCalls("Screenshot")
assert len(method_calls) > 0
_, args = method_calls[-1]
assert args[1] == app_id
assert args[2] == "" # parent window
assert args[3]["modal"] == modal
assert args[3]["interactive"] == interactive
def test_pick_color_basic(self, portals, dbus_con, app_id):
screenshot_intf = xdp.get_portal_iface(dbus_con, "Screenshot")
mock_intf = xdp.get_mock_iface(dbus_con)
request = xdp.Request(dbus_con, screenshot_intf)
options: Any = {}
response = request.call(
"PickColor",
parent_window="",
options=options,
)
assert response
assert response.response == 0
assert response.results["color"] == SCREENSHOT_DATA["color"]
# Check the impl portal was called with the right args
method_calls = mock_intf.GetMethodCalls("PickColor")
assert len(method_calls) > 0
_, args = method_calls[-1]
assert args[1] == app_id
assert args[2] == "" # parent window
@pytest.mark.parametrize(
"template_params", ({"screenshot": {"expect-close": True}},)
)
def test_pick_color_close(self, portals, dbus_con, app_id):
screenshot_intf = xdp.get_portal_iface(dbus_con, "Screenshot")
request = xdp.Request(dbus_con, screenshot_intf)
request.schedule_close(1000)
options: Any = {}
request.call(
"PickColor",
parent_window="",
options=options,
)
# Only true if the impl.Request was closed too
assert request.closed
@pytest.mark.parametrize("template_params", ({"screenshot": {"response": 1}},))
def test_pick_color_cancel(self, portals, dbus_con, app_id):
screenshot_intf = xdp.get_portal_iface(dbus_con, "Screenshot")
mock_intf = xdp.get_mock_iface(dbus_con)
request = xdp.Request(dbus_con, screenshot_intf)
options: Any = {}
response = request.call(
"PickColor",
parent_window="",
options=options,
)
assert response
assert response.response == 1
# Check the impl portal was called with the right args
method_calls = mock_intf.GetMethodCalls("PickColor")
assert len(method_calls) > 0
_, args = method_calls[-1]
assert args[1] == app_id
assert args[2] == "" # parent window

View File

@@ -0,0 +1,338 @@
# SPDX-License-Identifier: LGPL-2.1-or-later
#
# This file is formatted with Python Black
import tests as xdp
import dbus
import pytest
SETTINGS_DATA_TEST1 = {
"org.freedesktop.appearance": dbus.Dictionary(
{
"color-scheme": dbus.UInt32(1),
"accent-color": dbus.Struct((0.0, 0.1, 0.33), signature="ddd"),
},
signature="sv",
),
}
SETTINGS_DATA_TEST2 = {
"org.freedesktop.appearance": dbus.Dictionary(
{
"color-scheme": dbus.UInt32(2),
"contrast": dbus.UInt32(0),
},
signature="sv",
),
"org.example.custom": dbus.Dictionary(
{
"foo": "bar",
},
signature="sv",
),
}
SETTINGS_DATA_BAD = {
"org.freedesktop.appearance": dbus.Dictionary(
{
"color-scheme": dbus.UInt32(99),
"accent-color": dbus.Struct((11.11, 22.22, 33.33), signature="ddd"),
},
signature="sv",
),
"org.example.custom": dbus.Dictionary(
{
"foo": "baz",
},
signature="sv",
),
"org.example.custom.bad": dbus.Dictionary(
{
"bad": "bad",
},
signature="sv",
),
}
# This is the expected data, merged SETTINGS_DATA_TEST1 and SETTINGS_DATA_TEST2
SETTINGS_DATA = {
"org.freedesktop.appearance": dbus.Dictionary(
{
"color-scheme": dbus.UInt32(1),
"accent-color": dbus.Struct((0.0, 0.1, 0.33), signature="ddd"),
"contrast": dbus.UInt32(0),
},
signature="sv",
),
"org.example.custom": dbus.Dictionary(
{
"foo": "bar",
},
signature="sv",
),
}
@pytest.fixture
def required_templates():
return {
"settings:org.freedesktop.impl.portal.Test1": {
"settings": SETTINGS_DATA_TEST1,
},
"settings:org.freedesktop.impl.portal.Test2": {
"settings": SETTINGS_DATA_TEST2,
},
"settings:org.freedesktop.impl.portal.TestBad": {
"settings": SETTINGS_DATA_BAD,
},
}
PORTAL_CONFIG_FILES = {
"test1.portal": b"""
[portal]
DBusName=org.freedesktop.impl.portal.Test1
Interfaces=org.freedesktop.impl.portal.Settings;
""",
"test2.portal": b"""
[portal]
DBusName=org.freedesktop.impl.portal.Test2
Interfaces=org.freedesktop.impl.portal.Settings;
""",
"test_bad.portal": b"""
[portal]
DBusName=org.freedesktop.impl.portal.TestBad
Interfaces=org.freedesktop.impl.portal.Settings;
""",
"test_noimpl.portal": b"""
[portal]
DBusName=org.freedesktop.impl.portal.TestBad
Interfaces=org.freedesktop.impl.portal.NonExistant;
""",
}
def portal_config_good():
# test1 merged with test2 should result in the correct output
files = PORTAL_CONFIG_FILES.copy()
files["test-portals.conf"] = b"""
[preferred]
default=test1;test2;
"""
yield files
# a portal without the settings impl does not affect the result
files = PORTAL_CONFIG_FILES.copy()
files["test-portals.conf"] = b"""
[preferred]
default=test1;test_noimpl;test2;
"""
yield files
# the default should be ignored when the interface is configured
files = PORTAL_CONFIG_FILES.copy()
files["test-portals.conf"] = b"""
[preferred]
default=test_bad;
org.freedesktop.impl.portal.Settings=test1;test2
"""
yield files
# use * which should expand to test1;test2;test_noimpl
files = PORTAL_CONFIG_FILES.copy()
del files["test_bad.portal"]
files["test-portals.conf"] = b"""
[preferred]
default=test_noimpl;
org.freedesktop.impl.portal.Settings=*;
"""
yield files
def portal_config_bad():
# test1 alone should result in bad output
files = PORTAL_CONFIG_FILES.copy()
files["test-portals.conf"] = b"""
[preferred]
default=test1;
"""
yield files
# test2 merged with test1 is the wrong order
files = PORTAL_CONFIG_FILES.copy()
files["test-portals.conf"] = b"""
[preferred]
default=test2;test1;
"""
yield files
# test_noimpl does not affect anything
files = PORTAL_CONFIG_FILES.copy()
files["test-portals.conf"] = b"""
[preferred]
default=test_noimpl;test2;test1;
"""
yield files
# default should get ignored, test2 alone should result in bad output
files = PORTAL_CONFIG_FILES.copy()
files["test-portals.conf"] = b"""
[preferred]
default=test1;test2
org.freedesktop.impl.portal.Settings=test2;test_noimpl
"""
yield files
# test_bad anywhere in the active config should result in bad output
files = PORTAL_CONFIG_FILES.copy()
files["test-portals.conf"] = b"""
[preferred]
default=test1;test2
org.freedesktop.impl.portal.Settings=test_bad;test1;test2
"""
yield files
# use * which expands to test1;test2;test_bad;test_no_impl
# contains test_bad which should result in bad output
files = PORTAL_CONFIG_FILES.copy()
files["test-portals.conf"] = b"""
[preferred]
default=test_noimpl;
org.freedesktop.impl.portal.Settings=*;
"""
yield files
def portal_config_twice():
# check that test1 gets picked up only once
files = PORTAL_CONFIG_FILES.copy()
del files["test_bad.portal"]
files["test-portals.conf"] = b"""
[preferred]
default=test_noimpl;
org.freedesktop.impl.portal.Settings=test1;*;
"""
yield files
@pytest.fixture
def xdg_desktop_portal_dir_default_files():
return next(portal_config_good())
class TestSettings:
def test_version(self, portals, dbus_con):
xdp.check_version(dbus_con, "Settings", 2)
@pytest.mark.parametrize(
"xdg_desktop_portal_dir_default_files",
portal_config_good(),
)
def test_read_all(self, portals, dbus_con):
settings_intf = xdp.get_portal_iface(dbus_con, "Settings")
value = settings_intf.ReadAll([])
assert value == SETTINGS_DATA
value = settings_intf.ReadAll([""])
assert value == SETTINGS_DATA
value = settings_intf.ReadAll(["does-not-exist"])
assert value == {}
value = settings_intf.ReadAll(["org."])
assert value == {}
value = settings_intf.ReadAll(["org.*"])
assert value == SETTINGS_DATA
value = settings_intf.ReadAll(
["org.freedesktop.appearance", "org.example.custom"]
)
assert value == SETTINGS_DATA
value = settings_intf.ReadAll(["org.freedesktop.appearance"])
assert len(value) == 1
assert "org.freedesktop.appearance" in value
assert (
value["org.freedesktop.appearance"]
== SETTINGS_DATA["org.freedesktop.appearance"]
)
@pytest.mark.parametrize(
"xdg_desktop_portal_dir_default_files",
portal_config_bad(),
)
def test_read_all_bad_config(self, portals, dbus_con):
settings_intf = xdp.get_portal_iface(dbus_con, "Settings")
value = settings_intf.ReadAll([])
assert value != SETTINGS_DATA
@pytest.mark.parametrize(
"xdg_desktop_portal_dir_default_files",
portal_config_twice(),
)
def test_config_twice(self, portals, dbus_con):
settings_intf = xdp.get_portal_iface(dbus_con, "Settings")
mock_intf = xdp.get_mock_iface(dbus_con, "org.freedesktop.impl.portal.Test1")
value = settings_intf.ReadAll([])
assert value == SETTINGS_DATA
# The config is `test1;*`, make sure we only get a single call to Test1
method_calls = mock_intf.GetMethodCalls("ReadAll")
assert len(method_calls) == 1
def test_read(self, portals, dbus_con):
settings_intf = xdp.get_portal_iface(dbus_con, "Settings")
color_scheme = SETTINGS_DATA["org.freedesktop.appearance"]["color-scheme"]
value = settings_intf.ReadOne("org.freedesktop.appearance", "color-scheme")
assert isinstance(value, dbus.UInt32)
assert value.variant_level == 1
assert value == color_scheme
with pytest.raises(dbus.exceptions.DBusException) as excinfo:
settings_intf.ReadOne("org.does.not.exist", "color-scheme")
assert excinfo.value.get_dbus_name() == "org.freedesktop.portal.Error.NotFound"
with pytest.raises(dbus.exceptions.DBusException) as excinfo:
settings_intf.ReadOne("org.freedesktop.appearance", "xcolor-scheme")
assert excinfo.value.get_dbus_name() == "org.freedesktop.portal.Error.NotFound"
# deprecated but should still check that it works
# the crucial detail here is that the variant_level is 2
value = settings_intf.Read("org.freedesktop.appearance", "color-scheme")
assert isinstance(value, dbus.UInt32)
assert value.variant_level == 2
assert value == color_scheme
def test_changed(self, portals, dbus_con):
settings_intf = xdp.get_portal_iface(dbus_con, "Settings")
mock_intf = xdp.get_mock_iface(dbus_con, "org.freedesktop.impl.portal.Test1")
changed_count = 0
ns = "org.freedesktop.appearance"
key = "color-scheme"
current_value = SETTINGS_DATA[ns][key]
new_value = 2
assert current_value != new_value
value = settings_intf.ReadOne(ns, key)
assert value == current_value
def cb_settings_changed(changed_ns, changed_key, changed_value):
nonlocal changed_count
changed_count += 1
assert changed_ns == ns
assert changed_key == key
assert changed_value == new_value
settings_intf.connect_to_signal("SettingChanged", cb_settings_changed)
mock_intf.SetSetting(ns, key, new_value)
xdp.wait_for(lambda: changed_count == 1)

View File

@@ -0,0 +1,31 @@
# SPDX-License-Identifier: LGPL-2.1-or-later
#
# This file is formatted with Python Black
import tests as xdp
import os
import tempfile
from pathlib import Path
class TestTrash:
def test_version(self, portals, dbus_con):
xdp.check_version(dbus_con, "Trash", 1)
def test_trash_file_fails(self, portals, dbus_con):
trash_intf = xdp.get_portal_iface(dbus_con, "Trash")
with open("/proc/cmdline") as fd:
result = trash_intf.TrashFile(fd.fileno())
assert result == 0
def test_trash_file(self, portals, dbus_con):
trash_intf = xdp.get_portal_iface(dbus_con, "Trash")
fd, name = tempfile.mkstemp(prefix="trash_portal_mock_", dir=Path.home())
result = trash_intf.TrashFile(fd)
if result != 1:
os.unlink(name)
assert result == 1
assert not Path(name).exists()

View File

@@ -0,0 +1,384 @@
# SPDX-License-Identifier: LGPL-2.1-or-later
#
# This file is formatted with Python Black
import tests as xdp
import pytest
import os
import gi
import subprocess
import re
gi.require_version("UMockdev", "1.0")
from gi.repository import GLib, UMockdev # noqa E402
@pytest.fixture
def required_templates():
return {"usb": {}}
@pytest.fixture
def umockdev():
return UMockdev.Testbed.new()
def umockdev_has_working_remove():
# umockdev only generates remove events since version 0.18.4
# https://github.com/martinpitt/umockdev/releases/tag/0.18.4
required = (0, 18, 4)
result = subprocess.run(["umockdev-run", "--version"], stdout=subprocess.PIPE)
if result.returncode != 0:
return False
match = re.match(r"^(\d+)\.(\d+)\.(\d+)", result.stdout.decode("UTF-8").strip())
if not match:
return False
version = tuple(map(int, match.groups()))
return version >= required
class TestUsb:
_num_devices = 0
def generate_device(
self, testbed, vendor, vendor_name, product, product_name, serial
):
n = self._num_devices
self._num_devices += 1
testbed.add_from_string(f"""P: /devices/usb{n}
N: bus/usb/001/{n:03d}
E: BUSNUM=001
E: DEVNUM={n:03d}
E: DEVNAME=/dev/bus/usb/001/{n:03d}
E: DEVTYPE=usb_device
E: DRIVER=usb
E: ID_BUS=usb
E: ID_MODEL={product_name}
E: ID_MODEL_ID={product}
E: ID_REVISION=0002
E: ID_SERIAL={vendor_name}_{product_name}_{serial}
E: ID_SERIAL_SHORT={serial}
E: ID_VENDOR={vendor_name}
E: ID_VENDOR_ID={vendor}
E: SUBSYSTEM=usb
A: idProduct={product}
A: idVendor={vendor}
""")
return f"/sys/devices/usb{n}"
def test_version(self, portals, dbus_con):
xdp.check_version(dbus_con, "Usb", 1)
def test_create_close_session(self, portals, dbus_con, app_id):
usb_intf = xdp.get_portal_iface(dbus_con, "Usb")
session = xdp.Session(
dbus_con,
usb_intf.CreateSession({"session_handle_token": "session_token0"}),
)
session.close()
def test_empty_initial_devices(self, portals, dbus_con, app_id):
usb_intf = xdp.get_portal_iface(dbus_con, "Usb")
xdp.Session(
dbus_con,
usb_intf.CreateSession({"session_handle_token": "session_token0"}),
)
device_events_signal_received = False
def cb_device_events(session_handle, events):
nonlocal device_events_signal_received
device_events_signal_received = True
usb_intf.connect_to_signal("DeviceEvents", cb_device_events)
xdp.wait(300)
assert not device_events_signal_received
@pytest.mark.parametrize("usb_queries", ["vnd:04a9", None])
def test_initial_devices(self, portals, dbus_con, app_id, usb_queries, umockdev):
usb_intf = xdp.get_portal_iface(dbus_con, "Usb")
self.generate_device(
umockdev,
"04a9",
"Canon_Inc.",
"31c0",
"Canon_Digital_Camera",
"C767F1C714174C309255F70E4A7B2EE2",
)
# TODO: to make this more robust, we should find a way to wait for
# the portal to pick up the device
xdp.wait(300)
session = xdp.Session(
dbus_con,
usb_intf.CreateSession({"session_handle_token": "session_token0"}),
)
device_events_signal_received = False
devices_received = 0
def cb_device_events(session_handle, events):
nonlocal device_events_signal_received
nonlocal devices_received
assert session.handle == session_handle
for action, id, device in events:
assert action == "add"
devices_received += 1
device_events_signal_received = True
usb_intf.connect_to_signal("DeviceEvents", cb_device_events)
if usb_queries is None:
xdp.wait(300)
assert not device_events_signal_received
assert devices_received == 0
else:
xdp.wait_for(lambda: device_events_signal_received)
assert devices_received == 1
@pytest.mark.parametrize("usb_queries", ["vnd:04a9", None])
def test_device_add(self, portals, dbus_con, app_id, usb_queries, umockdev):
usb_intf = xdp.get_portal_iface(dbus_con, "Usb")
session = xdp.Session(
dbus_con,
usb_intf.CreateSession({"session_handle_token": "session_token0"}),
)
device_events_signal_received = False
devices_received = 0
device = None
def cb_device_events(session_handle, events):
nonlocal device_events_signal_received
nonlocal devices_received
nonlocal device
assert session.handle == session_handle
for action, _, dev in events:
assert action == "add"
device = dev
devices_received += 1
device_events_signal_received = True
usb_intf.connect_to_signal("DeviceEvents", cb_device_events)
xdp.wait(300)
assert not device_events_signal_received
self.generate_device(
umockdev,
"04a9",
"Canon_Inc.",
"31c0",
"Canon_Digital_Camera",
"C767F1C714174C309255F70E4A7B2EE2",
)
if usb_queries is None:
xdp.wait(300)
assert not device_events_signal_received
assert devices_received == 0
else:
xdp.wait_for(lambda: device_events_signal_received)
assert devices_received == 1
assert device
assert device["readable"]
assert device["writable"]
assert device["device-file"] == "/dev/bus/usb/001/000"
assert device["properties"]["ID_VENDOR_ID"] == "04a9"
assert device["properties"]["ID_MODEL_ID"] == "31c0"
assert (
device["properties"]["ID_SERIAL"]
== "Canon_Inc._Canon_Digital_Camera_C767F1C714174C309255F70E4A7B2EE2"
)
@pytest.mark.parametrize("usb_queries", ["vnd:04a9", None])
@pytest.mark.skipif(
not umockdev_has_working_remove(), reason="UMockdev version 0.18.4 required"
)
def test_device_remove(self, portals, dbus_con, app_id, usb_queries, umockdev):
usb_intf = xdp.get_portal_iface(dbus_con, "Usb")
dev_path = self.generate_device(
umockdev,
"04a9",
"Canon_Inc.",
"31c0",
"Canon_Digital_Camera",
"C767F1C714174C309255F70E4A7B2EE2",
)
session = xdp.Session(
dbus_con,
usb_intf.CreateSession({"session_handle_token": "session_token0"}),
)
device_events_signal_count = 0
devices_received = 0
devices_removed = 0
def cb_device_events(session_handle, events):
nonlocal device_events_signal_count
nonlocal devices_received
nonlocal devices_removed
assert session.handle == session_handle
for action, id, device in events:
if action == "add":
devices_received += 1
elif action == "remove":
devices_removed += 1
else:
assert False
device_events_signal_count += 1
usb_intf.connect_to_signal("DeviceEvents", cb_device_events)
if usb_queries is None:
xdp.wait(300)
assert device_events_signal_count == 0
assert devices_received == 0
assert devices_removed == 0
else:
xdp.wait_for(lambda: device_events_signal_count == 1)
assert devices_received == 1
assert devices_removed == 0
umockdev.remove_device(dev_path)
if usb_queries is None:
xdp.wait(300)
assert device_events_signal_count == 0
assert devices_received == 0
assert devices_removed == 0
else:
xdp.wait_for(lambda: device_events_signal_count == 2)
assert devices_received == 1
assert devices_removed == 1
@pytest.mark.parametrize("usb_queries", ["vnd:04a9;vnd:04aa"])
@pytest.mark.parametrize(
"template_params", [{"usb": {"filters": {"vendor": "04a9"}}}]
)
def test_acquire(self, portals, dbus_con, app_id, umockdev):
usb_intf = xdp.get_portal_iface(dbus_con, "Usb")
self.generate_device(
umockdev,
"04a9",
"Canon_Inc.",
"31c0",
"Canon_Digital_Camera",
"C767F1C714174C309255F70E4A7B2EE2",
)
self.generate_device(
umockdev,
"04aa",
"Someone Else.",
"31c0",
"SomeProduct",
"00001",
)
possible_vendors = ["04a9", "04aa"]
devices = usb_intf.EnumerateDevices({})
assert len(devices) == 2
(id1, dev_info1) = devices[0]
assert id1
assert dev_info1
vendor_id = dev_info1["properties"]["ID_VENDOR_ID"]
assert vendor_id in possible_vendors
possible_vendors.remove(vendor_id)
(id2, dev_info2) = devices[1]
assert id2
assert dev_info2
vendor_id = dev_info2["properties"]["ID_VENDOR_ID"]
assert vendor_id in possible_vendors
possible_vendors.remove(vendor_id)
request = xdp.Request(dbus_con, usb_intf)
response = request.call(
"AcquireDevices",
parent_window="",
devices=[
(id1, {"writable": True}),
(id2, {"writable": True}),
],
options={},
)
assert response
assert response.response == 0
(results, finished) = usb_intf.FinishAcquireDevices(request.handle, {})
assert finished
assert len(results) == 1
(res_id, device) = results[0]
assert res_id == id1 or res_id == id2
assert device["success"]
fd = device["fd"].take()
assert fd > 0
with os.fdopen(fd, "r") as f:
assert f
assert "error" not in device
usb_intf.ReleaseDevices([res_id], {})
@pytest.mark.parametrize("usb_queries", ["vnd:0001"])
@pytest.mark.parametrize(
"expected,template_params",
[
(1, {"usb": {"filters": {"model": "0000"}}}),
(1, {"usb": {"filters": {"model": "0001"}}}),
(0, {"usb": {"filters": {"model": "0002"}}}),
(2, {"usb": {"filters": {"vendor": "0001"}}}),
(0, {"usb": {"filters": {"vendor": "0002"}}}),
(1, {"usb": {"filters": {"vendor": "0001", "model": "0000"}}}),
(0, {"usb": {"filters": {"vendor": "0002", "model": "0000"}}}),
],
)
def test_queries(self, portals, dbus_con, expected, app_id, usb_queries, umockdev):
usb_intf = xdp.get_portal_iface(dbus_con, "Usb")
for i in range(2):
self.generate_device(
umockdev,
"0001",
"example_org",
f"000{i}",
f"model{i}",
"0001",
)
devices = usb_intf.EnumerateDevices({})
assert len(devices) == 2
acquire_devices = [(id, {"writable": True}) for (id, _) in devices]
request = xdp.Request(dbus_con, usb_intf)
response = request.call(
"AcquireDevices",
parent_window="",
devices=acquire_devices,
options={},
)
assert response
assert response.response == 0
(results, finished) = usb_intf.FinishAcquireDevices(request.handle, {})
assert finished
assert len(results) == expected

View File

@@ -0,0 +1,153 @@
# SPDX-License-Identifier: LGPL-2.1-or-later
#
# This file is formatted with Python Black
import tests as xdp
import pytest
import os
import tempfile
from pathlib import Path
@pytest.fixture
def required_templates():
return {"wallpaper": {}}
class TestWallpaper:
def set_permission(self, dbus_con, app_id, permission):
perm_store_intf = xdp.get_permission_store_iface(dbus_con)
perm_store_intf.SetPermission(
"wallpaper",
True,
"wallpaper",
app_id,
[permission],
)
def test_version(self, portals, dbus_con):
xdp.check_version(dbus_con, "Wallpaper", 1)
def test_wallpaper_uri(self, portals, dbus_con, app_id):
wallpaper_intf = xdp.get_portal_iface(dbus_con, "Wallpaper")
mock_intf = xdp.get_mock_iface(dbus_con)
uri = "file:///test"
show_preview = True
set_on = "both"
request = xdp.Request(dbus_con, wallpaper_intf)
options = {
"show-preview": show_preview,
"set-on": set_on,
}
response = request.call(
"SetWallpaperURI",
parent_window="",
uri=uri,
options=options,
)
assert response
assert response.response == 0
# Check the impl portal was called with the right args
method_calls = mock_intf.GetMethodCalls("SetWallpaperURI")
assert len(method_calls) > 0
_, args = method_calls[-1]
assert args[1] == app_id
assert args[2] == "" # parent window
assert args[3] == uri
assert args[4]["show-preview"] == show_preview
assert args[4]["set-on"] == set_on
def test_wallpaper_file(self, portals, dbus_con, app_id):
wallpaper_intf = xdp.get_portal_iface(dbus_con, "Wallpaper")
mock_intf = xdp.get_mock_iface(dbus_con)
fd, _ = tempfile.mkstemp(prefix="wallpaper_mock", dir=Path.home())
os.write(fd, b"wallpaper_mock_file")
show_preview = True
request = xdp.Request(dbus_con, wallpaper_intf)
options = {
"show-preview": show_preview,
}
response = request.call(
"SetWallpaperFile",
parent_window="",
fd=fd,
options=options,
)
assert response
assert response.response == 0
# Check the impl portal was called with the right args
method_calls = mock_intf.GetMethodCalls("SetWallpaperURI")
assert len(method_calls) > 0
_, args = method_calls[-1]
assert args[1] == app_id
assert args[2] == "" # parent window
assert args[4]["show-preview"] == show_preview
path = args[3]
assert path.startswith("file:///")
with open(path[7:]) as file:
wallpaper_file_contents = file.read()
assert wallpaper_file_contents == "wallpaper_mock_file"
@pytest.mark.parametrize("template_params", ({"wallpaper": {"response": 1}},))
def test_wallpaper_cancel(self, portals, dbus_con, app_id):
wallpaper_intf = xdp.get_portal_iface(dbus_con, "Wallpaper")
uri = "file:///test"
show_preview = True
set_on = "both"
request = xdp.Request(dbus_con, wallpaper_intf)
options = {
"show-preview": show_preview,
"set-on": set_on,
}
response = request.call(
"SetWallpaperURI",
parent_window="",
uri=uri,
options=options,
)
assert response
assert response.response == 1
def test_wallpaper_permission(self, portals, dbus_con, app_id):
wallpaper_intf = xdp.get_portal_iface(dbus_con, "Wallpaper")
mock_intf = xdp.get_mock_iface(dbus_con)
self.set_permission(dbus_con, app_id, "no")
uri = "file:///test"
show_preview = True
set_on = "both"
request = xdp.Request(dbus_con, wallpaper_intf)
options = {
"show-preview": show_preview,
"set-on": set_on,
}
response = request.call(
"SetWallpaperURI",
parent_window="",
uri=uri,
options=options,
)
assert response
assert response.response == 2
# Check the impl portal was called with the right args
method_calls = mock_intf.GetMethodCalls("SetWallpaperURI")
assert len(method_calls) == 0