# HG changeset patch
# Node ID 9287308b2507
# Parent  43fdbe891f5a
diff --git a/libpurple/meson.build b/libpurple/meson.build
--- a/libpurple/meson.build
+++ b/libpurple/meson.build
@@ -81,6 +81,7 @@
 	'purpleprotocolmanager.c',
 	'purpleprotocolmedia.c',
 	'purpleprotocolroomlist.c',
+	'purpleprotocolroster.c',
 	'purpleprotocolserver.c',
 	'purpleprotocolwhiteboard.c',
 	'purpleproxyinfo.c',
@@ -199,6 +200,7 @@
 	'purpleprotocolmedia.h',
 	'purpleprotocolmanager.h',
 	'purpleprotocolroomlist.h',
+	'purpleprotocolroster.h',
 	'purpleprotocolserver.h',
 	'purpleprotocolwhiteboard.h',
 	'purpleproxyinfo.h',
diff --git a/libpurple/purpleprotocolroster.h b/libpurple/purpleprotocolroster.h
new file mode 100644
--- /dev/null
+++ b/libpurple/purpleprotocolroster.h
@@ -0,0 +1,197 @@
+/*
+ * Purple - Internet Messaging Library
+ * Copyright (C) Pidgin Developers <devel@pidgin.im>
+ *
+ * Purple is the legal property of its developers, whose names are too numerous
+ * to list here.  Please refer to the COPYRIGHT file distributed with this
+ * source distribution.
+ *
+ * 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.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, see <https://www.gnu.org/licenses/>.
+ */
+
+#if !defined(PURPLE_GLOBAL_HEADER_INSIDE) && !defined(PURPLE_COMPILATION)
+# error "only <purple.h> may be included directly"
+#endif
+
+#ifndef PURPLE_PROTOCOL_ROSTER_H
+#define PURPLE_PROTOCOL_ROSTER_H
+
+#include <glib.h>
+#include <glib-object.h>
+
+#include <libpurple/account.h>
+#include <libpurple/purplecontact.h>
+#include <libpurple/purpleprotocol.h>
+
+#define PURPLE_TYPE_PROTOCOL_ROSTER (purple_protocol_roster_get_type())
+G_DECLARE_INTERFACE(PurpleProtocolRoster, purple_protocol_roster, PURPLE,
+                    PROTOCOL_ROSTER, PurpleProtocol)
+
+/**
+ * PurpleProtocolRoster:
+ *
+ * The #PurpleProtocolRoster interface defines the behavior to tell a
+ * protocol when the users wants to manage contacts on the server side roster.
+ *
+ * Since: 3.0.0
+ */
+
+/**
+ * PurpleProtocolRosterInterface:
+ * @add_async: Called when the user is trying to add a contact to the server
+ *             side roster.
+ * @add_finish: Called when adding the contact has completed.
+ * @update_async: Called when the user is trying to update a contact on the
+ *                server side roster.
+ * @update_finish: Called when updating the contact has completed.
+ * @remove_async: Called when the user is trying to remove a contact from the
+ *                server side roster.
+ * @remove_finish: Called when removing the contact has completed.
+ *
+ * The interface for managing the server side roster.
+ *
+ * This interface provides a gateway between purple and the protocol for
+ * managing the server side roster. All of the functions are asynchronous to
+ * make sure nothing blocks the rest of the program.
+ *
+ * Since: 3.0.0
+ */
+struct _PurpleProtocolRosterInterface {
+	/*< private >*/
+	GTypeInterface parent;
+
+	/*< public >*/
+	void (*add_async)(PurpleProtocolRoster *roster, PurpleContact *contact, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer data);
+	gboolean (*add_finish)(PurpleProtocolRoster *roster, GAsyncResult *result, GError **error);
+
+	void (*update_async)(PurpleProtocolRoster *roster, PurpleContact *contact, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer data);
+	gboolean (*update_finish)(PurpleProtocolRoster *roster, GAsyncResult *result, GError **error);
+
+	void (*remove_async)(PurpleProtocolRoster *roster, PurpleContact *contact, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer data);
+	gboolean (*remove_finish)(PurpleProtocolRoster *roster, GAsyncResult *result, GError **error);
+
+	/*< private >*/
+	gpointer reserved[4];
+};
+
+G_BEGIN_DECLS
+
+/**
+ * purple_protocol_roster_add_async:
+ * @roster: The instance.
+ * @contact: The [class@Contact] to add.
+ * @cancellable: A [class@Gio.Cancellable].
+ * @callback: A [callback@Gio.AsyncReadyCallback] to call when the request has
+ *            completed.
+ * @data: User data to pass to @callback.
+ *
+ * Requests that @roster adds @contact to the server side roster. When
+ * @callback is called, [method@ProtocolRoster.add_finish] should be called to
+ * get the result.
+ *
+ * Since: 3.0.0
+ */
+void purple_protocol_roster_add_async(PurpleProtocolRoster *roster, PurpleContact *contact, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer data);
+
+/**
+ * purple_protocol_roster_add_finish:
+ * @roster: The instance.
+ * @result: A [iface@Gio.AsyncResult].
+ * @error: (out): A return address for an error.
+ *
+ * Gets the result of a previous call to [method@ProtocolRoster.add_async].
+ *
+ * If an error was encountered, @error will be set and %FALSE will be returned.
+ * Otherwise the result will be returned.
+ *
+ * Returns: %TRUE if the operation was successful, otherwise %FALSE with error
+ *          optionally set.
+ *
+ * Since: 3.0.0
+ */
+gboolean purple_protocol_roster_add_finish(PurpleProtocolRoster *roster, GAsyncResult *result, GError **error);
+
+/**
+ * purple_protocol_roster_update_async:
+ * @roster: The instance.
+ * @contact: The [class@Contact] to update.
+ * @cancellable: A [class@Gio.Cancellable].
+ * @callback: A [callback@Gio.AsyncReadyCallback] to call when the request has
+ *            completed.
+ * @data: User data to pass to @callback.
+ *
+ * Requests that @roster updates @contact on the server side roster. When
+ * @callback is called, [method@ProtocolRoster.update_finish] should be called
+ * to get the result.
+ *
+ * Since: 3.0.0
+ */
+void purple_protocol_roster_update_async(PurpleProtocolRoster *roster, PurpleContact *contact, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer data);
+
+/**
+ * purple_protocol_roster_update_finish:
+ * @roster: The instance.
+ * @result: A [iface@Gio.AsyncResult].
+ * @error: (out): A return address for an error.
+ *
+ * Gets the result of a previous call to [method@ProtocolRoster.update_async].
+ *
+ * If an error was encountered, @error will be set and %FALSE will be returned.
+ * Otherwise the result will be returned.
+ *
+ * Returns: %TRUE if the operation was successful, otherwise %FALSE with error
+ *          optionally set.
+ *
+ * Since: 3.0.0
+ */
+gboolean purple_protocol_roster_update_finish(PurpleProtocolRoster *roster, GAsyncResult *result, GError **error);
+
+/**
+ * purple_protocol_roster_remove_async:
+ * @roster: The instance.
+ * @contact: The [class@Contact] to remove.
+ * @cancellable: A [class@Gio.Cancellable].
+ * @callback: A [callback@Gio.AsyncReadyCallback] to call when the request has
+ *            completed.
+ * @data: User data to pass to @callback.
+ *
+ * Requests that @roster removes @contact from the server side roster. When
+ * @callback is called, [method@ProtocolRoster.remove_finish] should be called
+ * to get the result.
+ *
+ * Since: 3.0.0
+ */
+void purple_protocol_roster_remove_async(PurpleProtocolRoster *roster, PurpleContact *contact, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer data);
+
+/**
+ * purple_protocol_roster_remove_finish:
+ * @roster: The instance.
+ * @result: A [iface@Gio.AsyncResult].
+ * @error: (out): A return address for an error.
+ *
+ * Gets the result of a previous call to [method@ProtocolRoster.remove_async].
+ *
+ * If an error was encountered, @error will be set and %FALSE will be returned.
+ * Otherwise the result will be returned.
+ *
+ * Returns: %TRUE if the operation was successful, otherwise %FALSE with error
+ *          optionally set.
+ *
+ * Since: 3.0.0
+ */
+gboolean purple_protocol_roster_remove_finish(PurpleProtocolRoster *roster, GAsyncResult *result, GError **error);
+
+G_END_DECLS
+
+#endif /* PURPLE_PROTOCOL_ROSTER_H */
diff --git a/libpurple/purpleprotocolroster.c b/libpurple/purpleprotocolroster.c
new file mode 100644
--- /dev/null
+++ b/libpurple/purpleprotocolroster.c
@@ -0,0 +1,139 @@
+/*
+ * Purple - Internet Messaging Library
+ * Copyright (C) Pidgin Developers <devel@pidgin.im>
+ *
+ * Purple is the legal property of its developers, whose names are too numerous
+ * to list here.  Please refer to the COPYRIGHT file distributed with this
+ * source distribution.
+ *
+ * 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.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include "purpleprotocolroster.h"
+
+/******************************************************************************
+ * GObject Implementation
+ *****************************************************************************/
+G_DEFINE_INTERFACE(PurpleProtocolRoster, purple_protocol_roster,
+                   PURPLE_TYPE_PROTOCOL)
+
+static void
+purple_protocol_roster_default_init(G_GNUC_UNUSED PurpleProtocolRosterInterface *iface)
+{
+}
+
+/******************************************************************************
+ * Public API
+ *****************************************************************************/
+void
+purple_protocol_roster_add_async(PurpleProtocolRoster *roster,
+                                 PurpleContact *contact,
+                                 GCancellable *cancellable,
+                                 GAsyncReadyCallback callback,
+                                 gpointer data)
+{
+	PurpleProtocolRosterInterface *iface = NULL;
+
+	g_return_if_fail(PURPLE_IS_PROTOCOL_ROSTER(roster));
+	g_return_if_fail(PURPLE_IS_CONTACT(contact));
+
+	iface = PURPLE_PROTOCOL_ROSTER_GET_IFACE(roster);
+	if(iface != NULL && iface->add_async != NULL) {
+		iface->add_async(roster, contact, cancellable, callback, data);
+	}
+}
+
+gboolean
+purple_protocol_roster_add_finish(PurpleProtocolRoster *roster,
+                                  GAsyncResult *result, GError **error)
+{
+	PurpleProtocolRosterInterface *iface = NULL;
+
+	g_return_val_if_fail(PURPLE_IS_PROTOCOL_ROSTER(roster), FALSE);
+
+	iface = PURPLE_PROTOCOL_ROSTER_GET_IFACE(roster);
+	if(iface != NULL && iface->add_finish != NULL) {
+		return iface->add_finish(roster, result, error);
+	}
+
+	return FALSE;
+}
+
+void
+purple_protocol_roster_update_async(PurpleProtocolRoster *roster,
+                                    PurpleContact *contact,
+                                    GCancellable *cancellable,
+                                    GAsyncReadyCallback callback,
+                                    gpointer data)
+{
+	PurpleProtocolRosterInterface *iface = NULL;
+
+	g_return_if_fail(PURPLE_IS_PROTOCOL_ROSTER(roster));
+	g_return_if_fail(PURPLE_IS_CONTACT(contact));
+
+	iface = PURPLE_PROTOCOL_ROSTER_GET_IFACE(roster);
+	if(iface != NULL && iface->update_async != NULL) {
+		iface->update_async(roster, contact, cancellable, callback, data);
+	}
+}
+
+gboolean
+purple_protocol_roster_update_finish(PurpleProtocolRoster *roster,
+                                     GAsyncResult *result, GError **error)
+{
+	PurpleProtocolRosterInterface *iface = NULL;
+
+	g_return_val_if_fail(PURPLE_IS_PROTOCOL_ROSTER(roster), FALSE);
+
+	iface = PURPLE_PROTOCOL_ROSTER_GET_IFACE(roster);
+	if(iface != NULL && iface->update_finish != NULL) {
+		return iface->update_finish(roster, result, error);
+	}
+
+	return FALSE;
+}
+
+void
+purple_protocol_roster_remove_async(PurpleProtocolRoster *roster,
+                                    PurpleContact *contact,
+                                    GCancellable *cancellable,
+                                    GAsyncReadyCallback callback,
+                                    gpointer data)
+{
+	PurpleProtocolRosterInterface *iface = NULL;
+
+	g_return_if_fail(PURPLE_IS_PROTOCOL_ROSTER(roster));
+	g_return_if_fail(PURPLE_IS_CONTACT(contact));
+
+	iface = PURPLE_PROTOCOL_ROSTER_GET_IFACE(roster);
+	if(iface != NULL && iface->remove_async != NULL) {
+		iface->remove_async(roster, contact, cancellable, callback, data);
+	}
+}
+
+gboolean
+purple_protocol_roster_remove_finish(PurpleProtocolRoster *roster,
+                                     GAsyncResult *result, GError **error)
+{
+	PurpleProtocolRosterInterface *iface = NULL;
+
+	g_return_val_if_fail(PURPLE_IS_PROTOCOL_ROSTER(roster), FALSE);
+
+	iface = PURPLE_PROTOCOL_ROSTER_GET_IFACE(roster);
+	if(iface != NULL && iface->remove_finish != NULL) {
+		return iface->remove_finish(roster, result, error);
+	}
+
+	return FALSE;
+}
diff --git a/libpurple/tests/meson.build b/libpurple/tests/meson.build
--- a/libpurple/tests/meson.build
+++ b/libpurple/tests/meson.build
@@ -23,6 +23,7 @@
     'presence',
     'protocol',
     'protocol_action',
