/* Regression test for object registration and unregistration
 *
 * Copyright © 2009 Collabora Ltd. <http://www.collabora.co.uk/>
 * Copyright © 2009-2011 Nokia Corporation
 *
 * SPDX-License-Identifier: AFL-2.1 OR GPL-2.0-or-later OR MIT
 *
 * In preparation for dbus-glib relicensing (if it ever happens), this file is
 * licensed under (at your option) either the AFL v2.1, the GPL v2 or later,
 * or an MIT/X11-style license:
 *
 * Licensed under the Academic Free License version 2.1
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * Permission is hereby granted, free of charge, to any person
 * obtaining a copy of this software and associated documentation
 * files (the "Software"), to deal in the Software without
 * restriction, including without limitation the rights to use, copy,
 * modify, merge, publish, distribute, sublicense, and/or sell copies
 * of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be
 * included in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
 * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
 * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
 * DEALINGS IN THE SOFTWARE.
 */

#include <config.h>
#undef G_DISABLE_ASSERT

#include <dbus/dbus-glib.h>
#include <dbus/dbus-glib-lowlevel.h>

#include "my-object.h"

#include "dbus-gmain/tests/util.h"

GMainLoop *loop = NULL;

typedef struct {
    DBusError dbus_error;
    DBusGConnection *bus;
    DBusGConnection *bus2;
    GObject *object;
    DBusMessage *frobnicate1_message;
    DBusMessage *frobnicate2_message;
    gboolean received_objectified;
} Fixture;

#define assert_no_error(e) _assert_no_error (e, __FILE__, __LINE__)
static void
_assert_no_error (const DBusError *e,
    const char *file,
    int line)
{
  if (G_UNLIKELY (dbus_error_is_set (e)))
    g_error ("%s:%d: expected success but got error: %s: %s",
        file, line, e->name, e->message);
}

static void
setup (Fixture *f,
    gconstpointer path_to_use)
{
  dbus_error_init (&f->dbus_error);

  f->bus = dbus_g_bus_get_private (DBUS_BUS_SESSION, NULL, NULL);
  g_assert_nonnull (f->bus);

  f->bus2 = dbus_g_bus_get_private (DBUS_BUS_SESSION, NULL, NULL);
  g_assert_nonnull (f->bus2);

  f->object = g_object_new (MY_TYPE_OBJECT, NULL);
  g_assert_true (MY_IS_OBJECT (f->object));
}

static void
teardown (Fixture *f,
    gconstpointer test_data G_GNUC_UNUSED)
{
  /* we close the connection before releasing the object, to test fd.o #5688
   * in test_lookup() */
  if (f->bus != NULL)
    {
      test_run_until_disconnected (dbus_g_connection_get_connection (f->bus), NULL);
      dbus_g_connection_unref (f->bus);
    }

  if (f->bus2 != NULL)
    {
      test_run_until_disconnected (dbus_g_connection_get_connection (f->bus2), NULL);
      dbus_g_connection_unref (f->bus2);
    }

  if (f->object != NULL)
    {
      g_object_unref (f->object);
    }

  /* This is safe to call on an initialized-but-unset DBusError, a bit like
   * g_clear_error */
  dbus_error_free (&f->dbus_error);

  dbus_shutdown ();
}

static void
test_lookup (Fixture *f,
    gconstpointer test_data G_GNUC_UNUSED)
{
  /* teardown() closes the connection before f->object is destroyed, which
   * used to be broken */
  g_test_bug ("5688");

  dbus_g_connection_register_g_object (f->bus, "/foo", f->object);
  g_assert_true (dbus_g_connection_lookup_g_object (f->bus, "/foo") ==
      f->object);
  /* this was briefly broken while fixing fd.o#5688 */
  g_assert_null (dbus_g_connection_lookup_g_object (f->bus, "/bar"));
}

static void
test_unregister (Fixture *f,
    gconstpointer test_data G_GNUC_UNUSED)
{
  /* feature test: objects can be unregistered */
  g_test_bug ("21219");

  dbus_g_connection_register_g_object (f->bus, "/foo", f->object);
  g_assert_true (dbus_g_connection_lookup_g_object (f->bus, "/foo") ==
      f->object);
  dbus_g_connection_unregister_g_object (f->bus, f->object);
  g_assert_null (dbus_g_connection_lookup_g_object (f->bus, "/foo"));
}

static void
test_unregister_on_last_unref (Fixture *f,
    gconstpointer test_data G_GNUC_UNUSED)
{
  gpointer weak_pointer;

  weak_pointer = f->object;
  g_object_add_weak_pointer (weak_pointer, &weak_pointer);

  dbus_g_connection_register_g_object (f->bus, "/foo", f->object);
  g_assert_true (dbus_g_connection_lookup_g_object (f->bus, "/foo") ==
      f->object);
  /* implicit unregistration by the last-unref of the object */
  g_object_unref (f->object);
  f->object = NULL;

  g_assert_null (weak_pointer);

  g_assert_null (dbus_g_connection_lookup_g_object (f->bus, "/foo"));
}

