/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*-
 *
 * Copyright 2025 GNOME Foundation, Inc.
 *
 * SPDX-License-Identifier: LGPL-2.1-or-later
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 *
 * Authors:
 *  - Philip Withnall <pwithnall@gnome.org>
 */

#include "config.h"

#include <glib.h>
#include <gio/gio.h>
#include <libmalcontent-timer/extension-agent-object.h>
#include <libmalcontent-timer/extension-agent-object-polkit.h>
#include <libglib-testing/dbus-queue.h>
#include <locale.h>
#include <pwd.h>
#include <string.h>
#include <sys/types.h>

#include "polkit-authority-iface.h"


/* Fixture for tests which have an `MctExtensionAgentObjectPolkit` interacting
 * with polkit over D-Bus. The polkit service is mocked up using @queue, which
 * allows us to reply to D-Bus calls from the code under test from within the
 * test process. The code under test is the `MctExtensionAgentObjectPolkit`. It
 * only has D-Bus interface, so the tests need to trigger it by making D-Bus
 * calls (rather than C function calls) via `timerd_connection`. In this way,
 * the unit test pretends to be the `malcontent-timerd` process which would be
 * interacting with the agent in production.
 */
typedef struct
{
  GtDBusQueue *queue;  /* (owned) */
  MctExtensionAgentObjectPolkit *extension_agent;  /* (owned) */
  unsigned int extension_agent_name_id;
  GDBusConnection *timerd_connection;  /* (owned) */
} BusFixture;

static void name_lost_cb (GDBusConnection *connection,
                          const char      *name,
                          void            *user_data);
static GDBusConnection *dbus_queue_open_additional_client_connection (void);

static void
bus_set_up (BusFixture *fixture,
            const void *test_data)
{
  g_autoptr(GError) local_error = NULL;

  fixture->queue = gt_dbus_queue_new ();

  gt_dbus_queue_connect (fixture->queue, &local_error);
  g_assert_no_error (local_error);

  gt_dbus_queue_own_name (fixture->queue, "org.freedesktop.PolicyKit1");

  gt_dbus_queue_export_object (fixture->queue,
                               "/org/freedesktop/PolicyKit1/Authority",
                               (GDBusInterfaceInfo *) &org_freedesktop_policy_kit1_authority_interface,
                               &local_error);
  g_assert_no_error (local_error);

  fixture->extension_agent_name_id =
      g_bus_own_name_on_connection (gt_dbus_queue_get_client_connection (fixture->queue),
                                    "org.freedesktop.MalcontentTimer1.ExtensionAgent",
                                    G_BUS_NAME_OWNER_FLAGS_DO_NOT_QUEUE,
                                    NULL,
                                    name_lost_cb,
                                    NULL,
                                    NULL);

  fixture->extension_agent = mct_extension_agent_object_polkit_new (gt_dbus_queue_get_client_connection (fixture->queue),
                                                                    "/org/freedesktop/MalcontentTimer1/ExtensionAgent");
  mct_extension_agent_object_register (MCT_EXTENSION_AGENT_OBJECT (fixture->extension_agent), &local_error);
  g_assert_no_error (local_error);

  fixture->timerd_connection = dbus_queue_open_additional_client_connection ();

  /* Create a mock app desktop file which we can refer to in the tests. It will
   * be automatically deleted by G_TEST_OPTION_ISOLATE_DIRS on teardown. */
  g_autofree char *applications_dir = NULL;
  g_autofree char *desktop_file_path = NULL;

  applications_dir = g_build_filename (g_get_user_data_dir (), "applications", NULL);
  g_assert_no_errno (g_mkdir_with_parents (applications_dir, 0755));

  desktop_file_path = g_build_filename (applications_dir, "org.freedesktop.Malcontent.TestApp.desktop", NULL);
  g_file_set_contents (desktop_file_path,
                       "[Desktop Entry]\n"
                       "Name=Test Application\n"
                       "Exec=true %U\n"
                       "Type=Application\n",
                       -1, &local_error);
  g_assert_no_error (local_error);
}

static void
bus_tear_down (BusFixture *fixture,
               const void *test_data)
{
  if (fixture->timerd_connection != NULL)
    g_dbus_connection_close_sync (fixture->timerd_connection, NULL, NULL);
  g_clear_object (&fixture->timerd_connection);
  mct_extension_agent_object_unregister (MCT_EXTENSION_AGENT_OBJECT (fixture->extension_agent));
  g_clear_object (&fixture->extension_agent);
  g_clear_handle_id (&fixture->extension_agent_name_id, g_bus_unown_name);
  gt_dbus_queue_disconnect (fixture->queue, TRUE);
  g_clear_pointer (&fixture->queue, gt_dbus_queue_free);
}

static void
async_result_cb (GObject      *source_object,
                 GAsyncResult *result,
                 void         *user_data)
{
  GAsyncResult **result_out = user_data;

  g_assert (*result_out == NULL);
  *result_out = g_object_ref (result);
  g_main_context_wakeup (NULL);
}

static void
name_lost_cb (GDBusConnection *connection,
              const char      *name,
              void            *user_data)
{
  g_assert_not_reached ();
}

static GDBusConnection *
dbus_queue_open_additional_client_connection (void)
{
  g_autoptr(GAsyncResult) result = NULL;
  g_autoptr(GDBusConnection) connection = NULL;
  g_autoptr(GError) local_error = NULL;

  /* FIXME: Hackily get the bus address as set by the GTestDBus instance
   * internally in the GtDBusQueue. We ideally need a new helper method on the
   * GtDBusQueue to get another connection. */
  g_dbus_connection_new_for_address (g_getenv ("DBUS_SESSION_BUS_ADDRESS"),
                                     G_DBUS_CONNECTION_FLAGS_AUTHENTICATION_CLIENT |
                                     G_DBUS_CONNECTION_FLAGS_MESSAGE_BUS_CONNECTION,
                                     NULL,
                                     NULL,
                                     async_result_cb,
                                     &result);
  while (result == NULL)
    g_main_context_iteration (NULL, TRUE);
  connection = g_dbus_connection_new_for_address_finish (result, &local_error);
  g_assert_no_error (local_error);
  g_assert_nonnull (connection);

  return g_steal_pointer (&connection);
}

