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