(root)/
glib-2.79.0/
gio/
tests/
fdo-notification-backend.c
       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 (&notification);
     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                                                          &current_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, &current_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, &current_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, &current_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  }