static void
test_unregister_on_forced_dispose (Fixture *f,
    gconstpointer test_data G_GNUC_UNUSED)
{
  dbus_g_connection_register_g_object (f->bus, "/foo", f->object);
  g_assert_true (dbus_g_connection_lookup_g_object (f->bus, "/foo") ==
      f->object);
  /* implicit unregistration by dispose() of the object (don't try
   * this at home) */
  g_object_run_dispose (f->object);

  g_assert_null (dbus_g_connection_lookup_g_object (f->bus, "/foo"));
}

static void
test_reregister (Fixture *f,
    gconstpointer test_data G_GNUC_UNUSED)
{
  dbus_g_connection_register_g_object (f->bus, "/foo", f->object);
  g_assert_true (dbus_g_connection_lookup_g_object (f->bus, "/foo") ==
      f->object);

  /* Before 0.82, re-registering the same object path was leaky but successful.
   * 0.82 disallowed this behaviour. Since 0.84 it was meant to be allowed
   * again, and a no-op, but it actually had the effect of removing all
   * record of the registrations (while leaving the object registered with
   * libdbus). */
  dbus_g_connection_register_g_object (f->bus, "/foo", f->object);
  g_assert_true (dbus_g_connection_lookup_g_object (f->bus, "/foo") ==
      f->object);

  /* This would critical in 0.84. */
  dbus_g_connection_unregister_g_object (f->bus, f->object);
  g_assert_null (dbus_g_connection_lookup_g_object (f->bus, "/foo"));
}

static DBusHandlerResult
frobnicate_cb (DBusConnection *conn,
    DBusMessage *message,
    void *user_data)
{
  Fixture *f = user_data;

  if (dbus_message_is_signal (message,
        "org.freedesktop.DBus.GLib.Tests.MyObject", "Frobnicate"))
    {
      const char *sender = dbus_message_get_sender (message);
      const char *path = dbus_message_get_path (message);

      g_assert_nonnull (sender);
      g_assert_nonnull (path);

      if (g_strcmp0 (path, "/foo") == 0)
        {
          g_assert_cmpstr (sender, ==, dbus_bus_get_unique_name (
                dbus_g_connection_get_connection (f->bus)));

          g_assert_null (f->frobnicate1_message);
          f->frobnicate1_message = dbus_message_ref (message);
        }
      else
        {
          g_assert_cmpstr (path, ==, "/bar");
          g_assert_cmpstr (sender, ==, dbus_bus_get_unique_name (
                dbus_g_connection_get_connection (f->bus2)));

          g_assert_null (f->frobnicate2_message);
          f->frobnicate2_message = dbus_message_ref (message);
        }
    }

  return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
}

static void
test_twice (Fixture *f,
    gconstpointer test_data G_GNUC_UNUSED)
{
  dbus_bool_t mem;

  g_test_bug ("32087");

  dbus_g_connection_register_g_object (f->bus, "/foo", f->object);

  dbus_g_connection_register_g_object (f->bus2, "/bar", f->object);

  g_assert_true (dbus_g_connection_lookup_g_object (f->bus, "/foo") ==
      f->object);
  g_assert_true (dbus_g_connection_lookup_g_object (f->bus2, "/bar") ==
      f->object);

  dbus_bus_add_match (dbus_g_connection_get_connection (f->bus),
      "type='signal'", &f->dbus_error);
  assert_no_error (&f->dbus_error);
  mem = dbus_connection_add_filter (dbus_g_connection_get_connection (f->bus),
      frobnicate_cb, f, NULL);
  g_assert_true (mem);

  my_object_emit_frobnicate ((MyObject *) f->object, NULL);

  /* The object should emit the signal once onto each connection,
   * from the appropriate location */
  while (f->frobnicate1_message == NULL || f->frobnicate2_message == NULL)
    g_main_context_iteration (NULL, TRUE);

  dbus_message_unref (f->frobnicate1_message);
  f->frobnicate1_message = NULL;
  dbus_message_unref (f->frobnicate2_message);
  f->frobnicate2_message = NULL;

  /* try again, to catch any extra emissions, but first unregister one of the
   * object's locations */
  dbus_g_connection_unregister_g_object (f->bus, f->object);
  my_object_emit_frobnicate ((MyObject *) f->object, NULL);

  while (f->frobnicate2_message == NULL)
    g_main_context_iteration (NULL, TRUE);

  g_assert_null (f->frobnicate1_message);
  dbus_message_unref (f->frobnicate2_message);
  f->frobnicate2_message = NULL;
}

