Branch data Line data Source code
1 : : /*
2 : : * Copyright © 2022 Endless OS Foundation, LLC
3 : : *
4 : : * SPDX-License-Identifier: LGPL-2.1-or-later
5 : : *
6 : : * This library is free software; you can redistribute it and/or
7 : : * modify it under the terms of the GNU Lesser General Public
8 : : * License as published by the Free Software Foundation; either
9 : : * version 2.1 of the License, or (at your option) any later version.
10 : : *
11 : : * This library is distributed in the hope that it will be useful,
12 : : * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 : : * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14 : : * Lesser General Public License for more details.
15 : : *
16 : : * You should have received a copy of the GNU Lesser General
17 : : * Public License along with this library; if not, see <http://www.gnu.org/licenses/>.
18 : : *
19 : : * Author: Philip Withnall <pwithnall@endlessos.org>
20 : : */
21 : :
22 : : #include <gio/gio.h>
23 : : #include <locale.h>
24 : :
25 : : #include <gio/giomodule-priv.h>
26 : : #include "gio/gnotificationbackend.h"
27 : :
28 : :
29 : : static GNotificationBackend *
30 : 2 : construct_backend (GApplication **app_out)
31 : : {
32 : 2 : GApplication *app = NULL;
33 : 2 : GType fdo_type = G_TYPE_INVALID;
34 : 2 : GNotificationBackend *backend = NULL;
35 : 2 : GError *local_error = NULL;
36 : :
37 : : /* Construct the app first and withdraw a notification, to ensure that IO modules are loaded. */
38 : 2 : app = g_application_new ("org.gtk.TestApplication", G_APPLICATION_DEFAULT_FLAGS);
39 : 2 : g_application_register (app, NULL, &local_error);
40 : 2 : g_assert_no_error (local_error);
41 : 2 : g_application_withdraw_notification (app, "org.gtk.TestApplication.NonexistentNotification");
42 : :
43 : 2 : fdo_type = g_type_from_name ("GFdoNotificationBackend");
44 : 2 : g_assert_cmpuint (fdo_type, !=, G_TYPE_INVALID);
45 : :
46 : : /* Replicate the behaviour from g_notification_backend_new_default(), which is
47 : : * not exported publicly so can‘t be easily used in the test. */
48 : 2 : backend = g_object_new (fdo_type, "application", app, NULL);
49 : 2 : backend->dbus_connection = g_application_get_dbus_connection (app);
50 : 2 : if (backend->dbus_connection)
51 : 2 : g_object_ref (backend->dbus_connection);
52 : :
53 : 2 : if (app_out != NULL)
54 : 2 : *app_out = g_object_ref (app);
55 : :
56 : 2 : g_clear_object (&app);
57 : :
58 : 2 : return g_steal_pointer (&backend);
59 : : }
60 : :
61 : : static void
62 : 1 : test_construction (void)
63 : : {
64 : 1 : GNotificationBackend *backend = NULL;
65 : 1 : GApplication *app = NULL;
66 : 1 : GTestDBus *bus = NULL;
67 : :
68 : 1 : g_test_message ("Test constructing a GFdoNotificationBackend");
69 : :
70 : : /* Set up a test session bus and connection. */
71 : 1 : bus = g_test_dbus_new (G_TEST_DBUS_NONE);
72 : 1 : g_test_dbus_up (bus);
73 : :
74 : 1 : backend = construct_backend (&app);
75 : 1 : g_assert_nonnull (backend);
76 : :
77 : 1 : g_application_quit (app);
78 : 1 : g_clear_object (&app);
79 : 1 : g_clear_object (&backend);
80 : :
81 : 1 : g_test_dbus_down (bus);
82 : 1 : g_clear_object (&bus);
83 : 1 : }
84 : :
85 : : static void
86 : 3 : daemon_method_call_cb (GDBusConnection *connection,
87 : : const gchar *sender,
88 : : const gchar *object_path,
89 : : const gchar *interface_name,
90 : : const gchar *method_name,
91 : : GVariant *parameters,
92 : : GDBusMethodInvocation *invocation,
93 : : gpointer user_data)
94 : : {
95 : 3 : GDBusMethodInvocation **current_method_invocation_out = user_data;
96 : :
97 : 3 : g_assert_null (*current_method_invocation_out);
98 : 3 : *current_method_invocation_out = g_steal_pointer (&invocation);
99 : :
100 : 3 : g_main_context_wakeup (NULL);
101 : 3 : }
102 : :
103 : : static void
104 : 1 : name_acquired_or_lost_cb (GDBusConnection *connection,
105 : : const gchar *name,
106 : : gpointer user_data)
107 : : {
108 : 1 : gboolean *name_acquired = user_data;
109 : :
110 : 1 : *name_acquired = !*name_acquired;
111 : :
112 : 1 : g_main_context_wakeup (NULL);
113 : 1 : }
114 : :
115 : : static void
116 : 3 : dbus_activate_action_cb (GSimpleAction *action,
117 : : GVariant *parameter,
118 : : gpointer user_data)
119 : : {
120 : 3 : guint *n_activations = user_data;
121 : :
122 : 3 : *n_activations = *n_activations + 1;
123 : 3 : g_main_context_wakeup (NULL);
124 : 3 : }
125 : :
126 : : static void
127 : 3 : assert_send_notification (GNotificationBackend *backend,
128 : : GDBusMethodInvocation **current_method_invocation,
129 : : guint32 notify_id)
130 : : {
131 : 3 : GNotification *notification = NULL;
132 : :
133 : 3 : notification = g_notification_new ("Some Notification");
134 : 3 : G_NOTIFICATION_BACKEND_GET_CLASS (backend)->send_notification (backend, "notification1", notification);
135 : 3 : g_clear_object (¬ification);
136 : :
137 : 10 : while (*current_method_invocation == NULL)
138 : 7 : g_main_context_iteration (NULL, TRUE);
139 : :
140 : 3 : g_assert_cmpstr (g_dbus_method_invocation_get_interface_name (*current_method_invocation), ==, "org.freedesktop.Notifications");
141 : 3 : g_assert_cmpstr (g_dbus_method_invocation_get_method_name (*current_method_invocation), ==, "Notify");
142 : 3 : g_dbus_method_invocation_return_value (g_steal_pointer (current_method_invocation), g_variant_new ("(u)", notify_id));
143 : 3 : }
144 : :
145 : : static void
146 : 9 : assert_emit_action_invoked (GDBusConnection *daemon_connection,
147 : : GVariant *parameters)
148 : : {
149 : 9 : GError *local_error = NULL;
150 : :
151 : 9 : g_dbus_connection_emit_signal (daemon_connection,
152 : : NULL,
153 : : "/org/freedesktop/Notifications",
154 : : "org.freedesktop.Notifications",
155 : : "ActionInvoked",
156 : : parameters,
157 : : &local_error);
158 : 9 : g_assert_no_error (local_error);
159 : 9 : }
160 : :
161 : : static void
162 : 1 : test_dbus_activate_action (void)
163 : : {
164 : : /* Very trimmed down version of
165 : : * https://specifications.freedesktop.org/notification-spec/notification-spec-latest.html */
166 : 1 : const GDBusArgInfo daemon_notify_in_app_name = { -1, "AppName", "s", NULL };
167 : 1 : const GDBusArgInfo daemon_notify_in_replaces_id = { -1, "ReplacesId", "u", NULL };
168 : 1 : const GDBusArgInfo daemon_notify_in_app_icon = { -1, "AppIcon", "s", NULL };
169 : 1 : const GDBusArgInfo daemon_notify_in_summary = { -1, "Summary", "s", NULL };
170 : 1 : const GDBusArgInfo daemon_notify_in_body = { -1, "Body", "s", NULL };
171 : 1 : const GDBusArgInfo daemon_notify_in_actions = { -1, "Actions", "as", NULL };
172 : 1 : const GDBusArgInfo daemon_notify_in_hints = { -1, "Hints", "a{sv}", NULL };
173 : 1 : const GDBusArgInfo daemon_notify_in_expire_timeout = { -1, "ExpireTimeout", "i", NULL };
174 : 1 : const GDBusArgInfo *daemon_notify_in_args[] =
175 : : {
176 : : &daemon_notify_in_app_name,
177 : : &daemon_notify_in_replaces_id,
178 : : &daemon_notify_in_app_icon,
179 : : &daemon_notify_in_summary,
180 : : &daemon_notify_in_body,
181 : : &daemon_notify_in_actions,
182 : : &daemon_notify_in_hints,
183 : : &daemon_notify_in_expire_timeout,
184 : : NULL
185 : : };
186 : 1 : const GDBusArgInfo daemon_notify_out_id = { -1, "Id", "u", NULL };
187 : 1 : const GDBusArgInfo *daemon_notify_out_args[] = { &daemon_notify_out_id, NULL };
188 : 1 : const GDBusMethodInfo daemon_notify_info = { -1, "Notify", (GDBusArgInfo **) daemon_notify_in_args, (GDBusArgInfo **) daemon_notify_out_args, NULL };
189 : 1 : const GDBusMethodInfo *daemon_methods[] = { &daemon_notify_info, NULL };
190 : 1 : const GDBusInterfaceInfo daemon_interface_info = { -1, "org.freedesktop.Notifications", (GDBusMethodInfo **) daemon_methods, NULL, NULL, NULL };
191 : :
192 : 1 : GTestDBus *bus = NULL;
193 : 1 : GDBusConnection *daemon_connection = NULL;
194 : 1 : guint daemon_object_id = 0, daemon_name_id = 0;
195 : 1 : const GDBusInterfaceVTable vtable = { daemon_method_call_cb, NULL, NULL, { NULL, } };
196 : 1 : GDBusMethodInvocation *current_method_invocation = NULL;
197 : 1 : GApplication *app = NULL;
198 : 1 : GNotificationBackend *backend = NULL;
199 : : guint32 notify_id;
200 : 1 : GError *local_error = NULL;
201 : 1 : const GActionEntry entries[] =
202 : : {
203 : : { "undo", dbus_activate_action_cb, NULL, NULL, NULL, { 0 } },
204 : : { "lang", dbus_activate_action_cb, "s", "'latin'", NULL, { 0 } },
205 : : };
206 : 1 : guint n_activations = 0;
207 : 1 : gboolean name_acquired = FALSE;
208 : :
209 : 1 : g_test_summary ("Test how the backend handles valid and invalid ActionInvoked signals from the daemon");
210 : :
211 : : /* Set up a test session bus and connection. */
212 : 1 : bus = g_test_dbus_new (G_TEST_DBUS_NONE);
213 : 1 : g_test_dbus_up (bus);
214 : :
215 : : /* Create a mock org.freedesktop.Notifications daemon */
216 : 1 : daemon_connection = g_dbus_connection_new_for_address_sync (g_test_dbus_get_bus_address (bus),
217 : : G_DBUS_CONNECTION_FLAGS_AUTHENTICATION_CLIENT |
218 : : G_DBUS_CONNECTION_FLAGS_MESSAGE_BUS_CONNECTION,
219 : : NULL, NULL, &local_error);
220 : 1 : g_assert_no_error (local_error);
221 : :
222 : 1 : daemon_object_id = g_dbus_connection_register_object (daemon_connection,
223 : : "/org/freedesktop/Notifications",
224 : : (GDBusInterfaceInfo *) &daemon_interface_info,
225 : : &vtable,
226 : : ¤t_method_invocation, NULL, &local_error);
227 : 1 : g_assert_no_error (local_error);
228 : :
229 : 1 : daemon_name_id = g_bus_own_name_on_connection (daemon_connection,
230 : : "org.freedesktop.Notifications",
231 : : G_BUS_NAME_OWNER_FLAGS_DO_NOT_QUEUE,
232 : : name_acquired_or_lost_cb,
233 : : name_acquired_or_lost_cb,
234 : : &name_acquired, NULL);
235 : :
236 : 3 : while (!name_acquired)
237 : 2 : g_main_context_iteration (NULL, TRUE);
238 : :
239 : : /* Construct our FDO backend under test */
240 : 1 : backend = construct_backend (&app);
241 : 1 : g_action_map_add_action_entries (G_ACTION_MAP (app), entries, G_N_ELEMENTS (entries), &n_activations);
242 : :
243 : : /* Send a notification to ensure that the backend is listening for D-Bus action signals. */
244 : 1 : notify_id = 1233;
245 : 1 : assert_send_notification (backend, ¤t_method_invocation, ++notify_id);
246 : :
247 : : /* Send a valid fake action signal. */
248 : 1 : n_activations = 0;
249 : 1 : assert_emit_action_invoked (daemon_connection, g_variant_new ("(us)", notify_id, "app.undo"));
250 : :
251 : 3 : while (n_activations == 0)
252 : 2 : g_main_context_iteration (NULL, TRUE);
253 : :
254 : 1 : g_assert_cmpuint (n_activations, ==, 1);
255 : :
256 : : /* Send a valid fake action signal with a target. We have to create a new
257 : : * notification first, as invoking an action on a notification removes it, and
258 : : * the GLib implementation of org.freedesktop.Notifications doesn’t currently
259 : : * support the `resident` hint to avoid that. */
260 : 1 : assert_send_notification (backend, ¤t_method_invocation, ++notify_id);
261 : 1 : n_activations = 0;
262 : 1 : assert_emit_action_invoked (daemon_connection, g_variant_new ("(us)", notify_id, "app.lang::spanish"));
263 : :
264 : 4 : while (n_activations == 0)
265 : 3 : g_main_context_iteration (NULL, TRUE);
266 : :
267 : 1 : g_assert_cmpuint (n_activations, ==, 1);
268 : :
269 : : /* Send a series of invalid action signals, followed by one valid one which
270 : : * we should be able to detect. */
271 : 1 : assert_send_notification (backend, ¤t_method_invocation, ++notify_id);
272 : 1 : n_activations = 0;
273 : 1 : assert_emit_action_invoked (daemon_connection, g_variant_new ("(us)", notify_id, "app.nonexistent"));
274 : 1 : assert_emit_action_invoked (daemon_connection, g_variant_new ("(us)", notify_id, "app.lang(13)"));
275 : 1 : assert_emit_action_invoked (daemon_connection, g_variant_new ("(us)", notify_id, "app.undo::should-have-no-parameter"));
276 : 1 : assert_emit_action_invoked (daemon_connection, g_variant_new ("(us)", notify_id, "app.lang"));
277 : 1 : assert_emit_action_invoked (daemon_connection, g_variant_new ("(us)", notify_id, "undo")); /* no `app.` prefix */
278 : 1 : assert_emit_action_invoked (daemon_connection, g_variant_new ("(us)", notify_id, "app.lang(")); /* invalid parse format */
279 : 1 : assert_emit_action_invoked (daemon_connection, g_variant_new ("(us)", notify_id, "app.undo"));
280 : :
281 : 7 : while (n_activations == 0)
282 : 6 : g_main_context_iteration (NULL, TRUE);
283 : :
284 : 1 : g_assert_cmpuint (n_activations, ==, 1);
285 : :
286 : : /* Shut down. */
287 : 1 : g_assert_null (current_method_invocation);
288 : :
289 : 1 : g_application_quit (app);
290 : 1 : g_clear_object (&app);
291 : 1 : g_clear_object (&backend);
292 : :
293 : 1 : g_dbus_connection_unregister_object (daemon_connection, daemon_object_id);
294 : 1 : g_bus_unown_name (daemon_name_id);
295 : :
296 : 1 : g_dbus_connection_flush_sync (daemon_connection, NULL, &local_error);
297 : 1 : g_assert_no_error (local_error);
298 : 1 : g_dbus_connection_close_sync (daemon_connection, NULL, &local_error);
299 : 1 : g_assert_no_error (local_error);
300 : :
301 : 1 : g_clear_object (&daemon_connection);
302 : :
303 : 1 : g_test_dbus_down (bus);
304 : 1 : g_clear_object (&bus);
305 : 1 : }
306 : :
307 : : int
308 : 1 : main (int argc,
309 : : char *argv[])
310 : : {
311 : 1 : setlocale (LC_ALL, "");
312 : :
313 : : /* Force use of the FDO backend */
314 : 1 : g_setenv ("GNOTIFICATION_BACKEND", "freedesktop", TRUE);
315 : :
316 : 1 : g_test_init (&argc, &argv, NULL);
317 : :
318 : : /* Make sure we don’t send notifications to the actual D-Bus session. */
319 : 1 : g_test_dbus_unset ();
320 : :
321 : 1 : g_test_add_func ("/fdo-notification-backend/construction", test_construction);
322 : 1 : g_test_add_func ("/fdo-notification-backend/dbus/activate-action", test_dbus_activate_action);
323 : :
324 : 1 : return g_test_run ();
325 : : }
|