# HG changeset patch
# Node ID e786d1b1178f
# Parent  c22448f50d5d
diff --git a/libpurple/core.c b/libpurple/core.c
--- a/libpurple/core.c
+++ b/libpurple/core.c
@@ -174,6 +174,7 @@
 
 	purple_account_manager_startup();
 	purple_accounts_init();
+	purple_contact_manager_startup();
 	purple_savedstatuses_init();
 	purple_notify_init();
 	purple_conversations_init();
@@ -255,6 +256,7 @@
 	purple_plugins_uninit();
 
 	/* after plugins */
+	purple_contact_manager_shutdown();
 	purple_account_manager_shutdown();
 	purple_credential_manager_shutdown();
 	purple_protocol_manager_shutdown();
diff --git a/libpurple/meson.build b/libpurple/meson.build
--- a/libpurple/meson.build
+++ b/libpurple/meson.build
@@ -46,6 +46,7 @@
 	'purplechatuser.c',
 	'purpleconnectionerrorinfo.c',
 	'purplecontact.c',
+	'purplecontactmanager.c',
 	'purpleconversation.c',
 	'purpleconversationmanager.c',
 	'purpleconversationuiops.c',
@@ -150,6 +151,7 @@
 	'purplechatuser.h',
 	'purpleconnectionerrorinfo.h',
 	'purplecontact.h',
+	'purplecontactmanager.h',
 	'purpleconversation.h',
 	'purpleconversationmanager.h',
 	'purpleconversationuiops.h',