static void
test_clean_slate (Fixture *f,
    gconstpointer test_data G_GNUC_UNUSED)
{
  dbus_bool_t mem;

  dbus_bus_add_match (dbus_g_connection_get_connection (f->bus),
      "type='signal'", &f->dbus_error);
  assert_no_error (&f->dbus_error);
  mem = dbus_connection_add_filter (dbus_g_connection_get_connection (f->bus),
      frobnicate_cb, f, NULL);
  g_assert_true (mem);

  dbus_g_connection_register_g_object (f->bus, "/foo", f->object);
  g_assert_true (dbus_g_connection_lookup_g_object (f->bus, "/foo") ==
      f->object);

  my_object_emit_frobnicate ((MyObject *) f->object, NULL);

  while (f->frobnicate1_message == NULL)
    g_main_context_iteration (NULL, TRUE);

  dbus_message_unref (f->frobnicate1_message);
  f->frobnicate1_message = NULL;

  /* unregister the object from its last object path, then put it back
   * in the same location */
  dbus_g_connection_unregister_g_object (f->bus, f->object);
  dbus_g_connection_register_g_object (f->bus, "/foo", f->object);
  g_assert_true (dbus_g_connection_lookup_g_object (f->bus, "/foo") ==
      f->object);

  /* bug: in 0.92, this would be emitted twice because the hook was added
   * twice */
  my_object_emit_frobnicate ((MyObject *) f->object, NULL);

  while (f->frobnicate1_message == NULL)
    g_main_context_iteration (NULL, TRUE);

  dbus_message_unref (f->frobnicate1_message);
  f->frobnicate1_message = NULL;

  /* unregister the object from its last object path, then put it back
   * at a different location */
  dbus_g_connection_unregister_g_object (f->bus, f->object);
  dbus_g_connection_register_g_object (f->bus2, "/bar", f->object);
  g_assert_true (dbus_g_connection_lookup_g_object (f->bus2, "/bar") ==
      f->object);

  my_object_emit_frobnicate ((MyObject *) f->object, NULL);

  while (f->frobnicate2_message == NULL)
    g_main_context_iteration (NULL, TRUE);

  /* check that this wasn't received anyway, which would indicate that
   * either unregistration from /foo was unsuccessful, or the double
   * emission mentioned above was seen */
  g_assert_null (f->frobnicate1_message);

  dbus_message_unref (f->frobnicate2_message);
  f->frobnicate2_message = NULL;
}

static DBusHandlerResult
objectified_cb (DBusConnection *conn,
    DBusMessage *message,
    void *user_data)
{
  Fixture *f = user_data;

  if (dbus_message_is_signal (message,
        "org.freedesktop.DBus.GLib.Tests.MyObject", "Objectified"))
    {
      const char *sender = dbus_message_get_sender (message);
      const char *path = dbus_message_get_path (message);
      dbus_bool_t ok;
      DBusError e;

      dbus_error_init (&e);

      g_assert_nonnull (sender);
      g_assert_nonnull (path);

      g_assert_cmpstr (path, ==, "/foo");
      g_assert_cmpstr (sender, ==, dbus_bus_get_unique_name (
            dbus_g_connection_get_connection (f->bus)));

      path = NULL;
      ok = dbus_message_get_args (message, &e,
          DBUS_TYPE_OBJECT_PATH, &path,
          DBUS_TYPE_INVALID);

      if (dbus_error_is_set (&e))
        g_error ("%s: %s", e.name, e.message);

      g_assert_true (ok);
      g_assert_cmpstr (path, ==, "/foo");

      f->received_objectified = TRUE;
    }

  return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
}

static void
test_marshal_object (Fixture *f,
    gconstpointer test_data G_GNUC_UNUSED)
{
  dbus_bool_t mem;

  g_test_bug ("37852");

  dbus_g_connection_register_g_object (f->bus, "/foo", f->object);
  g_assert_true (dbus_g_connection_lookup_g_object (f->bus, "/foo") ==
      f->object);

  dbus_bus_add_match (dbus_g_connection_get_connection (f->bus),
      "type='signal'", &f->dbus_error);
  assert_no_error (&f->dbus_error);
  mem = dbus_connection_add_filter (dbus_g_connection_get_connection (f->bus),
      objectified_cb, f, NULL);
  g_assert_true (mem);

  my_object_emit_objectified ((MyObject *) f->object, f->object);

  while (!f->received_objectified)
    g_main_context_iteration (NULL, TRUE);
}

int
main (int argc, char **argv)
{
  g_type_init ();
  g_log_set_always_fatal (G_LOG_LEVEL_WARNING | G_LOG_LEVEL_CRITICAL);
  dbus_g_type_specialized_init ();
  g_test_bug_base ("https://bugs.freedesktop.org/show_bug.cgi?id=");
  g_test_init (&argc, &argv, NULL);

  g_test_add ("/registrations/lookup", Fixture, NULL,
      setup, test_lookup, teardown);
  g_test_add ("/registrations/unregister", Fixture, NULL,
      setup, test_unregister, teardown);
  g_test_add ("/registrations/unregister-on-last-unref", Fixture, NULL,
      setup, test_unregister_on_last_unref, teardown);
  g_test_add ("/registrations/unregister-on-forced-dispose", Fixture, NULL,
      setup, test_unregister_on_forced_dispose, teardown);
  g_test_add ("/registrations/reregister", Fixture, NULL,
      setup, test_reregister, teardown);
  g_test_add ("/registrations/twice", Fixture, NULL,
      setup, test_twice, teardown);
  g_test_add ("/registrations/clean-slate", Fixture, NULL,
      setup, test_clean_slate, teardown);
  g_test_add ("/registrations/marshal-object", Fixture, NULL,
      setup, test_marshal_object, teardown);

  return g_test_run ();
}