+    'protocol_roster',
     'protocol_xfer',
     'purplepath',
     'queued_output_stream',
diff --git a/libpurple/tests/test_protocol_roster.c b/libpurple/tests/test_protocol_roster.c
new file mode 100644
--- /dev/null
+++ b/libpurple/tests/test_protocol_roster.c
@@ -0,0 +1,497 @@
+/*
+ * Purple - Internet Messaging Library
+ * Copyright (C) Pidgin Developers <devel@pidgin.im>
+ *
+ * Purple is the legal property of its developers, whose names are too numerous
+ * to list here.  Please refer to the COPYRIGHT file distributed with this
+ * source distribution.
+ *
+ * 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.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include <glib.h>
+
+#include <purple.h>
+
+#include "test_ui.h"
+
+/******************************************************************************
+ * Globals
+ *****************************************************************************/
+
+/* Since we're using GTask to test asynchronous functions, we need to use a
+ * main loop.
+ */
+static GMainLoop *loop = NULL;
+
+#define TEST_PURPLE_PROTOCOL_ROSTER_DOMAIN (g_quark_from_static_string("test-protocol-roster"))
+
+/******************************************************************************
+ * Helpers
+ *****************************************************************************/
+static gboolean
+test_purple_protocol_roster_timeout_cb(gpointer data) {
+	g_main_loop_quit(data);
+
+	g_warning("timed out waiting for the callback function to be called");
+
+	return G_SOURCE_REMOVE;
+}
+
+/******************************************************************************
+ * TestProtocolRoster implementation
+ *****************************************************************************/
+G_DECLARE_FINAL_TYPE(TestPurpleProtocolRoster, test_purple_protocol_roster,
+                     TEST_PURPLE, PROTOCOL_ROSTER, PurpleProtocol)
+
+struct _TestPurpleProtocolRoster {
+	PurpleProtocol parent;
+
+	gboolean should_error;
+
+	gboolean add_async;
+	gboolean add_finish;
+	gboolean update_async;
+	gboolean update_finish;
+	gboolean remove_async;
+	gboolean remove_finish;
+};
+
+static void
+test_purple_protocol_roster_add_async(PurpleProtocolRoster *r,
+                                      G_GNUC_UNUSED PurpleContact *contact,
+                                      GCancellable *cancellable,
+                                      GAsyncReadyCallback callback,
+                                      gpointer data)
+{
+	TestPurpleProtocolRoster *roster = TEST_PURPLE_PROTOCOL_ROSTER(r);
+	GTask *task = NULL;
+
+	roster->add_async = TRUE;
+
+	task = g_task_new(r, cancellable, callback, data);
+	if(roster->should_error) {
+		GError *error = g_error_new_literal(TEST_PURPLE_PROTOCOL_ROSTER_DOMAIN,
+		                                    0, "error");
+		g_task_return_error(task, error);
+	} else {
+		g_task_return_boolean(task, TRUE);
+	}
+
+	g_object_unref(G_OBJECT(task));
+}
+
+static gboolean
+test_purple_protocol_roster_add_finish(PurpleProtocolRoster *r,
+                                       GAsyncResult *result, GError **error)
+{
+	TestPurpleProtocolRoster *roster = TEST_PURPLE_PROTOCOL_ROSTER(r);
+
+	roster->add_finish = TRUE;
+
+	return g_task_propagate_boolean(G_TASK(result), error);
+}
+
+static void
+test_purple_protocol_roster_update_async(PurpleProtocolRoster *r,
+                                         G_GNUC_UNUSED PurpleContact *contact,
+                                         GCancellable *cancellable,
+                                         GAsyncReadyCallback callback,
+                                         gpointer data)
+{
+	TestPurpleProtocolRoster *roster = TEST_PURPLE_PROTOCOL_ROSTER(r);
+	GTask *task = NULL;
+
+	roster->update_async = TRUE;
+
+	task = g_task_new(r, cancellable, callback, data);
+	if(roster->should_error) {
+		GError *error = g_error_new_literal(TEST_PURPLE_PROTOCOL_ROSTER_DOMAIN,
+		                                    0, "error");
+		g_task_return_error(task, error);
+	} else {
+		g_task_return_boolean(task, TRUE);
+	}
+
+	g_object_unref(G_OBJECT(task));
+}
+
+static gboolean
+test_purple_protocol_roster_update_finish(PurpleProtocolRoster *r,
+                                          GAsyncResult *result, GError **error)
+{
+	TestPurpleProtocolRoster *roster = TEST_PURPLE_PROTOCOL_ROSTER(r);
+
+	roster->update_finish = TRUE;
+
+	return g_task_propagate_boolean(G_TASK(result), error);
+}
+
+static void
+test_purple_protocol_roster_remove_async(PurpleProtocolRoster *r,
+                                         G_GNUC_UNUSED PurpleContact *contact,
+                                         GCancellable *cancellable,
+                                         GAsyncReadyCallback callback,
+                                         gpointer data)
+{
+	TestPurpleProtocolRoster *roster = TEST_PURPLE_PROTOCOL_ROSTER(r);
+	GTask *task = NULL;
+
+	roster->remove_async = TRUE;
+
+	task = g_task_new(r, cancellable, callback, data);
+	if(roster->should_error) {
+		GError *error = g_error_new_literal(TEST_PURPLE_PROTOCOL_ROSTER_DOMAIN,
+		                                    0, "error");
+		g_task_return_error(task, error);
+	} else {
+		g_task_return_boolean(task, TRUE);
+	}
+
+	g_object_unref(G_OBJECT(task));
+}
+
+static gboolean
+test_purple_protocol_roster_remove_finish(PurpleProtocolRoster *r,
+                                          GAsyncResult *result, GError **error)
+{
+	TestPurpleProtocolRoster *roster = TEST_PURPLE_PROTOCOL_ROSTER(r);
+
+	roster->remove_finish = TRUE;
+
+	return g_task_propagate_boolean(G_TASK(result), error);
+}
+
+static void
+test_purple_protocol_roster_iface_init(PurpleProtocolRosterInterface *iface) {
+	iface->add_async = test_purple_protocol_roster_add_async;
+	iface->add_finish = test_purple_protocol_roster_add_finish;
+	iface->update_async = test_purple_protocol_roster_update_async;
+	iface->update_finish = test_purple_protocol_roster_update_finish;
+	iface->remove_async = test_purple_protocol_roster_remove_async;
+	iface->remove_finish = test_purple_protocol_roster_remove_finish;
+}
+
+G_DEFINE_TYPE_WITH_CODE(TestPurpleProtocolRoster, test_purple_protocol_roster,
+                        PURPLE_TYPE_PROTOCOL,
+                        G_IMPLEMENT_INTERFACE(PURPLE_TYPE_PROTOCOL_ROSTER,
+                                              test_purple_protocol_roster_iface_init))
+
+static void
+test_purple_protocol_roster_init(G_GNUC_UNUSED TestPurpleProtocolRoster *roster)
+{
+}
+
+static void
+test_purple_protocol_roster_class_init(G_GNUC_UNUSED TestPurpleProtocolRosterClass *klass)
+{
+}
+
+/******************************************************************************
+ * Add test
+ *****************************************************************************/
+static void
+test_purple_protocol_roster_add_cb(GObject *obj, GAsyncResult *res,
+                                   gpointer data)
+{
+	TestPurpleProtocolRoster *test_roster = TEST_PURPLE_PROTOCOL_ROSTER(obj);
+	PurpleProtocolRoster *roster = PURPLE_PROTOCOL_ROSTER(obj);
+	PurpleContact *contact = PURPLE_CONTACT(data);
+	GError *error = NULL;
+	gboolean result = FALSE;
+
+	result = purple_protocol_roster_add_finish(roster, res, &error);
+	if(test_roster->should_error) {
+		g_assert_error(error, TEST_PURPLE_PROTOCOL_ROSTER_DOMAIN, 0);
+		g_clear_error(&error);
+		g_assert_false(result);
+	} else {
+		g_assert_no_error(error);
+		g_assert_true(result);
+	}
+
+	g_clear_object(&contact);
+
+	g_main_loop_quit(loop);
+}
+
+static gboolean
+test_purple_protocol_roster_add_idle(gpointer data) {
+	PurpleProtocolRoster *roster = PURPLE_PROTOCOL_ROSTER(data);
+	PurpleAccount *account = NULL;
+	PurpleContact *contact = NULL;
+
+	account = purple_account_new("test", "test");
+	contact = purple_contact_new(account, NULL);
+	g_clear_object(&account);
+
+	purple_protocol_roster_add_async(roster, contact, NULL,
+	                                 test_purple_protocol_roster_add_cb,
+	                                 contact);
+
+	return G_SOURCE_REMOVE;
+}
+
+static void
+test_purple_protocol_roster_add(void) {
+	TestPurpleProtocolRoster *roster = NULL;
+
+	roster = g_object_new(test_purple_protocol_roster_get_type(), NULL);
+
+	g_idle_add(test_purple_protocol_roster_add_idle, roster);
+	g_timeout_add_seconds(10, test_purple_protocol_roster_timeout_cb, loop);
+
+	g_main_loop_run(loop);
+
+	g_assert_true(roster->add_async);
+	g_assert_true(roster->add_finish);
+	g_assert_false(roster->update_async);
+	g_assert_false(roster->update_finish);
+	g_assert_false(roster->remove_async);
+	g_assert_false(roster->remove_finish);
+
+	g_clear_object(&roster)
+;}
+
+static void
+test_purple_protocol_roster_add_error(void) {
+	TestPurpleProtocolRoster *roster = NULL;
+
+	roster = g_object_new(test_purple_protocol_roster_get_type(), NULL);
+	roster->should_error = TRUE;
+
+	g_idle_add(test_purple_protocol_roster_add_idle, roster);
+	g_timeout_add_seconds(10, test_purple_protocol_roster_timeout_cb, loop);
+
+	g_main_loop_run(loop);
+
+	g_assert_true(roster->add_async);
+	g_assert_true(roster->add_finish);
+	g_assert_false(roster->update_async);
+	g_assert_false(roster->update_finish);
+	g_assert_false(roster->remove_async);
+	g_assert_false(roster->remove_finish);
+
+	g_clear_object(&roster)
+;}
+
+/******************************************************************************
+ * Update test
+ *****************************************************************************/
+static void
+test_purple_protocol_roster_update_cb(GObject *obj, GAsyncResult *res,
+                                      gpointer data)
+{
+	TestPurpleProtocolRoster *test_roster = TEST_PURPLE_PROTOCOL_ROSTER(obj);
+	PurpleProtocolRoster *roster = PURPLE_PROTOCOL_ROSTER(obj);
+	PurpleContact *contact = PURPLE_CONTACT(data);
+	GError *error = NULL;
+	gboolean result = FALSE;
+
+	result = purple_protocol_roster_update_finish(roster, res, &error);
+	if(test_roster->should_error) {
+		g_assert_error(error, TEST_PURPLE_PROTOCOL_ROSTER_DOMAIN, 0);
+		g_clear_error(&error);
+		g_assert_false(result);
+	} else {
+		g_assert_no_error(error);
+		g_assert_true(result);
+	}
+
+	g_clear_object(&contact);
+
+	g_main_loop_quit(loop);
+}
+
+static gboolean
+test_purple_protocol_roster_update_idle(gpointer data) {
+	PurpleProtocolRoster *roster = PURPLE_PROTOCOL_ROSTER(data);
+	PurpleAccount *account = NULL;
+	PurpleContact *contact = NULL;
+
+	account = purple_account_new("test", "test");
+	contact = purple_contact_new(account, NULL);
+	g_clear_object(&account);
+
+	purple_protocol_roster_update_async(roster, contact, NULL,
+	                                    test_purple_protocol_roster_update_cb,
+	                                    contact);
+
+	return G_SOURCE_REMOVE;
+}
+
+static void
+test_purple_protocol_roster_update(void) {
+	TestPurpleProtocolRoster *roster = NULL;
+
+	roster = g_object_new(test_purple_protocol_roster_get_type(), NULL);
+
+	g_idle_add(test_purple_protocol_roster_update_idle, roster);
+	g_timeout_add_seconds(10, test_purple_protocol_roster_timeout_cb, loop);
+
+	g_main_loop_run(loop);
+
+	g_assert_false(roster->add_async);
+	g_assert_false(roster->add_finish);
+	g_assert_true(roster->update_async);
+	g_assert_true(roster->update_finish);
+	g_assert_false(roster->remove_async);
+	g_assert_false(roster->remove_finish);
+
+	g_clear_object(&roster)
+;}
+
+static void
+test_purple_protocol_roster_update_error(void) {
+	TestPurpleProtocolRoster *roster = NULL;
+
+	roster = g_object_new(test_purple_protocol_roster_get_type(), NULL);
+	roster->should_error = TRUE;
+
+	g_idle_add(test_purple_protocol_roster_update_idle, roster);
+	g_timeout_add_seconds(10, test_purple_protocol_roster_timeout_cb, loop);
+
+	g_main_loop_run(loop);
+
+	g_assert_false(roster->add_async);
+	g_assert_false(roster->add_finish);
+	g_assert_true(roster->update_async);
+	g_assert_true(roster->update_finish);
+	g_assert_false(roster->remove_async);
+	g_assert_false(roster->remove_finish);
+
+	g_clear_object(&roster)
+;}
+
+/******************************************************************************
+ * Remove test
+ *****************************************************************************/
+static void
+test_purple_protocol_roster_remove_cb(GObject *obj, GAsyncResult *res,
+                                      gpointer data)
+{
+	TestPurpleProtocolRoster *test_roster = TEST_PURPLE_PROTOCOL_ROSTER(obj);
+	PurpleProtocolRoster *roster = PURPLE_PROTOCOL_ROSTER(obj);
+	PurpleContact *contact = PURPLE_CONTACT(data);
+	GError *error = NULL;
+	gboolean result = FALSE;
+
+	result = purple_protocol_roster_remove_finish(roster, res, &error);
+	if(test_roster->should_error) {
+		g_assert_error(error, TEST_PURPLE_PROTOCOL_ROSTER_DOMAIN, 0);
+		g_clear_error(&error);
+		g_assert_false(result);
+	} else {
+		g_assert_no_error(error);
+		g_assert_true(result);
+	}
+
+	g_clear_object(&contact);
+
+	g_main_loop_quit(loop);
+}
+
+static gboolean
+test_purple_protocol_roster_remove_idle(gpointer data) {
+	PurpleProtocolRoster *roster = PURPLE_PROTOCOL_ROSTER(data);
+	PurpleAccount *account = NULL;
+	PurpleContact *contact = NULL;
+
+	account = purple_account_new("test", "test");
+	contact = purple_contact_new(account, NULL);
+	g_clear_object(&account);
+
+	purple_protocol_roster_remove_async(roster, contact, NULL,
+	                                    test_purple_protocol_roster_remove_cb,
+	                                    contact);
+
+	return G_SOURCE_REMOVE;
+}
+
+static void
+test_purple_protocol_roster_remove(void) {
+	TestPurpleProtocolRoster *roster = NULL;
+
+	roster = g_object_new(test_purple_protocol_roster_get_type(), NULL);
+
+	g_idle_add(test_purple_protocol_roster_remove_idle, roster);
+	g_timeout_add_seconds(10, test_purple_protocol_roster_timeout_cb, loop);
+
+	g_main_loop_run(loop);
+
+	g_assert_false(roster->add_async);
+	g_assert_false(roster->add_finish);
+	g_assert_false(roster->update_async);
+	g_assert_false(roster->update_finish);
+	g_assert_true(roster->remove_async);
+	g_assert_true(roster->remove_finish);
+
+	g_clear_object(&roster)
+;}
+
+static void
+test_purple_protocol_roster_remove_error(void) {
+	TestPurpleProtocolRoster *roster = NULL;
+
+	roster = g_object_new(test_purple_protocol_roster_get_type(), NULL);
+	roster->should_error = TRUE;
+
+	g_idle_add(test_purple_protocol_roster_remove_idle, roster);
+	g_timeout_add_seconds(10, test_purple_protocol_roster_timeout_cb, loop);
+
+	g_main_loop_run(loop);
+
+	g_assert_false(roster->add_async);
+	g_assert_false(roster->add_finish);
+	g_assert_false(roster->update_async);
+	g_assert_false(roster->update_finish);
+	g_assert_true(roster->remove_async);
+	g_assert_true(roster->remove_finish);
+
+	g_clear_object(&roster)
+;}
+
+/******************************************************************************
+ * Main
+ *****************************************************************************/
+gint
+main(gint argc, gchar **argv) {
+	gint ret = 0;
+
+	g_test_init(&argc, &argv, NULL);
+
+	test_ui_purple_init();
+
+	loop = g_main_loop_new(NULL, FALSE);
+
+	g_test_add_func("/protocol-roster/add", test_purple_protocol_roster_add);
+	g_test_add_func("/protocol-roster/add-error",
+	                test_purple_protocol_roster_add_error);
+	g_test_add_func("/protocol-roster/update",
+	                test_purple_protocol_roster_update);
+	g_test_add_func("/protocol-roster/update-error",
+	                test_purple_protocol_roster_update_error);
+	g_test_add_func("/protocol-roster/remove",
+	                test_purple_protocol_roster_remove);
+	g_test_add_func("/protocol-roster/remove-error",
+	                test_purple_protocol_roster_remove_error);
+
+	ret = g_test_run();
+
+	g_main_loop_unref(loop);
+
+	test_ui_purple_uninit();
+
+	return ret;
+}