diff --git a/libpurple/purplecontactmanager.h b/libpurple/purplecontactmanager.h
new file mode 100644
--- /dev/null
+++ b/libpurple/purplecontactmanager.h
@@ -0,0 +1,146 @@
+/*
+ * Purple - Internet Messaging Library
+ * Copyright (C) Pidgin Developers <devel@pidgin.im>
+ *
+ * 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 <pidgin.h> may be included directly"
+#endif
+
+#ifndef PURPLE_CONTACT_MANAGER_H
+#define PURPLE_CONTACT_MANAGER_H
+
+#include <glib.h>
+#include <glib-object.h>
+
+#include <libpurple/account.h>
+#include <libpurple/purplecontact.h>
+
+G_BEGIN_DECLS
+
+#define PURPLE_TYPE_CONTACT_MANAGER (purple_contact_manager_get_type())
+G_DECLARE_FINAL_TYPE(PurpleContactManager, purple_contact_manager, PURPLE,
+                     CONTACT_MANAGER, GObject)
+
+/**
+ * PurpleContactManager:
+ *
+ * A manager for [class@Purple.Contact]s.
+ *
+ * Since: 3.0.0
+ */
+
+/**
+ * purple_contact_manager_get_default:
+ *
+ * Gets the default instance of [class@Purple.ContactManager].
+ *
+ * Typically this will be the main way for everyone to access the contact
+ * manager.
+ *
+ * Returns: (transfer none): The default [class@Purple.ContactManager] instance.
+ *
+ * Since: 3.0.0
+ */
+PurpleContactManager *purple_contact_manager_get_default(void);
+
+/**
+ * purple_contact_manager_add:
+ * @manager: The instance.
+ * @contact: (transfer none): The [class@Purple.Contact] to add.
+ *
+ * Adds @contact to @manager. If a contact with a matching account and id
+ * already exists, no action will be taken.
+ *
+ * Since: 3.0.0
+ */
+void purple_contact_manager_add(PurpleContactManager *manager, PurpleContact *contact);
+
+/**
+ * purple_contact_manager_remove:
+ * @manager: The instance.
+ * @contact: (transfer none): The [class@Purple.Contact] to remove.
+ *
+ * Attempts to remove @contact from @manager.
+ *
+ * Returns: If @contact is found and removed %TRUE will be return otherwise
+ *          %FALSE will returned.
+ *
+ * Since: 3.0.0
+ */
+gboolean purple_contact_manager_remove(PurpleContactManager *manager, PurpleContact *contact);
+
+/**
+ * purple_contact_manager_remove_all:
+ * @manager: The instance.
+ * @account: The [class@Purple.Account] whose contacts to remove.
+ *
+ * Removes all of the contacts from @manager that belong to @account.
+ *
+ * Returns: %TRUE if anything was removed, %FALSE if nothing was removed.
+ *
+ * Since: 3.0.0
+ */
+gboolean purple_contact_manager_remove_all(PurpleContactManager *manager, PurpleAccount *account);
+
+/**
+ * purple_contact_manager_get_all:
+ * @manager: The instance.
+ * @account: The [class@Purple.Account] whose contacts to get.
+ *
+ * Gets a [iface@Gio.ListModel] of all contacts that belong to @account.
+ *
+ * Returns: (transfer none) (nullable): A [iface@Gio.ListModel] of all the
+ *          contacts belonging to @account.
+ *
+ * Since: 3.0.0
+ */
+GListModel *purple_contact_manager_get_all(PurpleContactManager *manager, PurpleAccount *account);
+
+/**
+ * purple_contact_manager_find_with_username:
+ * @manager: The instance.
+ * @account: The [class@Purple.Account] whose contact to find.
+ * @username: The username of the contact to find.
+ *
+ * Looks for a [class@Purple.Contact] that belongs to @account with a username
+ * of @username.
+ *
+ * Returns: (transfer none): The [class@Purple.Contact] if found, otherwise
+ *          %NULL.
+ *
+ * Since: 3.0.0
+ */
+PurpleContact *purple_contact_manager_find_with_username(PurpleContactManager *manager, PurpleAccount *account, const gchar *username);
+
+/**
+ * purple_contact_manager_find_with_id:
+ * @manager: The instance.
+ * @account: The [class@Purple.Account] whose contact to find.
+ * @id: The id of the contact to find.
+ *
+ * Looks for a [class@Purple.Contact] that belongs to @account with a id of @id.
+ *
+ * Returns: (transfer none): The [class@Purple.Contact] if found, otherwise
+ *          %NULL.
+ *
+ * Since: 3.0.0
+ */
+PurpleContact *purple_contact_manager_find_with_id(PurpleContactManager *manager, PurpleAccount *account, const gchar *id);
+
+G_END_DECLS
+
+#endif /* PURPLE_CONTACT_MANAGER_H */
diff --git a/libpurple/purplecontactmanager.c b/libpurple/purplecontactmanager.c
new file mode 100644
--- /dev/null
+++ b/libpurple/purplecontactmanager.c
@@ -0,0 +1,355 @@
+/*
+ * Purple - Internet Messaging Library
+ * Copyright (C) Pidgin Developers <devel@pidgin.im>
+ *
+ * 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/gi18n-lib.h>
+
+#include "purplecontactmanager.h"
+#include "purpleprivate.h"
+#include "util.h"
+
+enum {
+	SIG_ADDED,
+	SIG_REMOVED,
+	N_SIGNALS,
+};
+static guint signals[N_SIGNALS] = {0, };
+
+struct _PurpleContactManager {
+	GObject parent;
+
+	GHashTable *accounts;
+};
+
+static PurpleContactManager *default_manager = NULL;
+
+/******************************************************************************
+ * Helpers
+ *****************************************************************************/
+static gboolean
+purple_contact_manager_find_with_username_helper(gconstpointer a,
+                                                 gconstpointer b)
+{
+	PurpleContact *contact_a = (gpointer)a;
+	PurpleContact *contact_b = (gpointer)b;
+	const gchar *username_a = NULL;
+	const gchar *username_b = NULL;
+
+	username_a = purple_contact_get_username(contact_a);
+	username_b = purple_contact_get_username(contact_b);
+
+	return purple_strequal(username_a, username_b);
+}
+
+static gboolean
+purple_contact_manager_find_with_id_helper(gconstpointer a, gconstpointer b) {
+	PurpleContact *contact_a = (gpointer)a;
+	PurpleContact *contact_b = (gpointer)b;
+	const gchar *id_a = NULL;
+	const gchar *id_b = NULL;
+
+	id_a = purple_contact_get_id(contact_a);
+	id_b = purple_contact_get_id(contact_b);
+
+	return purple_strequal(id_a, id_b);
+}
+
+/******************************************************************************
+ * GObject Implementation
+ *****************************************************************************/
+G_DEFINE_TYPE(PurpleContactManager, purple_contact_manager, G_TYPE_OBJECT)
+
+static void
+purple_contact_manager_finalize(GObject *obj) {
+	PurpleContactManager *manager = NULL;
+
+	manager = PURPLE_CONTACT_MANAGER(obj);
+
+	g_clear_pointer(&manager->accounts, g_hash_table_destroy);
+
+	G_OBJECT_CLASS(purple_contact_manager_parent_class)->finalize(obj);
+}
+
+static void
+purple_contact_manager_init(PurpleContactManager *manager) {
+	manager->accounts = g_hash_table_new_full(g_direct_hash, g_direct_equal,
+	                                          g_object_unref, g_object_unref);
+}
+
+static void
+purple_contact_manager_class_init(PurpleContactManagerClass *klass) {
+	GObjectClass *obj_class = G_OBJECT_CLASS(klass);
+
+	obj_class->finalize = purple_contact_manager_finalize;
+
+	/**
+	 * PurpleContactManager::added:
+	 * @manager: The instance.
+	 * @contact: The [class@Purple.Contact] that was registered.
+	 *
+	 * Emitted after @contact has been added to @manager.
+	 *
+	 * Since: 3.0.0
+	 */
+	signals[SIG_ADDED] = g_signal_new_class_handler(
+		"added",
+		G_OBJECT_CLASS_TYPE(klass),
+		G_SIGNAL_RUN_LAST,
+		NULL,
+		NULL,
+		NULL,
+		NULL,
+		G_TYPE_NONE,
+		1,
+		PURPLE_TYPE_CONTACT);
+
+	/**
+	 * PurpleContactManager::removed:
+	 * @manager: The instance.
+	 * @contact: The [class@Purple.Contact] that was removed.
+	 *
+	 * Emitted after @contact has been removed from @manager.
+	 *
+	 * Since: 3.0.0
+	 */
+	signals[SIG_REMOVED] = g_signal_new_class_handler(
+		"removed",
+		G_OBJECT_CLASS_TYPE(klass),
+		G_SIGNAL_RUN_LAST,
+		NULL,
+		NULL,
+		NULL,
+		NULL,
+		G_TYPE_NONE,
+		1,
+		PURPLE_TYPE_CONTACT);
+}
+
+/******************************************************************************
+ * Private API
+ *****************************************************************************/
+void
+purple_contact_manager_startup(void) {
+	if(default_manager == NULL) {
+		default_manager = g_object_new(PURPLE_TYPE_CONTACT_MANAGER, NULL);
+		g_object_add_weak_pointer(G_OBJECT(default_manager),
+		                          (gpointer)&default_manager);
+	}
+}
+
+void
+purple_contact_manager_shutdown(void) {
+	g_clear_object(&default_manager);
+}
+
+/******************************************************************************
+ * Public API
+ *****************************************************************************/
+PurpleContactManager *
+purple_contact_manager_get_default(void) {
+	if(G_UNLIKELY(!PURPLE_IS_CONTACT_MANAGER(default_manager))) {
+		g_warning("The default contact manager was unexpectedly NULL");
+	}
+
+	return default_manager;
+}
+
+void
+purple_contact_manager_add(PurpleContactManager *manager,
+                           PurpleContact *contact)
+{
+	PurpleAccount *account = NULL;
+	GListStore *contacts = NULL;
+	gboolean added = FALSE;
+
+	g_return_if_fail(PURPLE_IS_CONTACT_MANAGER(manager));
+	g_return_if_fail(PURPLE_IS_CONTACT(contact));
+
+	account = purple_contact_get_account(contact);
+	contacts = g_hash_table_lookup(manager->accounts, account);
+	if(!G_IS_LIST_STORE(contacts)) {
+		contacts = g_list_store_new(PURPLE_TYPE_CONTACT);
+		g_hash_table_insert(manager->accounts, g_object_ref(account), contacts);
+
+		g_list_store_append(contacts, contact);
+
+		added = TRUE;
+	} else {
+		if(g_list_store_find(contacts, contact, NULL)) {
+			const gchar *username = purple_contact_get_username(contact);
+			const gchar *id = purple_contact_get_id(contact);
+
+			g_warning("double add detected for contact %s:%s", id, username);
+
+			return;
+		}
+
+		g_list_store_append(contacts, contact);
+		added = TRUE;
+	}
+
+	if(added) {
+		g_signal_emit(manager, signals[SIG_ADDED], 0, contact);
+	}
+}
+
+gboolean
+purple_contact_manager_remove(PurpleContactManager *manager,
+                              PurpleContact *contact)
+{
+	PurpleAccount *account = NULL;
+	GListStore *contacts = NULL;
+	guint position = 0;
+
+	g_return_val_if_fail(PURPLE_IS_CONTACT_MANAGER(manager), FALSE);
+	g_return_val_if_fail(PURPLE_IS_CONTACT(contact), FALSE);
+
+	account = purple_contact_get_account(contact);
+	contacts = g_hash_table_lookup(manager->accounts, account);
+	if(!G_IS_LIST_STORE(contacts)) {
+		return FALSE;
+	}
+
+	if(g_list_store_find(contacts, contact, &position)) {
+		gboolean removed = FALSE;
+		guint len = 0;
+
+		/* Ref the contact to make sure that the instance is valid when we emit
+		 * the removed signal.
+		 */
+		g_object_ref(contact);
+
+		len = g_list_model_get_n_items(G_LIST_MODEL(contacts));
+		g_list_store_remove(contacts, position);
+		if(g_list_model_get_n_items(G_LIST_MODEL(contacts)) < len) {
+			removed = TRUE;
+		}
+
+		if(removed) {
+			g_signal_emit(manager, signals[SIG_REMOVED], 0, contact);
+		}
+
+		g_object_unref(contact);
+
+		return removed;
+	}
+
+	return FALSE;
+}
+
+gboolean
+purple_contact_manager_remove_all(PurpleContactManager *manager,
+                                  PurpleAccount *account)
+{
+	GListStore *contacts = NULL;
+
+	g_return_val_if_fail(PURPLE_IS_CONTACT_MANAGER(manager), FALSE);
+	g_return_val_if_fail(PURPLE_IS_ACCOUNT(account), FALSE);
+
+	/* If there are any contacts for this account, manually iterate them and
+	 * emit the removed signal. This is more efficient than calling remove on
+	 * each one individually as that would require updating the backing
+	 * GListStore for each individual removal.
+	 */
+	contacts = g_hash_table_lookup(manager->accounts, account);
+	if(G_IS_LIST_STORE(contacts)) {
+		guint n_items = g_list_model_get_n_items(G_LIST_MODEL(contacts));
+		for(guint i = 0; i < n_items; i++) {
+			PurpleContact *contact = NULL;
+
+			contact = g_list_model_get_item(G_LIST_MODEL(contacts), i);
+
+			g_signal_emit(manager, signals[SIG_REMOVED], 0, contact);
+		}
+	}
+
+	return g_hash_table_remove(manager->accounts, account);
+}
+
+GListModel *
+purple_contact_manager_get_all(PurpleContactManager *manager,
+                               PurpleAccount *account)
+{
+	g_return_val_if_fail(PURPLE_IS_CONTACT_MANAGER(manager), FALSE);
+	g_return_val_if_fail(PURPLE_IS_ACCOUNT(account), FALSE);
+
+	return g_hash_table_lookup(manager->accounts, account);
+}
+
+PurpleContact *
+purple_contact_manager_find_with_username(PurpleContactManager *manager,
+                                          PurpleAccount *account,
+                                          const gchar *username)
+{
+	PurpleContact *needle = NULL;
+	GListStore *contacts = NULL;
+	guint position = 0;
+	gboolean found = FALSE;
+
+	g_return_val_if_fail(PURPLE_IS_CONTACT_MANAGER(manager), FALSE);
+	g_return_val_if_fail(PURPLE_IS_ACCOUNT(account), FALSE);
+	g_return_val_if_fail(username != NULL, FALSE);
+
+	contacts = g_hash_table_lookup(manager->accounts, account);
+	if(!G_IS_LIST_STORE(contacts)) {
+		return NULL;
+	}
+
+	needle = purple_contact_new(account, username);
+	found = g_list_store_find_with_equal_func(contacts, needle,
+	                                          purple_contact_manager_find_with_username_helper,
+	                                          &position);
+	g_clear_object(&needle);
+
+	if(found) {
+		return g_list_model_get_item(G_LIST_MODEL(contacts), position);
+	}
+
+	return NULL;
+}
+
+PurpleContact *
+purple_contact_manager_find_with_id(PurpleContactManager *manager,
+                                    PurpleAccount *account, const gchar *id)
+{
+	PurpleContact *needle = NULL;
+	GListStore *contacts = NULL;
+	guint position = 0;
+	gboolean found = FALSE;
+
+	g_return_val_if_fail(PURPLE_IS_CONTACT_MANAGER(manager), FALSE);
+	g_return_val_if_fail(PURPLE_IS_ACCOUNT(account), FALSE);
+	g_return_val_if_fail(id != NULL, FALSE);
+
+	contacts = g_hash_table_lookup(manager->accounts, account);
+	if(!G_IS_LIST_STORE(contacts)) {
+		return NULL;
+	}
+
+	needle = g_object_new(PURPLE_TYPE_CONTACT, "account", account, "id", id,
+	                      NULL);
+	found = g_list_store_find_with_equal_func(contacts, needle,
+	                                          purple_contact_manager_find_with_id_helper,
+	                                          &position);
+	g_clear_object(&needle);
+
+	if(found) {
+		return g_list_model_get_item(G_LIST_MODEL(contacts), position);
+	}
+
+	return NULL;
+}
diff --git a/libpurple/purpleprivate.h b/libpurple/purpleprivate.h
--- a/libpurple/purpleprivate.h
+++ b/libpurple/purpleprivate.h
@@ -200,6 +200,24 @@
 void purple_account_manager_shutdown(void);
 
 /**
+ * purple_coniact_manager_startup:
+ *
+ * Starts up the contact manager by creating the default instance.
+ *
+ * Since: 3.0.0
+ */
+void purple_contact_manager_startup(void);
+
+/**
+ * purple_contact_manager_shutdown:
+ *
+ * Shuts down the contact manager by destroying the default instance.
+ *
+ * Since: 3.0.0
+ */
+void purple_contact_manager_shutdown(void);
+
+/**
  * purple_conversation_manager_startup:
  *
  * Starts up the conversation manager by creating the default instance.
diff --git a/libpurple/tests/meson.build b/libpurple/tests/meson.build
--- a/libpurple/tests/meson.build
+++ b/libpurple/tests/meson.build
@@ -4,6 +4,7 @@
     'authorization_request',
     'circular_buffer',
     'contact',
+    'contact_manager',
     'credential_provider',
     'history_adapter',
     'history_manager',
diff --git a/libpurple/tests/test_contact_manager.c b/libpurple/tests/test_contact_manager.c
new file mode 100644
--- /dev/null
+++ b/libpurple/tests/test_contact_manager.c
@@ -0,0 +1,286 @@
+/*
+ * Purple - Internet Messaging Library
+ * Copyright (C) Pidgin Developers <devel@pidgin.im>
+ *
+ * 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 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, see <https://www.gnu.org/licenses/>.
+ */
+
+#include <glib.h>
+
+#include <purple.h>
+
+#include "test_ui.h"
+
+/******************************************************************************
+ * Callbacks
+ *****************************************************************************/
+static void
+test_purple_contact_manager_increment_cb(G_GNUC_UNUSED PurpleContactManager *manager,
+                                         G_GNUC_UNUSED PurpleContact *contact,
+                                         gpointer data)
+{
+	gint *called = data;
+
+	*called = *called + 1;
+}
+
+/******************************************************************************
+ * Tests
+ *****************************************************************************/
+static void
+test_purple_contact_manager_get_default(void) {
+	PurpleContactManager *manager1 = NULL, *manager2 = NULL;
+
+	manager1 = purple_contact_manager_get_default();
+	g_assert_true(PURPLE_IS_CONTACT_MANAGER(manager1));
+
+	manager2 = purple_contact_manager_get_default();
+	g_assert_true(PURPLE_IS_CONTACT_MANAGER(manager2));
+
+	g_assert_true(manager1 == manager2);
+}
+
+static void
+test_purple_contact_manager_add_remove(void) {
+	PurpleAccount *account = NULL;
+	PurpleContactManager *manager = NULL;
+	PurpleContact *contact = NULL;
+	GListModel *contacts = NULL;
+	gint added_called = 0, removed_called = 0;
+
+	manager = g_object_new(PURPLE_TYPE_CONTACT_MANAGER, NULL);
+
+	g_assert_true(PURPLE_IS_CONTACT_MANAGER(manager));
+
+	/* Wire up our signals. */
+	g_signal_connect(manager, "added",
+	                 G_CALLBACK(test_purple_contact_manager_increment_cb),
+	                 &added_called);
+	g_signal_connect(manager, "removed",
+	                 G_CALLBACK(test_purple_contact_manager_increment_cb),
+	                 &removed_called);
+
+	/* Create our account. */
+	account = purple_account_new("test", "test");
+
+	/* Create the and add it to the manager. */
+	contact = purple_contact_new(account, "test-user");
+
+	/* Add the contact to the manager. */
+	purple_contact_manager_add(manager, contact);
+
+	/* Make sure the contact is available. */
+	contacts = purple_contact_manager_get_all(manager, account);
+	g_assert_nonnull(contacts);
+	g_assert_cmpuint(g_list_model_get_n_items(contacts), ==, 1);
+
+	/* Make sure the added signal was called. */
+	g_assert_cmpint(added_called, ==, 1);
+
+	/* Remove the contact. */
+	purple_contact_manager_remove(manager, contact);
+	g_assert_cmpint(removed_called, ==, 1);
+
+	/* Clean up.*/
+	g_clear_object(&contact);
+	g_clear_object(&account);
+	g_clear_object(&manager);
+}
+
+static void
+test_purple_contact_manager_double_add(void) {
+	if(g_test_subprocess()) {
+		PurpleAccount *account = NULL;
+		PurpleContactManager *manager = NULL;
+		PurpleContact *contact = NULL;
+
+		manager = g_object_new(PURPLE_TYPE_CONTACT_MANAGER, NULL);
+
+		account = purple_account_new("test", "test");
+
+		contact = purple_contact_new(account, "test-user");
+
+		purple_contact_manager_add(manager, contact);
+		purple_contact_manager_add(manager, contact);
+
+		/* This will never get called as the double add outputs a g_warning()
+		 * that causes the test to fail. This is left to avoid a false postive
+		 * in static analysis.
+		 */
+		g_clear_object(&account);
+		g_clear_object(&contact);
+		g_clear_object(&manager);
+	}
+
+	g_test_trap_subprocess(NULL, 0, 0);
+	g_test_trap_assert_stderr("*Purple-WARNING*double add detected for contact*");
+}
+
+static void
+test_purple_contact_manager_double_remove(void) {
+	PurpleAccount *account = NULL;
+	PurpleContactManager *manager = NULL;
+	PurpleContact *contact = NULL;
+	gint removed_called = 0;
+
+	manager = g_object_new(PURPLE_TYPE_CONTACT_MANAGER, NULL);
+	g_signal_connect(manager, "removed",
+	                 G_CALLBACK(test_purple_contact_manager_increment_cb),
+	                 &removed_called);
+
+	account = purple_account_new("test", "test");
+
+	contact = purple_contact_new(account, "test-user");
+
+	purple_contact_manager_add(manager, contact);
+
+	g_assert_true(purple_contact_manager_remove(manager, contact));
+	g_assert_false(purple_contact_manager_remove(manager, contact));
+
+	g_assert_cmpint(removed_called, ==, 1);
+
+	g_clear_object(&account);
+	g_clear_object(&contact);
+	g_clear_object(&manager);
+}
+
+static void
+test_purple_contact_manager_remove_all(void) {
+	PurpleAccount *account = NULL;
+	PurpleContact *contact = NULL;
+	PurpleContactManager *manager = NULL;
+	GListModel *contacts = NULL;
+
+	manager = g_object_new(PURPLE_TYPE_CONTACT_MANAGER, NULL);
+
+	account = purple_account_new("test", "test");
+
+	contacts = purple_contact_manager_get_all(manager, account);
+	g_assert_null(contacts);
+
+	contact = purple_contact_new(account, "test-user");
+	purple_contact_manager_add(manager, contact);
+
+	contacts = purple_contact_manager_get_all(manager, account);
+	g_assert_nonnull(contacts);
+	g_assert_cmpuint(g_list_model_get_n_items(contacts), ==, 1);
+
+	g_assert_true(purple_contact_manager_remove_all(manager, account));
+	contacts = purple_contact_manager_get_all(manager, account);
+	g_assert_null(contacts);
+
+	/* Cleanup */
+	g_clear_object(&account);
+	g_clear_object(&contact);
+	g_clear_object(&manager);
+}
+
+static void
+test_purple_contact_manager_find_with_username(void) {
+	PurpleAccount *account = NULL;
+	PurpleContact *contact1 = NULL;
+	PurpleContact *contact2 = NULL;
+	PurpleContact *found = NULL;
+	PurpleContactManager *manager = NULL;
+
+	manager = g_object_new(PURPLE_TYPE_CONTACT_MANAGER, NULL);
+
+	account = purple_account_new("test", "test");
+
+	contact1 = purple_contact_new(account, "user1");
+	purple_contact_manager_add(manager, contact1);
+
+	contact2 = purple_contact_new(account, "user2");
+	purple_contact_manager_add(manager, contact2);
+
+	found = purple_contact_manager_find_with_username(manager, account,
+	                                                  "user1");
+	g_assert_nonnull(found);
+	g_assert_true(found == contact1);
+
+	found = purple_contact_manager_find_with_username(manager, account,
+	                                                  "user2");
+	g_assert_nonnull(found);
+	g_assert_true(found == contact2);
+
+	/* Cleanup. */
+	g_clear_object(&account);
+	g_clear_object(&contact1);
+	g_clear_object(&contact2);
+	g_clear_object(&manager);
+}
+
+static void
+test_purple_contact_manager_find_with_id(void) {
+	PurpleAccount *account = NULL;
+	PurpleContact *contact1 = NULL;
+	PurpleContact *contact2 = NULL;
+	PurpleContact *found = NULL;
+	PurpleContactManager *manager = NULL;
+
+	manager = g_object_new(PURPLE_TYPE_CONTACT_MANAGER, NULL);
+
+	account = purple_account_new("test", "test");
+
+	contact1 = g_object_new(PURPLE_TYPE_CONTACT, "account", account, "id",
+	                        "id-1", NULL);
+	purple_contact_manager_add(manager, contact1);
+
+	contact2 = g_object_new(PURPLE_TYPE_CONTACT, "account", account, "id",
+	                        "id-2", NULL);
+	purple_contact_manager_add(manager, contact2);
+
+	found = purple_contact_manager_find_with_id(manager, account, "id-1");
+	g_assert_nonnull(found);
+	g_assert_true(found == contact1);
+
+	found = purple_contact_manager_find_with_id(manager, account, "id-2");
+	g_assert_nonnull(found);
+	g_assert_true(found == contact2);
+
+	/* Cleanup. */
+	g_clear_object(&account);
+	g_clear_object(&contact1);
+	g_clear_object(&contact2);
+	g_clear_object(&manager);
+}
+
+/******************************************************************************
+ * Main
+ *****************************************************************************/
+gint
+main(gint argc, gchar *argv[]) {
+	g_test_init(&argc, &argv, NULL);
+
+	test_ui_purple_init();
+
+	g_test_add_func("/contact-manager/get-default",
+	                test_purple_contact_manager_get_default);
+	g_test_add_func("/contact-manager/add-remove",
+	                test_purple_contact_manager_add_remove);
+	g_test_add_func("/contact-manager/double-add",
+	                test_purple_contact_manager_double_add);
+	g_test_add_func("/contact-manager/double-remove",
+	                test_purple_contact_manager_double_remove);
+
+	g_test_add_func("/contact-manager/remove-all",
+	                test_purple_contact_manager_remove_all);
+
+	g_test_add_func("/contact-manager/find/with-username",
+	                test_purple_contact_manager_find_with_username);
+	g_test_add_func("/contact-manager/find/with-id",
+	                test_purple_contact_manager_find_with_id);
+
+	return g_test_run();
+}