#define assert_cmpvariant_parsed(v1, v2_string, ...) \
  G_STMT_START \
    { \
      g_autoptr(GVariant) v2 = g_variant_new_parsed ((v2_string), ##__VA_ARGS__); \
      g_assert_cmpvariant ((v1), v2); \
    } \
  G_STMT_END

/* Test that a D-Bus object exists, by assuming that it implements the
 * o.fd.DBus.Introspectable interface, and querying that. We don’t care what the
 * result is, we care whether it errors. */
static gboolean
dbus_object_exists (GDBusConnection *connection,
                    const char      *bus_name,
                    const char      *object_path)
{
  g_autoptr(GAsyncResult) result = NULL;
  g_autoptr(GVariant) reply = NULL;
  const char *introspection_data;
  g_autoptr(GError) local_error = NULL;

  g_dbus_connection_call (connection,
                          bus_name,
                          object_path,
                          "org.freedesktop.DBus.Introspectable",
                          "Introspect",
                          NULL,
                          G_VARIANT_TYPE ("(s)"),
                          G_DBUS_CALL_FLAGS_NONE,
                          G_MAXINT,  /* timeout (ms) */
                          NULL,
                          async_result_cb,
                          &result);
  while (result == NULL)
    g_main_context_iteration (NULL, TRUE);
  reply = g_dbus_connection_call_finish (connection, result, &local_error);

  if (g_error_matches (local_error, G_DBUS_ERROR, G_DBUS_ERROR_UNKNOWN_METHOD))
    return FALSE;

  g_assert_no_error (local_error);
  g_assert_nonnull (reply);

  /* FIXME: GDBus can return `<node></node>` for an object which doesn’t exist,
   * rather than returning a D-Bus error. This doesn’t seem right to me, but
   * it’s the behaviour in stable releases so we have to deal with it. */
  g_variant_get (reply, "(&s)", &introspection_data);
  if (strstr (introspection_data, "<interface") == NULL)
    return FALSE;

  return TRUE;
}

/* Generic mock polkit implementation which expects a single
 * `CheckAuthorization()` call, and returns the given `.result` for it. Intended
 * to be used for writing tests for the extension agent process. */
typedef struct
{
  uid_t expected_subject_uid;
  const char *expected_polkit_message;
  const char *expected_duration_str;
  const char *expected_app_name;
  gboolean expected_allow_user_interaction;
  struct
    {
      /* Either the first three fields should be set, or the second two */
      gboolean is_authorized;
      gboolean is_challenge;
      const char *details;  /* (nullable) */
      const char *error_name;  /* (nullable) */
      const char *error_message;  /* (nullable) */
    } result;
} PolkitData;

/* This is run in a worker thread. */
static void
polkit_server_cb (GtDBusQueue *queue,
                  void        *user_data)
{
  const PolkitData *data = user_data;
  g_autoptr(GDBusMethodInvocation) invocation1 = NULL;
  g_autoptr(GVariant) subject_variant = NULL;
  const char *action_id;
  g_autoptr(GVariantIter) details_iter = NULL;
  uint32_t flags;
  const char *cancellation_id;
  const char *details_key, *details_value;
  size_t n_expected_entries;

  /* Handle the CheckAuthorization() call. */
  invocation1 =
      gt_dbus_queue_assert_pop_message (queue,
                                        "/org/freedesktop/PolicyKit1/Authority",
                                        "org.freedesktop.PolicyKit1.Authority",
                                        "CheckAuthorization",
                                        "(@r&sa{ss}u&s)",
                                        &subject_variant,
                                        &action_id,
                                        &details_iter,
                                        &flags,
                                        &cancellation_id);

  assert_cmpvariant_parsed (subject_variant,
                            "('unix-process', {'pidfd': <@h 0>, 'uid': <@i %i>})",
                            data->expected_subject_uid);
  g_assert_cmpstr (action_id, ==, "org.freedesktop.Malcontent.SessionLimits.Extend");

  while (g_variant_iter_loop (details_iter, "{&s&s}", &details_key, &details_value))
    {
      if (g_str_equal (details_key, "polkit.message"))
        g_assert_cmpstr (details_value, ==, data->expected_polkit_message);
      else if (g_str_equal (details_key, "polkit.gettext_domain"))
        g_assert_cmpstr (details_value, ==, "malcontent");
      else if (g_str_equal (details_key, "polkit.icon_name"))
        g_assert_cmpstr (details_value, ==, "org.freedesktop.MalcontentControl");
      else if (g_str_equal (details_key, "child_user_display_name"))
        g_assert_cmpstr (details_value, !=, "");
      else if (g_str_equal (details_key, "duration_str"))
        g_assert_cmpstr (details_value, ==, data->expected_duration_str);
      else if (g_str_equal (details_key, "time_str"))
        g_assert_cmpstr (details_value, !=, "");  /* can’t check the actual time as it varies */
      else if (g_str_equal (details_key, "app_name"))
        g_assert_cmpstr (details_value, ==, data->expected_app_name);
      else
        g_assert_not_reached ();
    }

  n_expected_entries = 5;
  if (data->expected_duration_str != NULL)
    n_expected_entries++;
  if (data->expected_app_name != NULL)
    n_expected_entries++;

  g_assert_cmpuint (g_variant_iter_n_children (details_iter), ==, n_expected_entries);

  g_assert_cmpuint (flags, ==, data->expected_allow_user_interaction ? 0x01 : 0x00);
  g_assert_cmpstr (cancellation_id, !=, "");

  if (data->result.error_name == NULL)
    {
      /* Note: The extra struct wrapper is correct as per the polkit API */
      g_dbus_method_invocation_return_value (invocation1,
                                             g_variant_new ("((bb@a{ss}))",
                                                            data->result.is_authorized,
                                                            data->result.is_challenge,
                                                            g_variant_new_parsed (data->result.details)));
    }
  else
    {
      g_dbus_method_invocation_return_dbus_error (invocation1,
                                                  data->result.error_name,
                                                  data->result.error_message);
    }
}

static void
test_extension_agent_polkit_construction (BusFixture *fixture,
                                          const void *test_data)
{
  g_autoptr(GDBusConnection) connection = NULL;
  g_autofree char *object_path = NULL;
  gboolean busy;

  g_test_summary ("Smoketest for constructing an MctExtensionAgentObjectPolkit");

  /* The extension agent object was created in bus_set_up(); we now just need
   * to query its methods. */
  g_assert_true (mct_extension_agent_object_get_connection (MCT_EXTENSION_AGENT_OBJECT (fixture->extension_agent)) ==
                 gt_dbus_queue_get_client_connection (fixture->queue));
  g_assert_false (mct_extension_agent_object_get_busy (MCT_EXTENSION_AGENT_OBJECT (fixture->extension_agent)));

  g_object_get (fixture->extension_agent,
                "connection", &connection,
                "object-path", &object_path,
                "busy", &busy,
                NULL);
  g_assert_true (connection == gt_dbus_queue_get_client_connection (fixture->queue));
  g_assert_cmpstr (object_path, ==, "/org/freedesktop/MalcontentTimer1/ExtensionAgent");
  g_assert_false (busy);
}

static void
request_response_cb (GDBusConnection *connection,
                     const char      *sender_name,
                     const char      *object_path,
                     const char      *interface_name,
                     const char      *signal_name,
                     GVariant        *parameters,
                     void            *user_data)
{
  GVariant **parameters_out = user_data;

  /* This is the extension agent’s unique name, which we don’t easily have
   * access to in this callback: */
  g_assert_cmpstr (sender_name, !=, "");

  g_assert_true (g_str_has_prefix (object_path, "/org/freedesktop/MalcontentTimer1/ExtensionAgent/Request"));
  g_assert_cmpstr (interface_name, ==, "org.freedesktop.MalcontentTimer1.ExtensionAgent.Request");
  g_assert_cmpstr (signal_name, ==, "Response");

  g_assert (*parameters_out == NULL);
  *parameters_out = g_variant_ref (parameters);
  g_main_context_wakeup (NULL);
}

static unsigned int
extension_agent_subscribe_responses (GDBusConnection  *connection,
                                     GVariant        **response_variant_out)
{
  return g_dbus_connection_signal_subscribe (connection,
                                             "org.freedesktop.MalcontentTimer1.ExtensionAgent",
                                             "org.freedesktop.MalcontentTimer1.ExtensionAgent.Request",
                                             "Response",
                                             NULL,  /* object path */
                                             NULL,  /* arg0 */
                                             G_DBUS_SIGNAL_FLAGS_NONE,
                                             request_response_cb,
                                             response_variant_out,
                                             NULL);
}

static char *
extension_agent_request_extension (GDBusConnection  *connection,
                                   uid_t             subject_uid,
                                   const char       *record_type,
                                   const char       *identifier,
                                   uint64_t          duration_secs,
                                   gboolean          allow_user_interaction,
                                   GError          **error)
{
  g_autoptr(GAsyncResult) result = NULL;
  g_autoptr(GVariant) reply = NULL;
  g_autofree char *request_object_path = NULL;

  g_dbus_connection_call (connection,
                          "org.freedesktop.MalcontentTimer1.ExtensionAgent",
                          "/org/freedesktop/MalcontentTimer1/ExtensionAgent",
                          "org.freedesktop.MalcontentTimer1.ExtensionAgent",
                          "RequestExtension",
                          g_variant_new ("(sst(s@a{sv})@a{sv})",
                                         record_type,
                                         identifier,
                                         duration_secs,
                                         "unix-process",
                                         g_variant_new_parsed ("@a{sv} { 'pidfd': <@h 0>, 'uid': <@i %i> }", subject_uid),
                                         g_variant_new_parsed ("@a{sv} {}")),
                          G_VARIANT_TYPE ("(o)"),
                          allow_user_interaction ? G_DBUS_CALL_FLAGS_ALLOW_INTERACTIVE_AUTHORIZATION : G_DBUS_CALL_FLAGS_NONE,
                          G_MAXINT,  /* timeout (ms) */
                          NULL,
                          async_result_cb,
                          &result);
  while (result == NULL)
    g_main_context_iteration (NULL, TRUE);
  reply = g_dbus_connection_call_finish (connection, result, error);

  if (reply == NULL)
    return NULL;

  g_variant_get (reply, "(o)", &request_object_path);
  g_assert_true (g_str_has_prefix (request_object_path, "/org/freedesktop/MalcontentTimer1/ExtensionAgent/Request"));

  return g_steal_pointer (&request_object_path);
}

static gboolean
extension_agent_request_close (GDBusConnection  *connection,
                               const char       *request_object_path,
                               GError          **error)
{
  g_autoptr(GAsyncResult) result = NULL;
  g_autoptr(GVariant) reply = NULL;

  g_dbus_connection_call (connection,
                          "org.freedesktop.MalcontentTimer1.ExtensionAgent",
                          request_object_path,
                          "org.freedesktop.MalcontentTimer1.ExtensionAgent.Request",
                          "Close",
                          NULL,
                          G_VARIANT_TYPE_UNIT,
                          G_DBUS_CALL_FLAGS_NONE,
                          G_MAXINT,  /* timeout (ms) */
                          NULL,
                          async_result_cb,
                          &result);
  while (result == NULL)
    g_main_context_iteration (NULL, TRUE);
  reply = g_dbus_connection_call_finish (connection, result, error);

  if (reply != NULL)
    {
      assert_cmpvariant_parsed (reply, "()");
      return TRUE;
    }
  else
    {
      return FALSE;
    }
}

/* Test that making a time limit extension request works on the success path.
 *
 * The mock D-Bus replies are generated in polkit_server_cb().
 *
 * @test_data contains a `SuccessfulRequestData` with the request arguments and
 * the expected results. */
typedef struct
{
  const uid_t subject_uid;
  const char *record_type;
  const char *identifier;
  uint64_t duration_secs;
  const char *expected_polkit_message;
  const char *expected_duration_str;
  const char *expected_app_name;
} SuccessfulRequestData;

static void
test_extension_agent_polkit_request_successful (BusFixture *fixture,
                                                const void *test_data)
{
  g_autoptr(GError) local_error = NULL;
  unsigned int signal_id = 0;
  g_autoptr(GVariant) response_variant = NULL;
  g_autofree char *request_object_path = NULL;
  const SuccessfulRequestData *request_data = test_data;
  const PolkitData polkit_data =
    {
      .expected_subject_uid = request_data->subject_uid,
      .expected_polkit_message = request_data->expected_polkit_message,
      .expected_duration_str = request_data->expected_duration_str,
      .expected_app_name = request_data->expected_app_name,
      .expected_allow_user_interaction = TRUE,
      .result.is_authorized = TRUE,
      .result.is_challenge = FALSE,
      .result.details = "@a{ss} {}",
    };

  gt_dbus_queue_set_server_func (fixture->queue, polkit_server_cb,
                                 (void *) &polkit_data);

  /* Call the RequestExtension method on the agent. We need to use a separate
   * D-Bus connection so we call from a different unique name from the one
   * which registered the Agent object.
   *
   * Connect to signals from requests first, to avoid race conditions. */
  signal_id = extension_agent_subscribe_responses (fixture->timerd_connection,
                                                   &response_variant);

  request_object_path = extension_agent_request_extension (fixture->timerd_connection,
                                                           request_data->subject_uid,
                                                           request_data->record_type,
                                                           request_data->identifier,
                                                           request_data->duration_secs,
                                                           TRUE,
                                                           &local_error);
  g_assert_no_error (local_error);

  /* Wait for the Response signal. @response_variant is set in request_response_cb(). */
  while (response_variant == NULL)
    g_main_context_iteration (NULL, TRUE);

  assert_cmpvariant_parsed (response_variant, "(true, @a{sv} {})");

  /* The agent should be marked as busy until the request object is closed. */
  g_assert_true (mct_extension_agent_object_get_busy (MCT_EXTENSION_AGENT_OBJECT (fixture->extension_agent)));
  g_assert_true (dbus_object_exists (fixture->timerd_connection, "org.freedesktop.MalcontentTimer1.ExtensionAgent", request_object_path));

  /* Call Close() on the request. */
  extension_agent_request_close (fixture->timerd_connection, request_object_path, &local_error);
  g_assert_no_error (local_error);

  /* The agent should no longer be busy, and the Request object should no longer exist.
   * Check that by querying its properties. */
  g_assert_false (mct_extension_agent_object_get_busy (MCT_EXTENSION_AGENT_OBJECT (fixture->extension_agent)));
  g_assert_false (dbus_object_exists (fixture->timerd_connection, "org.freedesktop.MalcontentTimer1.ExtensionAgent", request_object_path));

  /* Clean up. */
  g_dbus_connection_signal_unsubscribe (fixture->timerd_connection, signal_id);
  signal_id = 0;
}

/* Test that making a time limit extension request fails gracefully if polkit
 * returns an error (which is different from returning that permission was
 * denied).
 *
 * The mock D-Bus replies are generated in polkit_server_cb().
 *
 * @test_data contains an `PolkitErrorPair` with the polkit error and the
 * corresponding expected error from the extension agent. */
typedef struct
{
  const char *polkit_error_name;
  const char *expected_mct_error_name;
} PolkitErrorPair;

static void
test_extension_agent_polkit_request_error_polkit (BusFixture *fixture,
                                                  const void *test_data)
{
  g_autoptr(GError) local_error = NULL;
  unsigned int signal_id = 0;
  g_autoptr(GVariant) response_variant = NULL;
  g_autofree char *request_object_path = NULL;
  const uid_t subject_uid = getuid ();
  const PolkitErrorPair *error_data = test_data;
  const PolkitData polkit_data =
    {
      .expected_subject_uid = subject_uid,
      .expected_polkit_message = "$(child_user_display_name) has requested a device screen time limit extension of $(duration_str) (until $(time_str))",
      .expected_duration_str = "1 hour",
      .expected_allow_user_interaction = TRUE,
      .result.error_name = error_data->polkit_error_name,
      .result.error_message = "Polkit gave some error",
    };

  gt_dbus_queue_set_server_func (fixture->queue, polkit_server_cb,
                                 (void *) &polkit_data);

  /* Call the RequestExtension method on the agent. We need to use a separate
   * D-Bus connection so we call from a different unique name from the one
   * which registered the Agent object.
   *
   * Connect to signals from requests first, to avoid race conditions. */
  signal_id = extension_agent_subscribe_responses (fixture->timerd_connection,
                                                   &response_variant);

  request_object_path = extension_agent_request_extension (fixture->timerd_connection,
                                                           subject_uid,
                                                           "login-session",
                                                           "",
                                                           3600,
                                                           TRUE,
                                                           &local_error);
  g_assert_no_error (local_error);

  /* Wait for the Response signal. @response_variant is set in request_response_cb(). */
  while (response_variant == NULL)
    g_main_context_iteration (NULL, TRUE);

  assert_cmpvariant_parsed (response_variant, "(false, {'error-name': <%s>})", error_data->expected_mct_error_name);

  /* The agent should be marked as busy until the request object is closed. */
  g_assert_true (mct_extension_agent_object_get_busy (MCT_EXTENSION_AGENT_OBJECT (fixture->extension_agent)));
  g_assert_true (dbus_object_exists (fixture->timerd_connection, "org.freedesktop.MalcontentTimer1.ExtensionAgent", request_object_path));

  /* Call Close() on the request. */
  extension_agent_request_close (fixture->timerd_connection, request_object_path, &local_error);
  g_assert_no_error (local_error);

  /* The agent should no longer be busy, and the Request object should no longer exist.
   * Check that by querying its properties. */
  g_assert_false (mct_extension_agent_object_get_busy (MCT_EXTENSION_AGENT_OBJECT (fixture->extension_agent)));
  g_assert_false (dbus_object_exists (fixture->timerd_connection, "org.freedesktop.MalcontentTimer1.ExtensionAgent", request_object_path));

  /* Clean up. */
  g_dbus_connection_signal_unsubscribe (fixture->timerd_connection, signal_id);
  signal_id = 0;
}

/* Test that making a time limit extension request fails gracefully if the UID
 * of the subject (the child user) doesn’t actually exist.
 *
 * No mock D-Bus replies are needed as the polkit daemon is never queried. */
static void
test_extension_agent_polkit_request_error_user_non_existent (BusFixture *fixture,
                                                             const void *test_data G_GNUC_UNUSED)
{
  g_autoptr(GError) local_error = NULL;
  unsigned int signal_id = 0;
  g_autoptr(GVariant) response_variant = NULL;
  g_autofree char *request_object_path = NULL;
  const uid_t subject_uid = 123456;  /* arbitrarily chosen to hopefully not exist */
  char buffer[4096];
  struct passwd pwbuf;
  struct passwd *result;
  int pwuid_errno;

  /* Check that @subject_uid doesn’t actually exist on this system. */
  pwuid_errno = getpwuid_r (subject_uid, &pwbuf, buffer, sizeof (buffer), &result);
  if (pwuid_errno != 0 || result != NULL)
    {
      g_autofree char *message = g_strdup_printf ("User %u actually exists", subject_uid);
      g_test_skip (message);
      return;
    }

  /* Call the RequestExtension method on the agent. We need to use a separate
   * D-Bus connection so we call from a different unique name from the one
   * which registered the Agent object.
   *
   * Connect to signals from requests first, to avoid race conditions. */
  signal_id = extension_agent_subscribe_responses (fixture->timerd_connection,
                                                   &response_variant);

  request_object_path = extension_agent_request_extension (fixture->timerd_connection,
                                                           subject_uid,
                                                           "login-session",
                                                           "",
                                                           3600,
                                                           TRUE,
                                                           &local_error);
  g_assert_no_error (local_error);

  /* Wait for the Response signal. @response_variant is set in request_response_cb(). */
  while (response_variant == NULL)
    g_main_context_iteration (NULL, TRUE);

  assert_cmpvariant_parsed (response_variant, "(false, {'error-name': <'org.freedesktop.MalcontentTimer1.ExtensionAgent.Error.IdentifyingUser'>})");

  /* The agent should be marked as busy until the request object is closed. */
  g_assert_true (mct_extension_agent_object_get_busy (MCT_EXTENSION_AGENT_OBJECT (fixture->extension_agent)));
  g_assert_true (dbus_object_exists (fixture->timerd_connection, "org.freedesktop.MalcontentTimer1.ExtensionAgent", request_object_path));

  /* Call Close() on the request. */
  extension_agent_request_close (fixture->timerd_connection, request_object_path, &local_error);
  g_assert_no_error (local_error);

  /* The agent should no longer be busy, and the Request object should no longer exist.
   * Check that by querying its properties. */
  g_assert_false (mct_extension_agent_object_get_busy (MCT_EXTENSION_AGENT_OBJECT (fixture->extension_agent)));
  g_assert_false (dbus_object_exists (fixture->timerd_connection, "org.freedesktop.MalcontentTimer1.ExtensionAgent", request_object_path));

  /* Clean up. */
  g_dbus_connection_signal_unsubscribe (fixture->timerd_connection, signal_id);
  signal_id = 0;
}

/* Test the validation of arguments to RequestExtension().
 *
 * No mock D-Bus replies are needed as the polkit daemon is never queried.
 *
 * @test_data contains a `ValidationData` with the valid/invalid arguments to
 * use. The same error is always expected as a result. */
typedef struct
{
  const char *record_type;
  const char *identifier;
  const char *subject_kind;
  const char *subject_details;
} ValidationData;

static void
test_extension_agent_polkit_request_error_validation (BusFixture *fixture,
                                                      const void *test_data)
{
  g_autoptr(GError) local_error = NULL;
  g_autoptr(GAsyncResult) result = NULL;
  g_autoptr(GVariant) reply = NULL;
  const ValidationData *validation_data = test_data;

  g_dbus_connection_call (fixture->timerd_connection,
                          "org.freedesktop.MalcontentTimer1.ExtensionAgent",
                          "/org/freedesktop/MalcontentTimer1/ExtensionAgent",
                          "org.freedesktop.MalcontentTimer1.ExtensionAgent",
                          "RequestExtension",
                          g_variant_new ("(sst(s@a{sv})@a{sv})",
                                         validation_data->record_type,
                                         validation_data->identifier,
                                         3600,
                                         validation_data->subject_kind,
                                         g_variant_new_parsed (validation_data->subject_details),
                                         g_variant_new_parsed ("@a{sv} {}")),
                          G_VARIANT_TYPE ("(o)"),
                          G_DBUS_CALL_FLAGS_NONE,
                          G_MAXINT,  /* timeout (ms) */
                          NULL,
                          async_result_cb,
                          &result);
  while (result == NULL)
    g_main_context_iteration (NULL, TRUE);
  reply = g_dbus_connection_call_finish (fixture->timerd_connection, result, &local_error);

  g_assert_error (local_error, MCT_EXTENSION_AGENT_OBJECT_ERROR, MCT_EXTENSION_AGENT_OBJECT_ERROR_INVALID_QUERY);
  g_assert_null (reply);
}

static GVariant *
call_dbus_property_get (GDBusConnection  *connection,
                        const char       *bus_name,
                        const char       *object_path,
                        const char       *interface_name,
                        const char       *property_name,
                        GError          **error)
{
  g_autoptr(GAsyncResult) result = NULL;
  g_autoptr(GVariant) reply = NULL;

  g_dbus_connection_call (connection,
                          bus_name,
                          object_path,
                          "org.freedesktop.DBus.Properties",
                          "Get",
                          g_variant_new ("(ss)",
                                         interface_name,
                                         property_name),
                          G_VARIANT_TYPE ("(v)"),
                          G_DBUS_CALL_FLAGS_NONE,
                          G_MAXINT,  /* timeout (ms) */
                          NULL,
                          async_result_cb,
                          &result);
  while (result == NULL)
    g_main_context_iteration (NULL, TRUE);
  reply = g_dbus_connection_call_finish (connection, result, error);

  if (reply == NULL)
    return NULL;

  return g_variant_get_child_value (reply, 0);
}

static gboolean
call_dbus_property_set (GDBusConnection  *connection,
                        const char       *bus_name,
                        const char       *object_path,
                        const char       *interface_name,
                        const char       *property_name,
                        GVariant         *property_value,
                        GError          **error)
{
  g_autoptr(GVariant) owned_property_value = g_variant_ref_sink (property_value);
  g_autoptr(GAsyncResult) result = NULL;
  g_autoptr(GVariant) reply = NULL;

  g_dbus_connection_call (connection,
                          bus_name,
                          object_path,
                          "org.freedesktop.DBus.Properties",
                          "Set",
                          g_variant_new ("(ssv)",
                                         interface_name,
                                         property_name,
                                         owned_property_value),
                          NULL,
                          G_DBUS_CALL_FLAGS_NONE,
                          G_MAXINT,  /* timeout (ms) */
                          NULL,
                          async_result_cb,
                          &result);
  while (result == NULL)
    g_main_context_iteration (NULL, TRUE);
  reply = g_dbus_connection_call_finish (connection, result, error);

  return (reply != NULL);
}

static GVariant *
call_dbus_property_get_all (GDBusConnection  *connection,
                            const char       *bus_name,
                            const char       *object_path,
                            const char       *interface_name,
                            GError          **error)
{
  g_autoptr(GAsyncResult) result = NULL;
  g_autoptr(GVariant) reply = NULL;

  g_dbus_connection_call (connection,
                          bus_name,
                          object_path,
                          "org.freedesktop.DBus.Properties",
                          "GetAll",
                          g_variant_new ("(s)", interface_name),
                          NULL,
                          G_DBUS_CALL_FLAGS_NONE,
                          G_MAXINT,  /* timeout (ms) */
                          NULL,
                          async_result_cb,
                          &result);
  while (result == NULL)
    g_main_context_iteration (NULL, TRUE);
  reply = g_dbus_connection_call_finish (connection, result, error);

  if (reply == NULL)
    return NULL;

  return g_variant_get_child_value (reply, 0);
}

/* Test that calling methods on `org.freedesktop.DBus.Properties` works as
 * expected for the agent object. Currently it exposes no properties, so the
 * replies should basically all be errors.
 *
 * The object is exposed directly by the agent, so no mock D-Bus replies are
 * needed. */
static void
test_extension_agent_polkit_properties (BusFixture *fixture,
                                        const void *test_data G_GNUC_UNUSED)
{
  const struct
    {
      const char *interface_name;
      const char *property_name;
      GDBusError expected_error_code;
    }
  get_vectors[] =
    {
      {
        "org.freedesktop.MalcontentTimer1.ExtensionAgent",
        "PropertyDoesntExist",
        G_DBUS_ERROR_UNKNOWN_PROPERTY
      },
      {
        "org.freedesktop.NonExistentInterface",
        "PropertyDoesntExist",
        G_DBUS_ERROR_UNKNOWN_PROPERTY
      },
      {
        "123invalidinterface",
        "PropertyDoesntExist",
        G_DBUS_ERROR_UNKNOWN_INTERFACE
      },
      {
        "org.freedesktop.MalcontentTimer1.ExtensionAgent",
        "",
        G_DBUS_ERROR_UNKNOWN_PROPERTY
      },
    };

  for (size_t i = 0; i < G_N_ELEMENTS (get_vectors); i++)
    {
      g_autoptr(GVariant) value = NULL;
      g_autoptr(GError) local_error = NULL;

      value = call_dbus_property_get (fixture->timerd_connection,
                                      "org.freedesktop.MalcontentTimer1.ExtensionAgent",
                                      "/org/freedesktop/MalcontentTimer1/ExtensionAgent",
                                      get_vectors[i].interface_name,
                                      get_vectors[i].property_name,
                                      &local_error);
      g_assert_error (local_error, G_DBUS_ERROR, (int) get_vectors[i].expected_error_code);
      g_assert_null (value);
    }

  const struct
    {
      const char *interface_name;
      const char *property_name;
      GDBusError expected_error_code;
    }
  set_vectors[] =
    {
      {
        "org.freedesktop.MalcontentTimer1.ExtensionAgent",
        "PropertyDoesntExist",
        G_DBUS_ERROR_UNKNOWN_PROPERTY
      },
      {
        "org.freedesktop.NonExistentInterface",
        "PropertyDoesntExist",
        G_DBUS_ERROR_UNKNOWN_PROPERTY
      },
      {
        "123invalidinterface",
        "PropertyDoesntExist",
        G_DBUS_ERROR_UNKNOWN_INTERFACE
      },
      {
        "org.freedesktop.MalcontentTimer1.ExtensionAgent",
        "",
        G_DBUS_ERROR_UNKNOWN_PROPERTY
      },
    };

  for (size_t i = 0; i < G_N_ELEMENTS (set_vectors); i++)
    {
      gboolean success;
      g_autoptr(GError) local_error = NULL;

      success = call_dbus_property_set (fixture->timerd_connection,
                                        "org.freedesktop.MalcontentTimer1.ExtensionAgent",
                                        "/org/freedesktop/MalcontentTimer1/ExtensionAgent",
                                        set_vectors[i].interface_name,
                                        set_vectors[i].property_name,
                                        g_variant_new_parsed ("<'nope'>"),
                                        &local_error);
      g_assert_error (local_error, G_DBUS_ERROR, (int) set_vectors[i].expected_error_code);
      g_assert_false (success);
    }

  const struct
    {
      const char *interface_name;
      GDBusError expected_error_code;
      const char *expected_values;  /* (nullable) */
    }
  get_all_vectors[] =
    {
      {
        "org.freedesktop.NonExistentInterface",
        G_DBUS_ERROR_UNKNOWN_INTERFACE,
        NULL
      },
      {
        "123invalidinterface",
        G_DBUS_ERROR_UNKNOWN_INTERFACE,
        NULL
      },
      {
        "org.freedesktop.MalcontentTimer1.ExtensionAgent",
        0,
        "@a{sv} {}"
      },
    };

  for (size_t i = 0; i < G_N_ELEMENTS (get_all_vectors); i++)
    {
      g_autoptr(GVariant) values = NULL;
      g_autoptr(GError) local_error = NULL;

      values = call_dbus_property_get_all (fixture->timerd_connection,
                                           "org.freedesktop.MalcontentTimer1.ExtensionAgent",
                                           "/org/freedesktop/MalcontentTimer1/ExtensionAgent",
                                           get_all_vectors[i].interface_name,
                                           &local_error);

      if (get_all_vectors[i].expected_error_code != 0)
        {
          g_assert_error (local_error, G_DBUS_ERROR, (int) get_all_vectors[i].expected_error_code);
          g_assert_null (values);
        }
      else
        {
          g_assert_no_error (local_error);
          assert_cmpvariant_parsed (values, get_all_vectors[i].expected_values);
        }
    }
}

/* Test that calling methods on `org.freedesktop.DBus.Properties` works as
 * expected for a request object. Currently it exposes no properties, so the
 * replies should basically all be errors.
 *
 * The mock D-Bus replies are generated in polkit_server_cb(). */
static void
test_extension_agent_polkit_request_properties (BusFixture *fixture,
                                                const void *test_data G_GNUC_UNUSED)
{
  g_autoptr(GError) outer_error = NULL;
  g_autofree char *request_object_path = NULL;
  g_autofree char *nonexistent_request_object_path = NULL;
  const PolkitData polkit_data =
    {
      .expected_subject_uid = getuid (),
      .expected_polkit_message = "$(child_user_display_name) has requested a device screen time limit extension of $(duration_str) (until $(time_str))",
      .expected_duration_str = "1 hour",
      .expected_allow_user_interaction = FALSE,
      .result.is_authorized = TRUE,
      .result.is_challenge = FALSE,
      .result.details = "@a{ss} {}",
    };

  gt_dbus_queue_set_server_func (fixture->queue, polkit_server_cb,
                                 (void *) &polkit_data);

  /* Call the RequestExtension method on the agent to get a Request object to
   * test the properties of. We don’t care about the response, so don’t bother
   * connecting the signal for that. */
  request_object_path = extension_agent_request_extension (fixture->timerd_connection,
                                                           getuid (),
                                                           "login-session",
                                                           "",
                                                           3600,
                                                           FALSE,
                                                           &outer_error);
  g_assert_no_error (outer_error);

  nonexistent_request_object_path = g_strconcat (request_object_path, "0", NULL);
  g_assert_false (dbus_object_exists (fixture->timerd_connection, "org.freedesktop.MalcontentTimer1.ExtensionAgent", nonexistent_request_object_path));

  /* Now test the D-Bus properties of the request object */
  const struct
    {
      const char *object_path;
      const char *interface_name;
      const char *property_name;
      GDBusError expected_error_code;
    }
  get_vectors[] =
    {
      {
        request_object_path,
        "org.freedesktop.MalcontentTimer1.ExtensionAgent.Request",
        "PropertyDoesntExist",
        G_DBUS_ERROR_UNKNOWN_PROPERTY
      },
      {
        request_object_path,
        "org.freedesktop.NonExistentInterface",
        "PropertyDoesntExist",
        G_DBUS_ERROR_UNKNOWN_PROPERTY
      },
      {
        request_object_path,
        "123invalidinterface",
        "PropertyDoesntExist",
        G_DBUS_ERROR_UNKNOWN_INTERFACE
      },
      {
        request_object_path,
        "org.freedesktop.MalcontentTimer1.ExtensionAgent.Request",
        "",
        G_DBUS_ERROR_UNKNOWN_PROPERTY
      },
      {
        nonexistent_request_object_path,
        "org.freedesktop.MalcontentTimer1.ExtensionAgent.Request",
        "PropertyDoesntExist",
        /* FIXME: I think this should be G_DBUS_ERROR_UNKNOWN_OBJECT, but the
         * fallback from the (!handled) return from
         * handle_subtree_method_invocation() in GIO unconditionally returns
         * this error (https://gitlab.gnome.org/GNOME/glib/-/issues/3822): */
        G_DBUS_ERROR_UNKNOWN_METHOD
      },
    };

  for (size_t i = 0; i < G_N_ELEMENTS (get_vectors); i++)
    {
      g_autoptr(GVariant) value = NULL;
      g_autoptr(GError) local_error = NULL;

      value = call_dbus_property_get (fixture->timerd_connection,
                                      "org.freedesktop.MalcontentTimer1.ExtensionAgent",
                                      get_vectors[i].object_path,
                                      get_vectors[i].interface_name,
                                      get_vectors[i].property_name,
                                      &local_error);
      g_assert_error (local_error, G_DBUS_ERROR, (int) get_vectors[i].expected_error_code);
      g_assert_null (value);
    }

  const struct
    {
      const char *object_path;
      const char *interface_name;
      const char *property_name;
      GDBusError expected_error_code;
    }
  set_vectors[] =
    {
      {
        request_object_path,
        "org.freedesktop.MalcontentTimer1.ExtensionAgent.Request",
        "PropertyDoesntExist",
        G_DBUS_ERROR_UNKNOWN_PROPERTY
      },
      {
        request_object_path,
        "org.freedesktop.NonExistentInterface",
        "PropertyDoesntExist",
        G_DBUS_ERROR_UNKNOWN_PROPERTY
      },
      {
        request_object_path,
        "123invalidinterface",
        "PropertyDoesntExist",
        G_DBUS_ERROR_UNKNOWN_INTERFACE
      },
      {
        request_object_path,
        "org.freedesktop.MalcontentTimer1.ExtensionAgent.Request",
        "",
        G_DBUS_ERROR_UNKNOWN_PROPERTY
      },
      {
        nonexistent_request_object_path,
        "org.freedesktop.MalcontentTimer1.ExtensionAgent.Request",
        "PropertyDoesntExist",
        /* FIXME: Same issue as with Get() on an unknown object: */
        G_DBUS_ERROR_UNKNOWN_METHOD
      },
    };

  for (size_t i = 0; i < G_N_ELEMENTS (set_vectors); i++)
    {
      gboolean success;
      g_autoptr(GError) local_error = NULL;

      success = call_dbus_property_set (fixture->timerd_connection,
                                        "org.freedesktop.MalcontentTimer1.ExtensionAgent",
                                        set_vectors[i].object_path,
                                        set_vectors[i].interface_name,
                                        set_vectors[i].property_name,
                                        g_variant_new_parsed ("<'nope'>"),
                                        &local_error);
      g_assert_error (local_error, G_DBUS_ERROR, (int) set_vectors[i].expected_error_code);
      g_assert_false (success);
    }

  const struct
    {
      const char *object_path;
      const char *interface_name;
      GDBusError expected_error_code;
      const char *expected_values;  /* (nullable) */
    }
  get_all_vectors[] =
    {
      {
        request_object_path,
        "org.freedesktop.NonExistentInterface",
        G_DBUS_ERROR_UNKNOWN_INTERFACE,
        NULL
      },
      {
        request_object_path,
        "123invalidinterface",
        G_DBUS_ERROR_UNKNOWN_INTERFACE,
        NULL
      },
      {
        request_object_path,
        "org.freedesktop.MalcontentTimer1.ExtensionAgent.Request",
        0,
        "@a{sv} {}"
      },
      {
        request_object_path,
        "org.freedesktop.MalcontentTimer1.ExtensionAgent",
        G_DBUS_ERROR_UNKNOWN_INTERFACE,
        NULL
      },
      {
        nonexistent_request_object_path,
        "org.freedesktop.MalcontentTimer1.ExtensionAgent.Request",
        /* FIXME: Same issue as with Get() on an unknown object: */
        G_DBUS_ERROR_UNKNOWN_METHOD,
        NULL
      },
    };

  for (size_t i = 0; i < G_N_ELEMENTS (get_all_vectors); i++)
    {
      g_autoptr(GVariant) values = NULL;
      g_autoptr(GError) local_error = NULL;

      values = call_dbus_property_get_all (fixture->timerd_connection,
                                           "org.freedesktop.MalcontentTimer1.ExtensionAgent",
                                           get_all_vectors[i].object_path,
                                           get_all_vectors[i].interface_name,
                                           &local_error);

      if (get_all_vectors[i].expected_error_code != 0)
        {
          g_assert_error (local_error, G_DBUS_ERROR, (int) get_all_vectors[i].expected_error_code);
          g_assert_null (values);
        }
      else
        {
          g_assert_no_error (local_error);
          assert_cmpvariant_parsed (values, get_all_vectors[i].expected_values);
        }
    }

  /* Clean up */
  extension_agent_request_close (fixture->timerd_connection, request_object_path, &outer_error);
  g_assert_no_error (outer_error);
}

/* Test that a pending extension request is cancelled automatically if the
 * client which requested it drops off the bus.
 *
 * The mock D-Bus replies are generated inline for clarity. */
static void
test_extension_agent_polkit_request_error_cancelled (BusFixture *fixture,
                                                     const void *test_data G_GNUC_UNUSED)
{
  unsigned int signal_id = 0;
  g_autoptr(GVariant) response_variant = NULL;
  g_autoptr(GAsyncResult) request_extension_result = NULL;
  g_autoptr(GAsyncResult) close_result = NULL;
  const uid_t subject_uid = getuid ();
  g_autoptr(GDBusMethodInvocation) check_authorization_invocation = NULL;
  g_autoptr(GDBusMethodInvocation) cancel_check_authorization_invocation = NULL;
  const char *expected_cancellation_id, *cancellation_id;
  g_autoptr(GError) local_error = NULL;

  /* Call the RequestExtension method on the agent. We need to use a separate
   * D-Bus connection so we call from a different unique name from the one
   * which registered the Agent object.
   *
   * Connect to signals from requests first, to avoid race conditions. */
  signal_id = extension_agent_subscribe_responses (fixture->timerd_connection,
                                                   &response_variant);

  g_dbus_connection_call (fixture->timerd_connection,
                          "org.freedesktop.MalcontentTimer1.ExtensionAgent",
                          "/org/freedesktop/MalcontentTimer1/ExtensionAgent",
                          "org.freedesktop.MalcontentTimer1.ExtensionAgent",
                          "RequestExtension",
                          g_variant_new ("(sst(s@a{sv})@a{sv})",
                                         "login-session",
                                         "",
                                         3600,
                                         "unix-process",
                                         g_variant_new_parsed ("@a{sv} { 'pidfd': <@h 0>, 'uid': <@i %i> }", subject_uid),
                                         g_variant_new_parsed ("@a{sv} {}")),
                          G_VARIANT_TYPE ("(o)"),
                          G_DBUS_CALL_FLAGS_NONE,
                          G_MAXINT,  /* timeout (ms) */
                          NULL,
                          async_result_cb,
                          &request_extension_result);

  /* Handle the CheckAuthorization() call. */
  check_authorization_invocation =
      gt_dbus_queue_assert_pop_message (fixture->queue,
                                        "/org/freedesktop/PolicyKit1/Authority",
                                        "org.freedesktop.PolicyKit1.Authority",
                                        "CheckAuthorization",
                                        "(@r&sa{ss}u&s)",
                                        NULL,
                                        NULL,
                                        NULL,
                                        NULL,
                                        &expected_cancellation_id);

  g_assert_cmpstr (expected_cancellation_id, !=, "");

  /* Before anything else happens, drop off the bus (as if the timerd has
   * crashed). The agent should gracefully cancel the pending authorisation
   * request. */
  g_dbus_connection_close (fixture->timerd_connection, NULL, async_result_cb, &close_result);
  while (close_result == NULL)
    g_main_context_iteration (NULL, TRUE);
  g_dbus_connection_close_finish (fixture->timerd_connection, close_result, &local_error);
  g_assert_no_error (local_error);
  g_clear_object (&fixture->timerd_connection);
  (void) signal_id;  /* can’t actually explicitly disonnect this, but it has just been disconnected */
  signal_id = 0;

  /* So we expect a cancellation call to polkit from the agent. */
  cancel_check_authorization_invocation =
      gt_dbus_queue_assert_pop_message (fixture->queue,
                                        "/org/freedesktop/PolicyKit1/Authority",
                                        "org.freedesktop.PolicyKit1.Authority",
                                        "CancelCheckAuthorization",
                                        "(&s)",
                                        &cancellation_id);

  g_assert_cmpstr (cancellation_id, ==, expected_cancellation_id);

  g_dbus_method_invocation_return_dbus_error (check_authorization_invocation,
                                              "org.freedesktop.PolicyKit1.Error.Cancelled",
                                              "Authorization was cancelled");
  g_dbus_method_invocation_return_value (cancel_check_authorization_invocation, NULL);

  /* Clear out the async result for the (now cancelled) D-Bus call. */
  while (request_extension_result == NULL)
    g_main_context_iteration (NULL, TRUE);
  g_clear_object (&request_extension_result);

  /* We shouldn’t have received a response signal. */
  g_assert_null (response_variant);
}

static void
request_response_queue_cb (GDBusConnection *connection,
                           const char      *sender_name,
                           const char      *object_path,
                           const char      *interface_name,
                           const char      *signal_name,
                           GVariant        *parameters,
                           void            *user_data)
{
  GQueue *queue = user_data;

  /* This is the extension agent’s unique name, which we don’t easily have
   * access to in this callback: */
  g_assert_cmpstr (sender_name, !=, "");

  g_assert_true (g_str_has_prefix (object_path, "/org/freedesktop/MalcontentTimer1/ExtensionAgent/Request"));
  g_assert_cmpstr (interface_name, ==, "org.freedesktop.MalcontentTimer1.ExtensionAgent.Request");
  g_assert_cmpstr (signal_name, ==, "Response");

  g_queue_push_tail (queue, g_variant_ref (parameters));
  g_main_context_wakeup (NULL);
}

static void
assert_pop_request_response_queue (GQueue     *queue,
                                   const char *expected_response_str)
{
  g_autoptr(GVariant) response = NULL;

  /* Wait for the Response signal. A queue item is pushed in request_response_queue_cb(). */
  while (g_queue_is_empty (queue))
    g_main_context_iteration (NULL, TRUE);

  response = g_queue_pop_head (queue);
  g_assert_nonnull (response);
  assert_cmpvariant_parsed (response, expected_response_str);
}

/* Test that the agent can handle requests from multiple clients simultaneously,
 * some in parallel and some overlapping.
 *
 * The mock D-Bus replies are generated inline for clarity. */
static void
test_extension_agent_polkit_request_multiplexing (BusFixture *fixture,
                                                  const void *test_data G_GNUC_UNUSED)
{
  g_autoptr(GError) local_error = NULL;
  g_autoptr(GDBusConnection) client1_connection = NULL;
  g_autoptr(GDBusConnection) client2_connection = NULL;
  unsigned int client1_signal_id = 0, client2_signal_id = 0;
  g_autoptr(GQueue) client1_request_response_queue = NULL;
  g_autoptr(GQueue) client2_request_response_queue = NULL;
  g_autofree char *client1_request1_object_path = NULL;
  g_autofree char *client1_request2_object_path = NULL;
  g_autofree char *client1_request3_object_path = NULL;
  g_autofree char *client2_request1_object_path = NULL;
  g_autoptr(GDBusMethodInvocation) client1_request1_invocation = NULL;
  g_autoptr(GDBusMethodInvocation) client1_request2_invocation = NULL;
  g_autoptr(GDBusMethodInvocation) client1_request3_invocation = NULL;
  g_autoptr(GDBusMethodInvocation) client2_request1_invocation = NULL;
  g_autoptr(GVariant) details = NULL;
  const char *duration_str = NULL;

  client1_connection = g_object_ref (fixture->timerd_connection);
  client2_connection = dbus_queue_open_additional_client_connection ();

  /* Connect to signals from requests first, to avoid race conditions. We can’t
   * use extension_agent_subscribe_responses() here as we need a queue. */
  client1_request_response_queue = g_queue_new ();
  client2_request_response_queue = g_queue_new ();

  client1_signal_id =
      g_dbus_connection_signal_subscribe (client1_connection,
                                          "org.freedesktop.MalcontentTimer1.ExtensionAgent",
                                          "org.freedesktop.MalcontentTimer1.ExtensionAgent.Request",
                                          "Response",
                                          NULL,  /* object path */
                                          NULL,  /* arg0 */
                                          G_DBUS_SIGNAL_FLAGS_NONE,
                                          request_response_queue_cb,
                                          client1_request_response_queue,
                                          NULL);
  client2_signal_id =
      g_dbus_connection_signal_subscribe (client2_connection,
                                          "org.freedesktop.MalcontentTimer1.ExtensionAgent",
                                          "org.freedesktop.MalcontentTimer1.ExtensionAgent.Request",
                                          "Response",
                                          NULL,  /* object path */
                                          NULL,  /* arg0 */
                                          G_DBUS_SIGNAL_FLAGS_NONE,
                                          request_response_queue_cb,
                                          client2_request_response_queue,
                                          NULL);

  /* Make a request from client 1 */
  client1_request1_object_path = extension_agent_request_extension (client1_connection,
                                                                    getuid (),
                                                                    "login-session",
                                                                    "",
                                                                    3600,
                                                                    FALSE,
                                                                    &local_error);
  g_assert_no_error (local_error);

  /* Handle the polkit call for client 1’s request */
  client1_request1_invocation =
      gt_dbus_queue_assert_pop_message (fixture->queue,
                                        "/org/freedesktop/PolicyKit1/Authority",
                                        "org.freedesktop.PolicyKit1.Authority",
                                        "CheckAuthorization",
                                        "(@r&s@a{ss}u&s)",
                                        NULL,
                                        NULL,
                                        &details,
                                        NULL,
                                        NULL);
  g_assert_true (g_variant_lookup (details, "duration_str", "&s", &duration_str));
  g_assert_cmpstr (duration_str, ==, "1 hour");
  g_clear_pointer (&details, g_variant_unref);

  g_dbus_method_invocation_return_value (client1_request1_invocation,
                                         g_variant_new_parsed ("((true, false, {'id-for-tests': '1'}),)"));

  /* Make a request from client 2 */
  client2_request1_object_path = extension_agent_request_extension (client2_connection,
                                                                    getuid (),
                                                                    "login-session",
                                                                    "",
                                                                    1800,
                                                                    FALSE,
                                                                    &local_error);
  g_assert_no_error (local_error);

  /* Close client 1’s request now it’s complete */
  assert_pop_request_response_queue (client1_request_response_queue, "(true, @a{sv} {})");
  extension_agent_request_close (client1_connection, client1_request1_object_path, &local_error);
  g_assert_no_error (local_error);

  /* Make another two requests from client 1 */
  client1_request2_object_path = extension_agent_request_extension (client1_connection,
                                                                    getuid (),
                                                                    "app",
                                                                    "org.freedesktop.Malcontent.TestApp",
                                                                    1000,
                                                                    FALSE,
                                                                    &local_error);
  g_assert_no_error (local_error);

  client1_request3_object_path = extension_agent_request_extension (client1_connection,
                                                                    getuid (),
                                                                    "login-session",
                                                                    "",
                                                                    7200,
                                                                    FALSE,
                                                                    &local_error);
  g_assert_no_error (local_error);

  /* Handle the polkit call for client 2’s first request */
  client2_request1_invocation =
      gt_dbus_queue_assert_pop_message (fixture->queue,
                                        "/org/freedesktop/PolicyKit1/Authority",
                                        "org.freedesktop.PolicyKit1.Authority",
                                        "CheckAuthorization",
                                        "(@r&s@a{ss}u&s)",
                                        NULL,
                                        NULL,
                                        &details,
                                        NULL,
                                        NULL);
  g_assert_true (g_variant_lookup (details, "duration_str", "&s", &duration_str));
  g_assert_cmpstr (duration_str, ==, "30 minutes");
  g_clear_pointer (&details, g_variant_unref);

  g_dbus_method_invocation_return_value (client2_request1_invocation,
                                         g_variant_new_parsed ("((true, false, {'id-for-tests': '2'}),)"));

  /* Handle the polkit call for client 1’s second request */
  client1_request2_invocation =
      gt_dbus_queue_assert_pop_message (fixture->queue,
                                        "/org/freedesktop/PolicyKit1/Authority",
                                        "org.freedesktop.PolicyKit1.Authority",
                                        "CheckAuthorization",
                                        "(@r&s@a{ss}u&s)",
                                        NULL,
                                        NULL,
                                        &details,
                                        NULL,
                                        NULL);
  g_assert_true (g_variant_lookup (details, "duration_str", "&s", &duration_str));
  g_assert_cmpstr (duration_str, ==, "16 minutes 40 seconds");
  g_clear_pointer (&details, g_variant_unref);

  g_dbus_method_invocation_return_value (client1_request2_invocation,
                                         g_variant_new_parsed ("((true, false, {'id-for-tests': '3'}),)"));

  /* Close client 1’s second request */
  assert_pop_request_response_queue (client1_request_response_queue, "(true, @a{sv} {})");
  extension_agent_request_close (client1_connection, client1_request2_object_path, &local_error);
  g_assert_no_error (local_error);

  /* Handle the polkit call for client 1’s third request */
  client1_request3_invocation =
      gt_dbus_queue_assert_pop_message (fixture->queue,
                                        "/org/freedesktop/PolicyKit1/Authority",
                                        "org.freedesktop.PolicyKit1.Authority",
                                        "CheckAuthorization",
                                        "(@r&s@a{ss}u&s)",
                                        NULL,
                                        NULL,
                                        &details,
                                        NULL,
                                        NULL);
  g_assert_true (g_variant_lookup (details, "duration_str", "&s", &duration_str));
  g_assert_cmpstr (duration_str, ==, "2 hours");
  g_clear_pointer (&details, g_variant_unref);

  g_dbus_method_invocation_return_value (client1_request3_invocation,
                                         g_variant_new_parsed ("((true, false, {'id-for-tests': '4'}),)"));

  /* Close the final remaining client 1 request */
  assert_pop_request_response_queue (client1_request_response_queue, "(true, @a{sv} {})");
  extension_agent_request_close (client1_connection, client1_request3_object_path, &local_error);
  g_assert_no_error (local_error);

  /* Client 1 disappears off the bus */
  g_assert_false (dbus_object_exists (client1_connection, "org.freedesktop.MalcontentTimer1.ExtensionAgent", client1_request1_object_path));
  g_assert_false (dbus_object_exists (client1_connection, "org.freedesktop.MalcontentTimer1.ExtensionAgent", client1_request2_object_path));
  g_assert_false (dbus_object_exists (client1_connection, "org.freedesktop.MalcontentTimer1.ExtensionAgent", client1_request3_object_path));

  g_assert_true (g_queue_is_empty (client1_request_response_queue));
  g_dbus_connection_signal_unsubscribe (client1_connection, client1_signal_id);
  client1_signal_id = 0;

  g_dbus_connection_close_sync (client1_connection, NULL, NULL);
  g_clear_object (&client1_connection);

  /* Close the remaining client 2 request */
  assert_pop_request_response_queue (client2_request_response_queue, "(true, @a{sv} {})");
  extension_agent_request_close (client2_connection, client2_request1_object_path, &local_error);
  g_assert_no_error (local_error);

  /* The agent should no longer be busy, and none of the Request objects should
   * exist any longer. */
  g_assert_false (mct_extension_agent_object_get_busy (MCT_EXTENSION_AGENT_OBJECT (fixture->extension_agent)));
  g_assert_false (dbus_object_exists (client2_connection, "org.freedesktop.MalcontentTimer1.ExtensionAgent", client2_request1_object_path));

  /* Clean up. */
  g_assert_true (g_queue_is_empty (client2_request_response_queue));
  g_dbus_connection_signal_unsubscribe (client2_connection, client2_signal_id);
  client2_signal_id = 0;
}

/* Test that calling Close() on a request belonging to a different D-Bus
 * connection is disallowed.
 *
 * The mock D-Bus replies are generated inline for simplicity. */
static void
test_extension_agent_polkit_request_close_unowned (BusFixture *fixture,
                                                   const void *test_data G_GNUC_UNUSED)
{
  g_autoptr(GError) local_error = NULL;
  g_autoptr(GDBusConnection) client1_connection = NULL;
  g_autoptr(GDBusConnection) client2_connection = NULL;
  g_autofree char *client1_request_object_path = NULL;
  g_autoptr(GDBusMethodInvocation) client1_request_invocation = NULL;

  client1_connection = g_object_ref (fixture->timerd_connection);
  client2_connection = dbus_queue_open_additional_client_connection ();

  /* Make a request from client 1 */
  client1_request_object_path = extension_agent_request_extension (client1_connection,
                                                                   getuid (),
                                                                   "login-session",
                                                                   "",
                                                                   3600,
                                                                   FALSE,
                                                                   &local_error);
  g_assert_no_error (local_error);

  /* Handle the polkit call for client 1’s request */
  client1_request_invocation =
      gt_dbus_queue_assert_pop_message (fixture->queue,
                                        "/org/freedesktop/PolicyKit1/Authority",
                                        "org.freedesktop.PolicyKit1.Authority",
                                        "CheckAuthorization",
                                        "(@r&s@a{ss}u&s)",
                                        NULL,
                                        NULL,
                                        NULL,
                                        NULL,
                                        NULL);
  g_dbus_method_invocation_return_value (client1_request_invocation,
                                         g_variant_new_parsed ("((true, false, @a{ss} {}),)"));

  /* Try and close client 1’s request from client 2 */
  extension_agent_request_close (client2_connection, client1_request_object_path, &local_error);
  g_assert_error (local_error, G_DBUS_ERROR, G_DBUS_ERROR_UNKNOWN_OBJECT);
  g_clear_error (&local_error);

  /* Actually close client 1’s request from client 1 */
  extension_agent_request_close (client1_connection, client1_request_object_path, &local_error);
  g_assert_no_error (local_error);

  /* The agent should no longer be busy, and none of the Request objects should
   * exist any longer. */
  g_assert_false (mct_extension_agent_object_get_busy (MCT_EXTENSION_AGENT_OBJECT (fixture->extension_agent)));
  g_assert_false (dbus_object_exists (client1_connection, "org.freedesktop.MalcontentTimer1.ExtensionAgent", client1_request_object_path));
}

/* Get the UID of a user with no GECOS field in `/etc/passwd`, so we can test
 * fallback code paths in the agent for handling that field being missing.
 *
 * On Fedora systems, the `gdm` user seems to have no GECOS data, so use that
 * if possible. Otherwise, we’ll just have to use a user with some GECOS data
 * and pass the test trivially.
 *
 * We could improve on this by mocking getpwuid_r(), but that’s more work for
 * now. */
static uid_t
get_uid_with_no_gecos (void)
{
  char buffer[4096];
  struct passwd pwbuf;
  struct passwd *result;
  int pwnam_errno;

  pwnam_errno = getpwnam_r ("gdm", &pwbuf, buffer, sizeof (buffer), &result);
  if (pwnam_errno == 0 && result != NULL && (result->pw_gecos == NULL || result->pw_gecos[0] == '\0'))
    return result->pw_uid;

  return getuid ();
}

int
main (int    argc,
      char **argv)
{
  setlocale (LC_ALL, "");
  g_test_init (&argc, &argv, G_TEST_OPTION_ISOLATE_DIRS, NULL);

  g_test_add ("/extension-agent-polkit/construction", BusFixture, NULL,
              bus_set_up, test_extension_agent_polkit_construction, bus_tear_down);

  const SuccessfulRequestData successful_request_datas[] =
    {
      {
        .subject_uid = getuid (),
        .record_type = "login-session",
        .identifier = "",
        .duration_secs = 3600,
        .expected_polkit_message = "$(child_user_display_name) has requested a device screen time limit extension of $(duration_str) (until $(time_str))",
        .expected_duration_str = "1 hour",
      },
      {
        .subject_uid = getuid (),
        .record_type = "login-session",
        .identifier = "",
        .duration_secs = 0,
        .expected_polkit_message = "$(child_user_display_name) has requested a device screen time limit extension until the end of today (at $(time_str))",
      },
      {
        .subject_uid = getuid (),
        .record_type = "app",
        .identifier = "org.freedesktop.Malcontent.TestApp",
        .duration_secs = 3600,
        .expected_polkit_message = "$(child_user_display_name) has requested an app screen time limit extension of $(duration_str) (until $(time_str)) for $(app_name)",
        .expected_duration_str = "1 hour",
        .expected_app_name = "Test Application",
      },
      {
        .subject_uid = getuid (),
        .record_type = "app",
        .identifier = "org.freedesktop.Malcontent.TestApp",
        .duration_secs = 0,
        .expected_polkit_message = "$(child_user_display_name) has requested an app screen time limit extension until the end of today (at $(time_str)) for $(app_name)",
        .expected_app_name = "Test Application",
      },
      {
        .subject_uid = getuid (),
        .record_type = "app",
        .identifier = "org.gnome.Unknown",
        .duration_secs = 3600,
        .expected_polkit_message = "$(child_user_display_name) has requested an app screen time limit extension of $(duration_str) (until $(time_str)) for $(app_name)",
        .expected_duration_str = "1 hour",
        .expected_app_name = "org.gnome.Unknown",
      },
      {
        .subject_uid = getuid (),
        .record_type = "login-session",
        .identifier = "",
        .duration_secs = 1,
        .expected_polkit_message = "$(child_user_display_name) has requested a device screen time limit extension of $(duration_str) (until $(time_str))",
        .expected_duration_str = "1 second",
      },
      {
        .subject_uid = getuid (),
        .record_type = "login-session",
        .identifier = "",
        .duration_secs = 2,
        .expected_polkit_message = "$(child_user_display_name) has requested a device screen time limit extension of $(duration_str) (until $(time_str))",
        .expected_duration_str = "2 seconds",
      },
      {
        .subject_uid = getuid (),
        .record_type = "login-session",
        .identifier = "",
        .duration_secs = 60,
        .expected_polkit_message = "$(child_user_display_name) has requested a device screen time limit extension of $(duration_str) (until $(time_str))",
        .expected_duration_str = "1 minute",
      },
      {
        .subject_uid = getuid (),
        .record_type = "login-session",
        .identifier = "",
        .duration_secs = 120,
        .expected_polkit_message = "$(child_user_display_name) has requested a device screen time limit extension of $(duration_str) (until $(time_str))",
        .expected_duration_str = "2 minutes",
      },
      {
        .subject_uid = getuid (),
        .record_type = "login-session",
        .identifier = "",
        .duration_secs = 65,
        .expected_polkit_message = "$(child_user_display_name) has requested a device screen time limit extension of $(duration_str) (until $(time_str))",
        .expected_duration_str = "1 minute 5 seconds",
      },
      {
        .subject_uid = getuid (),
        .record_type = "login-session",
        .identifier = "",
        .duration_secs = 7200,
        .expected_polkit_message = "$(child_user_display_name) has requested a device screen time limit extension of $(duration_str) (until $(time_str))",
        .expected_duration_str = "2 hours",
      },
      {
        .subject_uid = getuid (),
        .record_type = "login-session",
        .identifier = "",
        .duration_secs = 3660,
        .expected_polkit_message = "$(child_user_display_name) has requested a device screen time limit extension of $(duration_str) (until $(time_str))",
        .expected_duration_str = "1 hour 1 minute",
      },
      {
        .subject_uid = getuid (),
        .record_type = "login-session",
        .identifier = "",
        .duration_secs = 3666,
        .expected_polkit_message = "$(child_user_display_name) has requested a device screen time limit extension of $(duration_str) (until $(time_str))",
        .expected_duration_str = "1 hour 1 minute 6 seconds",
      },
      {
        .subject_uid = get_uid_with_no_gecos (),
        .record_type = "login-session",
        .identifier = "",
        .duration_secs = 3600,
        .expected_polkit_message = "$(child_user_display_name) has requested a device screen time limit extension of $(duration_str) (until $(time_str))",
        .expected_duration_str = "1 hour",
      },
    };

  for (size_t i = 0; i < G_N_ELEMENTS (successful_request_datas); i++)
    {
      g_autofree char *test_path = g_strdup_printf ("/extension-agent-polkit/request/error/successful/%" G_GSIZE_FORMAT, i);
      g_test_add (test_path, BusFixture, &successful_request_datas[i],
                  bus_set_up, test_extension_agent_polkit_request_successful, bus_tear_down);
    }

  const PolkitErrorPair polkit_error_pairs[] =
    {
      { "org.freedesktop.PolicyKit1.Error.Failed",
        "org.freedesktop.MalcontentTimer1.ExtensionAgent.Error.Failed" },
      { "org.freedesktop.PolicyKit1.Error.NotSupported",
        "org.freedesktop.MalcontentTimer1.ExtensionAgent.Error.Failed" },
      { "org.freedesktop.PolicyKit1.Error.CancellationIdNotUnique",
        "org.freedesktop.MalcontentTimer1.ExtensionAgent.Error.Failed" },
      { "org.freedesktop.PolicyKit1.Error.Cancelled",
        "org.freedesktop.MalcontentTimer1.ExtensionAgent.Error.Cancelled" },
      { "org.freedesktop.PolicyKit1.Error.MadeUpDoesntExist",
        "org.freedesktop.MalcontentTimer1.ExtensionAgent.Error.Failed" },
    };
  for (size_t i = 0; i < G_N_ELEMENTS (polkit_error_pairs); i++)
    {
      g_autofree char *test_path = g_strdup_printf ("/extension-agent-polkit/request/error/polkit/%" G_GSIZE_FORMAT, i);
      g_test_add (test_path, BusFixture, &polkit_error_pairs[i],
                  bus_set_up, test_extension_agent_polkit_request_error_polkit, bus_tear_down);
    }

  g_test_add ("/extension-agent-polkit/request/error/user-non-existent", BusFixture, NULL,
              bus_set_up, test_extension_agent_polkit_request_error_user_non_existent, bus_tear_down);

  const ValidationData validation_datas[] =
    {
      {
        .record_type = "invalid",
        .identifier = "",
        .subject_kind = "unix-process",
        .subject_details = "@a{sv} { 'pidfd': <@h 0>, 'uid': <@i 1000> }",
      },
      {
        .record_type = "login-session",
        .identifier = "invalid",
        .subject_kind = "unix-process",
        .subject_details = "@a{sv} { 'pidfd': <@h 0>, 'uid': <@i 1000> }",
      },
      {
        .record_type = "app",
        .identifier = "",
        .subject_kind = "unix-process",
        .subject_details = "@a{sv} { 'pidfd': <@h 0>, 'uid': <@i 1000> }",
      },
      {
        .record_type = "login-session",
        .identifier = "",
        .subject_kind = "invalid",
        .subject_details = "@a{sv} { 'pidfd': <@h 0>, 'uid': <@i 1000> }",
      },
      {
        .record_type = "login-session",
        .identifier = "",
        .subject_kind = "unix-process",
        .subject_details = "@a{sv} { 'pidfd': <@h 0>, 'uid': <@s 'invald'> }",
      },
      {
        .record_type = "login-session",
        .identifier = "",
        .subject_kind = "unix-process",
        .subject_details = "@a{sv} { 'pidfd': <@h 0> }",
      },
      {
        .record_type = "login-session",
        .identifier = "",
        .subject_kind = "unix-process",
        .subject_details = "@a{sv} { 'uid': <@i 1000> }",
      },
      {
        .record_type = "login-session",
        .identifier = "",
        .subject_kind = "unix-process",
        .subject_details = "@a{sv} { 'pidfd': <@s 'invalid'>, 'uid': <@i 1000> }",
      },
      {
        .record_type = "login-session",
        .identifier = "",
        .subject_kind = "unix-process",
        .subject_details = "@a{sv} {}",
      },
    };
  for (size_t i = 0; i < G_N_ELEMENTS (validation_datas); i++)
    {
      g_autofree char *test_path = g_strdup_printf ("/extension-agent-polkit/request/error/validation/%" G_GSIZE_FORMAT, i);
      g_test_add (test_path, BusFixture, &validation_datas[i],
                  bus_set_up, test_extension_agent_polkit_request_error_validation, bus_tear_down);
    }

  g_test_add ("/extension-agent-polkit/properties", BusFixture, NULL,
              bus_set_up, test_extension_agent_polkit_properties, bus_tear_down);
  g_test_add ("/extension-agent-polkit/request/properties", BusFixture, NULL,
              bus_set_up, test_extension_agent_polkit_request_properties, bus_tear_down);
  g_test_add ("/extension-agent-polkit/request/error/cancelled", BusFixture, NULL,
              bus_set_up, test_extension_agent_polkit_request_error_cancelled, bus_tear_down);
  g_test_add ("/extension-agent-polkit/request/multiplexing", BusFixture, NULL,
              bus_set_up, test_extension_agent_polkit_request_multiplexing, bus_tear_down);
  g_test_add ("/extension-agent-polkit/request/close-unowned", BusFixture, NULL,
              bus_set_up, test_extension_agent_polkit_request_close_unowned, bus_tear_down);

  return g_test_run ();
}
