[folks] Add the framework for a test suite



commit 2dc9aee089f3550f8eda1da00a70bf3b911fc25b
Author: Philip Withnall <philip withnall collabora co uk>
Date:   Thu Jun 10 07:19:50 2010 -0700

    Add the framework for a test suite
    
    Add the framework for a test suite for the Telepathy backend, including
    a dummy account manager, account and connection, to allow complete control
    over the personas created in libfolks.
    
    This comes with a test case framework which wraps the GLib test framework,
    used by a test case which tests that all expected individuals are exposed by
    the individual aggregator.
    
    Heavily based on work by Travis Reitter <travis reitter collabora co uk>.

 Makefile.am                                        |    1 +
 configure.ac                                       |   19 +
 tests/Makefile.am                                  |   16 +
 tests/lib/Makefile.am                              |    7 +
 tests/lib/telepathy/Makefile.am                    |    5 +
 tests/lib/telepathy/contactlist/Makefile.am        |   84 +
 tests/lib/telepathy/contactlist/account-manager.c  |  188 +++
 tests/lib/telepathy/contactlist/account-manager.h  |   61 +
 tests/lib/telepathy/contactlist/account.c          |  341 ++++
 tests/lib/telepathy/contactlist/account.h          |   60 +
 tests/lib/telepathy/contactlist/conn.c             |  614 ++++++++
 tests/lib/telepathy/contactlist/conn.h             |   68 +
 .../telepathy/contactlist/contact-list-manager.c   | 1649 ++++++++++++++++++++
 .../telepathy/contactlist/contact-list-manager.h   |  107 ++
 tests/lib/telepathy/contactlist/contact-list.c     |  632 ++++++++
 tests/lib/telepathy/contactlist/contact-list.h     |  118 ++
 tests/lib/telepathy/contactlist/manager-file.py    |   23 +
 tests/lib/telepathy/contactlist/session.conf.in    |   54 +
 .../telepathy/contactlist/tp-test-contactlist.deps |    5 +
 .../telepathy/contactlist/tp-test-contactlist.h    |   10 +
 tests/telepathy/Makefile.am                        |   63 +
 tests/telepathy/contact-retrieval.vala             |  191 +++
 tests/telepathy/test-case.vala                     |   82 +
 tests/tools/with-session-bus.sh                    |   94 ++
 tools/manager-file.py                              |  175 +++
 25 files changed, 4667 insertions(+), 0 deletions(-)
---
diff --git a/Makefile.am b/Makefile.am
index 88b7654..9f96506 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -3,6 +3,7 @@ ACLOCAL_AMFLAGS = -I m4
 SUBDIRS = \
 	folks \
 	backends \
+	tests \
 	$(NULL)
 
 if ENABLE_DOCS
diff --git a/configure.ac b/configure.ac
index d2755db..b3fddc4 100644
--- a/configure.ac
+++ b/configure.ac
@@ -60,6 +60,9 @@ AC_SUBST(LDFLAGS)
 # -----------------------------------------------------------
 GLIB_REQUIRED=2.24.0
 TP_GLIB_REQUIRED=0.11.11
+# FIXME: remove the machinery using this once it's safe to require this version
+# (be sure to remove the HAVE_TP_GLIB_FOR_TESTS conditionals in Makefile.am files)
+TP_GLIB_TESTS_REQUIRED=0.11.14.1
 VALA_REQUIRED=0.9.6
 
 AM_PROG_VALAC([$VALA_REQUIRED])
@@ -132,6 +135,15 @@ BACKEND_DIR='$(libdir)/folks/$(FOLKS_MODULE_VERSION)/backends'
 AC_SUBST(BACKEND_DIR)
 
 # -----------------------------------------------------------
+# Tests
+# -----------------------------------------------------------
+PKG_CHECK_MODULES(TP_GLIB_FOR_TESTS,
+                  telepathy-glib >= $TP_GLIB_TESTS_REQUIRED,
+                  [have_tp_glib_for_tests=true],
+                  [have_tp_glib_for_tests=false])
+AM_CONDITIONAL([HAVE_TP_GLIB_FOR_TESTS], [$have_tp_glib_for_tests])
+
+# -----------------------------------------------------------
 # Documentation
 # -----------------------------------------------------------
 AC_ARG_ENABLE(docs,
@@ -204,6 +216,12 @@ AC_CONFIG_FILES([
 	backends/telepathy/lib/Makefile
 	folks/Makefile
 	docs/Makefile
+	tests/Makefile
+	tests/telepathy/Makefile
+	tests/lib/Makefile
+	tests/lib/telepathy/Makefile
+	tests/lib/telepathy/contactlist/Makefile
+	tests/lib/telepathy/contactlist/session.conf
 ])
 
 AC_OUTPUT
@@ -216,6 +234,7 @@ Configure summary:
         Prefix......................:  ${prefix}
         Bugreporting URL............:  ${PACKAGE_BUGREPORT}
         Documentation...............:  ${enable_docs}
+        Tests.......................:  ${have_tp_glib_for_tests}
 
 
 "
diff --git a/tests/Makefile.am b/tests/Makefile.am
new file mode 100644
index 0000000..8f7ccdf
--- /dev/null
+++ b/tests/Makefile.am
@@ -0,0 +1,16 @@
+SUBDIRS = \
+    lib \
+    $(NULL)
+
+if HAVE_TP_GLIB_FOR_TESTS
+SUBDIRS += telepathy
+endif
+
+TESTS_ENVIRONMENT = \
+    abs_top_builddir= abs_top_builddir@ \
+    abs_top_srcdir= abs_top_srcdir@ \
+    G_SLICE=debug-blocks \
+    G_DEBUG=fatal_warnings,fatal_criticals \
+    PYTHONPATH= abs_top_srcdir@/tools
+
+-include $(top_srcdir)/git.mk
diff --git a/tests/lib/Makefile.am b/tests/lib/Makefile.am
new file mode 100644
index 0000000..61c997e
--- /dev/null
+++ b/tests/lib/Makefile.am
@@ -0,0 +1,7 @@
+SUBDIRS =
+
+if HAVE_TP_GLIB_FOR_TESTS
+SUBDIRS += telepathy
+endif
+
+-include $(top_srcdir)/git.mk
diff --git a/tests/lib/telepathy/Makefile.am b/tests/lib/telepathy/Makefile.am
new file mode 100644
index 0000000..6a855c1
--- /dev/null
+++ b/tests/lib/telepathy/Makefile.am
@@ -0,0 +1,5 @@
+SUBDIRS = \
+        contactlist \
+        $(NULL)
+
+-include $(top_srcdir)/git.mk
diff --git a/tests/lib/telepathy/contactlist/Makefile.am b/tests/lib/telepathy/contactlist/Makefile.am
new file mode 100644
index 0000000..84eb4ca
--- /dev/null
+++ b/tests/lib/telepathy/contactlist/Makefile.am
@@ -0,0 +1,84 @@
+# Taken from telepathy-glib. The only change is to remove the option to install # the data files.
+#
+# PLEASE DO NOT MODIFY THIS CONNECTION MANAGER. Either subclass it,
+# copy-and-modify (moving it to a better namespace), or make changes in the
+# copy in telepathy-glib first.
+
+VALAFLAGS += \
+	--vapidir=. \
+	--pkg gobject-2.0 \
+	--pkg gio-2.0 \
+	--pkg gee-1.0 \
+	--pkg gmodule-2.0 \
+	--pkg dbus-glib-1 \
+	--pkg telepathy-glib \
+	$(NULL)
+
+noinst_LTLIBRARIES = libtp-test-contactlist.la
+
+libtp_test_contactlist_la_SOURCES = \
+        _gen/param-spec-struct.h \
+        account.c \
+        account.h \
+        account-manager.c \
+        account-manager.h \
+        conn.c \
+        conn.h \
+        contact-list.c \
+        contact-list.h \
+        contact-list-manager.c \
+        contact-list-manager.h \
+	$(NULL)
+
+libtp_test_contactlist_la_CFLAGS = $(TP_GLIB_CFLAGS)
+libtp_test_contactlist_la_LIBADD = $(TP_GLIB_LIBS)
+
+_gen/tp_test_contact_list.manager _gen/param-spec-struct.h: \
+			manager-file.py $(top_srcdir)/tools/manager-file.py
+		$(AM_V_at)$(mkdir_p) _gen
+		$(AM_V_GEN)$(PYTHON) $(top_srcdir)/tools/manager-file.py \
+			$(srcdir)/manager-file.py _gen
+
+DISTCHECK_CONFIGURE_FLAGS = --enable-introspection
+
+-include $(INTROSPECTION_MAKEFILE)
+INTROSPECTION_GIRS =
+INTROSPECTION_SCANNER_ARGS = --add-include-path=$(srcdir)
+INTROSPECTION_COMPILER_ARGS = --includedir=$(srcdir)
+
+tp-test-contactlist.gir: libtp-test-contactlist.la
+tp_test_contactlist_gir_INCLUDES = GObject-2.0 TelepathyGLib-0.12
+tp_test_contactlist_gir_CFLAGS = $(TP_GLIB_CFLAGS)
+tp_test_contactlist_gir_LIBS = libtp-test-contactlist.la
+tp_test_contactlist_gir_FILES = $(libtp_test_contactlist_la_SOURCES)
+tp_test_contactlist_gir_NAMESPACE = TpTest
+INTROSPECTION_GIRS += tp-test-contactlist.gir
+
+tp-test-contactlist.vapi: tp-test-contactlist.gir
+	$(AM_V_GEN)$(VAPIGEN) $(VALAFLAGS) --library tp-test-contactlist \
+		tp-test-contactlist.gir
+
+BUILT_SOURCES = \
+    tp-test-contactlist.vapi \
+    $(NULL)
+
+CLEANFILES = \
+	$(BUILT_SOURCES) \
+	_gen/param-spec-struct.h \
+	_gen/tp_test_contact_list.manager \
+	$(INTROSPECTION_GIRS) \
+	tp-test-contactlist.vapi \
+	session.conf \
+	$(gir_DATA) \
+	$(typelib_DATA) \
+	$(NULL)
+
+EXTRA_DIST = \
+    manager-file.py \
+    tp-test-contactlist.h \
+    $(NULL)
+
+clean-local:
+	rm -rf _gen
+
+-include $(top_srcdir)/git.mk
diff --git a/tests/lib/telepathy/contactlist/account-manager.c b/tests/lib/telepathy/contactlist/account-manager.c
new file mode 100644
index 0000000..1eebc1e
--- /dev/null
+++ b/tests/lib/telepathy/contactlist/account-manager.c
@@ -0,0 +1,188 @@
+/*
+ * account-manager.c - a simple account manager service.
+ *
+ * Copyright (C) 2007-2009 Collabora Ltd. <http://www.collabora.co.uk/>
+ * Copyright (C) 2007-2008 Nokia Corporation
+ *
+ * Copying and distribution of this file, with or without modification,
+ * are permitted in any medium without royalty provided the copyright
+ * notice and this notice are preserved.
+ *
+ * Copied from telepathy-glib/tests/lib/simple-account-manager.c.
+ */
+
+#include "account-manager.h"
+
+#include <telepathy-glib/gtypes.h>
+#include <telepathy-glib/interfaces.h>
+#include <telepathy-glib/svc-generic.h>
+#include <telepathy-glib/svc-account-manager.h>
+
+static void account_manager_iface_init (gpointer, gpointer);
+
+G_DEFINE_TYPE_WITH_CODE (TpTestAccountManager,
+    tp_test_account_manager,
+    G_TYPE_OBJECT,
+    G_IMPLEMENT_INTERFACE (TP_TYPE_SVC_ACCOUNT_MANAGER,
+        account_manager_iface_init);
+    G_IMPLEMENT_INTERFACE (TP_TYPE_SVC_DBUS_PROPERTIES,
+        tp_dbus_properties_mixin_iface_init)
+    )
+
+
+/* TP_IFACE_ACCOUNT_MANAGER is implied */
+static const char *ACCOUNT_MANAGER_INTERFACES[] = { NULL };
+
+static gchar *VALID_ACCOUNTS[] = {
+  "/org/freedesktop/Telepathy/Account/cm/protocol/account",
+  NULL };
+
+static gchar *INVALID_ACCOUNTS[] = {
+  "/org/freedesktop/Telepathy/Account/fakecm/fakeproto/invalidaccount",
+  NULL };
+
+enum
+{
+  PROP_0,
+  PROP_INTERFACES,
+  PROP_VALID_ACCOUNTS,
+  PROP_INVALID_ACCOUNTS,
+};
+
+struct _TpTestAccountManagerPrivate
+{
+  int dummy;
+};
+
+static void
+tp_test_account_manager_create_account (TpSvcAccountManager *self,
+    const gchar *in_Connection_Manager,
+    const gchar *in_Protocol,
+    const gchar *in_Display_Name,
+    GHashTable *in_Parameters,
+    GHashTable *in_Properties,
+    DBusGMethodInvocation *context)
+{
+  const gchar *out_Account = "/some/fake/account/i/think";
+
+  tp_svc_account_manager_return_from_create_account (context, out_Account);
+}
+
+static void
+account_manager_iface_init (gpointer klass,
+    gpointer unused G_GNUC_UNUSED)
+{
+#define IMPLEMENT(x) tp_svc_account_manager_implement_##x (\
+  klass, tp_test_account_manager_##x)
+  IMPLEMENT (create_account);
+#undef IMPLEMENT
+}
+
+
+static void
+tp_test_account_manager_init (TpTestAccountManager *self)
+{
+  self->priv = G_TYPE_INSTANCE_GET_PRIVATE (self,
+      TP_TEST_TYPE_ACCOUNT_MANAGER, TpTestAccountManagerPrivate);
+}
+
+static void
+tp_test_account_manager_get_property (GObject *object,
+              guint property_id,
+              GValue *value,
+              GParamSpec *spec)
+{
+  GPtrArray *accounts;
+  guint i = 0;
+
+  switch (property_id) {
+    case PROP_INTERFACES:
+      g_value_set_boxed (value, ACCOUNT_MANAGER_INTERFACES);
+      break;
+
+    case PROP_VALID_ACCOUNTS:
+      accounts = g_ptr_array_new ();
+
+      for (i=0; VALID_ACCOUNTS[i] != NULL; i++)
+        g_ptr_array_add (accounts, g_strdup (VALID_ACCOUNTS[i]));
+
+      g_value_take_boxed (value, accounts);
+      break;
+
+    case PROP_INVALID_ACCOUNTS:
+      accounts = g_ptr_array_new ();
+
+      for (i=0; INVALID_ACCOUNTS[i] != NULL; i++)
+        g_ptr_array_add (accounts, g_strdup (INVALID_ACCOUNTS[i]));
+
+      g_value_take_boxed (value, accounts);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, spec);
+      break;
+  }
+}
+
+/**
+  * This class currently only provides the minimum for
+  * tp_account_manager_prepare to succeed. This turns out to be only a working
+  * Properties.GetAll(). If we wanted later to check the case where
+  * tp_account_prepare succeeds, we would need to implement an account object
+  * too.
+  */
+static void
+tp_test_account_manager_class_init (
+    TpTestAccountManagerClass *klass)
+{
+  GObjectClass *object_class = (GObjectClass *) klass;
+  GParamSpec *param_spec;
+
+  static TpDBusPropertiesMixinPropImpl am_props[] = {
+        { "Interfaces", "interfaces", NULL },
+        { "ValidAccounts", "valid-accounts", NULL },
+        { "InvalidAccounts", "invalid-accounts", NULL },
+        /*
+        { "SupportedAccountProperties", "supported-account-properties", NULL },
+        */
+        { NULL }
+  };
+
+  static TpDBusPropertiesMixinIfaceImpl prop_interfaces[] = {
+        { TP_IFACE_ACCOUNT_MANAGER,
+          tp_dbus_properties_mixin_getter_gobject_properties,
+          NULL,
+          am_props
+        },
+        { NULL },
+  };
+
+  g_type_class_add_private (klass, sizeof (TpTestAccountManagerPrivate));
+  object_class->get_property = tp_test_account_manager_get_property;
+
+  param_spec = g_param_spec_boxed ("interfaces", "Extra D-Bus interfaces",
+      "In this case we only implement AccountManager, so none.",
+      G_TYPE_STRV,
+      G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+  g_object_class_install_property (object_class, PROP_INTERFACES, param_spec);
+  param_spec = g_param_spec_boxed ("valid-accounts", "Valid accounts",
+      "The accounts which are valid on this account. This may be a lie.",
+      TP_ARRAY_TYPE_OBJECT_PATH_LIST,
+      G_PARAM_READABLE);
+  g_object_class_install_property (object_class, PROP_VALID_ACCOUNTS, param_spec);
+  param_spec = g_param_spec_boxed ("invalid-accounts", "Invalid accounts",
+      "The accounts which are invalid on this account. This may be a lie.",
+      TP_ARRAY_TYPE_OBJECT_PATH_LIST,
+      G_PARAM_READABLE);
+  g_object_class_install_property (object_class, PROP_INVALID_ACCOUNTS, param_spec);
+
+  klass->dbus_props_class.interfaces = prop_interfaces;
+  tp_dbus_properties_mixin_class_init (object_class,
+      G_STRUCT_OFFSET (TpTestAccountManagerClass, dbus_props_class));
+}
+
+TpTestAccountManager *
+tp_test_account_manager_new (void)
+{
+  return g_object_new (TP_TEST_TYPE_ACCOUNT_MANAGER, NULL);
+}
diff --git a/tests/lib/telepathy/contactlist/account-manager.h b/tests/lib/telepathy/contactlist/account-manager.h
new file mode 100644
index 0000000..c35edb5
--- /dev/null
+++ b/tests/lib/telepathy/contactlist/account-manager.h
@@ -0,0 +1,61 @@
+/*
+ * account-manager.h - header for a simple account manager service.
+ *
+ * Copyright (C) 2007-2009 Collabora Ltd. <http://www.collabora.co.uk/>
+ * Copyright (C) 2007-2008 Nokia Corporation
+ *
+ * Copying and distribution of this file, with or without modification,
+ * are permitted in any medium without royalty provided the copyright
+ * notice and this notice are preserved.
+ *
+ * Copied from telepathy-glib/tests/lib/simple-account-manager.h.
+ */
+
+#ifndef __TP_TEST_ACCOUNT_MANAGER_H__
+#define __TP_TEST_ACCOUNT_MANAGER_H__
+
+#include <glib-object.h>
+#include <telepathy-glib/dbus-properties-mixin.h>
+
+
+G_BEGIN_DECLS
+
+typedef struct _TpTestAccountManager TpTestAccountManager;
+typedef struct _TpTestAccountManagerClass TpTestAccountManagerClass;
+typedef struct _TpTestAccountManagerPrivate TpTestAccountManagerPrivate;
+
+struct _TpTestAccountManagerClass {
+    GObjectClass parent_class;
+    TpDBusPropertiesMixinClass dbus_props_class;
+};
+
+struct _TpTestAccountManager {
+    GObject parent;
+
+    TpTestAccountManagerPrivate *priv;
+};
+
+GType tp_test_account_manager_get_type (void);
+
+/* TYPE MACROS */
+#define TP_TEST_TYPE_ACCOUNT_MANAGER \
+  (tp_test_account_manager_get_type ())
+#define TP_TEST_ACCOUNT_MANAGER(obj) \
+  (G_TYPE_CHECK_INSTANCE_CAST((obj), TP_TEST_TYPE_ACCOUNT_MANAGER, \
+                              TpTestAccountManager))
+#define TP_TEST_ACCOUNT_MANAGER_CLASS(klass) \
+  (G_TYPE_CHECK_CLASS_CAST((klass), TP_TEST_TYPE_ACCOUNT_MANAGER, \
+                           TpTestAccountManagerClass))
+#define IS_TP_TEST_ACCOUNT_MANAGER(obj) \
+  (G_TYPE_CHECK_INSTANCE_TYPE((obj), TP_TEST_TYPE_ACCOUNT_MANAGER))
+#define TP_TEST_IS_ACCOUNT_MANAGER_CLASS(klass) \
+  (G_TYPE_CHECK_CLASS_TYPE((klass), TP_TEST_TYPE_ACCOUNT_MANAGER))
+#define TP_TEST_ACCOUNT_MANAGER_GET_CLASS(obj) \
+  (G_TYPE_INSTANCE_GET_CLASS ((obj), TP_TEST_TYPE_ACCOUNT_MANAGER, \
+                              TpTestAccountManagerClass))
+
+TpTestAccountManager *tp_test_account_manager_new (void);
+
+G_END_DECLS
+
+#endif /* #ifndef __TP_TEST_ACCOUNT_MANAGER_H__ */
diff --git a/tests/lib/telepathy/contactlist/account.c b/tests/lib/telepathy/contactlist/account.c
new file mode 100644
index 0000000..6ada9ef
--- /dev/null
+++ b/tests/lib/telepathy/contactlist/account.c
@@ -0,0 +1,341 @@
+/*
+ * account.c - a simple account service.
+ *
+ * Copyright (C) 2010 Collabora Ltd. <http://www.collabora.co.uk/>
+ *
+ * Copying and distribution of this file, with or without modification,
+ * are permitted in any medium without royalty provided the copyright
+ * notice and this notice are preserved.
+ *
+ * Copied from telepathy-glib/tests/lib/simple-account.c.
+ */
+
+#include "account.h"
+
+#include <telepathy-glib/dbus.h>
+#include <telepathy-glib/defs.h>
+#include <telepathy-glib/enums.h>
+#include <telepathy-glib/gtypes.h>
+#include <telepathy-glib/interfaces.h>
+#include <telepathy-glib/util.h>
+#include <telepathy-glib/svc-generic.h>
+#include <telepathy-glib/svc-account.h>
+
+static void account_iface_init (gpointer, gpointer);
+
+G_DEFINE_TYPE_WITH_CODE (TpTestAccount,
+    tp_test_account,
+    G_TYPE_OBJECT,
+    G_IMPLEMENT_INTERFACE (TP_TYPE_SVC_ACCOUNT,
+        account_iface_init);
+    G_IMPLEMENT_INTERFACE (TP_TYPE_SVC_DBUS_PROPERTIES,
+        tp_dbus_properties_mixin_iface_init)
+    )
+
+/* TP_IFACE_ACCOUNT is implied */
+static const char *ACCOUNT_INTERFACES[] = { NULL };
+
+enum
+{
+  PROP_0,
+  PROP_INTERFACES,
+  PROP_DISPLAY_NAME,
+  PROP_ICON,
+  PROP_VALID,
+  PROP_ENABLED,
+  PROP_NICKNAME,
+  PROP_PARAMETERS,
+  PROP_AUTOMATIC_PRESENCE,
+  PROP_CONNECT_AUTO,
+  PROP_CONNECTION,
+  PROP_CONNECTION_STATUS,
+  PROP_CONNECTION_STATUS_REASON,
+  PROP_CURRENT_PRESENCE,
+  PROP_REQUESTED_PRESENCE,
+  PROP_NORMALIZED_NAME,
+  PROP_HAS_BEEN_ONLINE,
+};
+
+struct _TpTestAccountPrivate
+{
+  gchar *connection_path;
+};
+
+static void
+account_iface_init (gpointer klass,
+    gpointer unused G_GNUC_UNUSED)
+{
+#define IMPLEMENT(x) tp_svc_account_implement_##x (\
+  klass, tp_test_account_##x)
+  /* TODO */
+#undef IMPLEMENT
+}
+
+
+static void
+tp_test_account_init (TpTestAccount *self)
+{
+  self->priv = G_TYPE_INSTANCE_GET_PRIVATE (self, TP_TEST_TYPE_ACCOUNT,
+      TpTestAccountPrivate);
+}
+
+static void
+tp_test_account_get_property (GObject *object,
+              guint property_id,
+              GValue *value,
+              GParamSpec *spec)
+{
+  GValueArray *presence;
+  TpTestAccountPrivate *priv = TP_TEST_ACCOUNT (object)->priv;
+
+  presence = tp_value_array_build (3,
+      G_TYPE_UINT, TP_CONNECTION_PRESENCE_TYPE_AVAILABLE,
+      G_TYPE_STRING, "available",
+      G_TYPE_STRING, "",
+      G_TYPE_INVALID);
+
+  switch (property_id) {
+    case PROP_INTERFACES:
+      g_value_set_boxed (value, ACCOUNT_INTERFACES);
+      break;
+    case PROP_DISPLAY_NAME:
+      g_value_set_string (value, "Fake Account");
+      break;
+    case PROP_ICON:
+      g_value_set_string (value, "");
+      break;
+    case PROP_VALID:
+      g_value_set_boolean (value, TRUE);
+      break;
+    case PROP_ENABLED:
+      g_value_set_boolean (value, TRUE);
+      break;
+    case PROP_NICKNAME:
+      g_value_set_string (value, "badger");
+      break;
+    case PROP_PARAMETERS:
+      g_value_take_boxed (value, g_hash_table_new (NULL, NULL));
+      break;
+    case PROP_AUTOMATIC_PRESENCE:
+      g_value_set_boxed (value, presence);
+      break;
+    case PROP_CONNECT_AUTO:
+      g_value_set_boolean (value, FALSE);
+      break;
+    case PROP_CONNECTION:
+      g_value_set_boxed (value, priv->connection_path);
+      break;
+    case PROP_CONNECTION_STATUS:
+      g_value_set_uint (value, TP_CONNECTION_STATUS_CONNECTED);
+      break;
+    case PROP_CONNECTION_STATUS_REASON:
+      g_value_set_uint (value, TP_CONNECTION_STATUS_REASON_REQUESTED);
+      break;
+    case PROP_CURRENT_PRESENCE:
+      g_value_set_boxed (value, presence);
+      break;
+    case PROP_REQUESTED_PRESENCE:
+      g_value_set_boxed (value, presence);
+      break;
+    case PROP_NORMALIZED_NAME:
+      g_value_set_string (value, "");
+      break;
+    case PROP_HAS_BEEN_ONLINE:
+      g_value_set_boolean (value, TRUE);
+      break;
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, spec);
+      break;
+  }
+
+  g_boxed_free (TP_STRUCT_TYPE_SIMPLE_PRESENCE, presence);
+}
+
+static void
+tp_test_account_set_property (GObject *object,
+              guint property_id,
+              GValue *value,
+              GParamSpec *spec)
+{
+  TpTestAccountPrivate *priv = TP_TEST_ACCOUNT (object)->priv;
+
+  switch (property_id) {
+    case PROP_CONNECTION:
+      priv->connection_path = g_strdup (g_value_get_boxed (value));
+      break;
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, spec);
+      break;
+  }
+}
+
+static void
+tp_test_account_finalize (GObject *object)
+{
+  TpTestAccountPrivate *priv = TP_TEST_ACCOUNT (object)->priv;
+
+  g_free (priv->connection_path);
+
+  G_OBJECT_CLASS (tp_test_account_parent_class)->finalize (object);
+}
+
+/**
+  * This class currently only provides the minimum for
+  * tp_account_prepare to succeed. This turns out to be only a working
+  * Properties.GetAll().
+  */
+static void
+tp_test_account_class_init (TpTestAccountClass *klass)
+{
+  GObjectClass *object_class = (GObjectClass *) klass;
+  GParamSpec *param_spec;
+
+  static TpDBusPropertiesMixinPropImpl a_props[] = {
+        { "Interfaces", "interfaces", NULL },
+        { "DisplayName", "display-name", NULL },
+        { "Icon", "icon", NULL },
+        { "Valid", "valid", NULL },
+        { "Enabled", "enabled", NULL },
+        { "Nickname", "nickname", NULL },
+        { "Parameters", "parameters", NULL },
+        { "AutomaticPresence", "automatic-presence", NULL },
+        { "ConnectAutomatically", "connect-automatically", NULL },
+        { "Connection", "connection", NULL },
+        { "ConnectionStatus", "connection-status", NULL },
+        { "ConnectionStatusReason", "connection-status-reason", NULL },
+        { "CurrentPresence", "current-presence", NULL },
+        { "RequestedPresence", "requested-presence", NULL },
+        { "NormalizedName", "normalized-name", NULL },
+        { "HasBeenOnline", "has-been-online", NULL },
+        { NULL }
+  };
+
+  static TpDBusPropertiesMixinIfaceImpl prop_interfaces[] = {
+        { TP_IFACE_ACCOUNT,
+          tp_dbus_properties_mixin_getter_gobject_properties,
+          NULL,
+          a_props
+        },
+        { NULL },
+  };
+
+  g_type_class_add_private (klass, sizeof (TpTestAccountPrivate));
+  object_class->get_property = tp_test_account_get_property;
+  object_class->set_property = tp_test_account_set_property;
+  object_class->finalize = tp_test_account_finalize;
+
+  param_spec = g_param_spec_boxed ("interfaces", "Extra D-Bus interfaces",
+      "In this case we only implement Account, so none.",
+      G_TYPE_STRV,
+      G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+  g_object_class_install_property (object_class, PROP_INTERFACES, param_spec);
+
+  param_spec = g_param_spec_string ("display-name", "display name",
+      "DisplayName property",
+      NULL,
+      G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+  g_object_class_install_property (object_class, PROP_DISPLAY_NAME, param_spec);
+
+  param_spec = g_param_spec_string ("icon", "icon",
+      "Icon property",
+      NULL,
+      G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+  g_object_class_install_property (object_class, PROP_ICON, param_spec);
+
+  param_spec = g_param_spec_boolean ("valid", "valid",
+      "Valid property",
+      FALSE,
+      G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+  g_object_class_install_property (object_class, PROP_VALID, param_spec);
+
+  param_spec = g_param_spec_boolean ("enabled", "enabled",
+      "Enabled property",
+      FALSE,
+      G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+  g_object_class_install_property (object_class, PROP_ENABLED, param_spec);
+
+  param_spec = g_param_spec_string ("nickname", "nickname",
+      "Nickname property",
+      NULL,
+      G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+  g_object_class_install_property (object_class, PROP_NICKNAME, param_spec);
+
+  param_spec = g_param_spec_boxed ("parameters", "parameters",
+      "Parameters property",
+      TP_HASH_TYPE_STRING_VARIANT_MAP,
+      G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+  g_object_class_install_property (object_class, PROP_PARAMETERS, param_spec);
+
+  param_spec = g_param_spec_boxed ("automatic-presence", "automatic presence",
+      "AutomaticPresence property",
+      TP_STRUCT_TYPE_SIMPLE_PRESENCE,
+      G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+  g_object_class_install_property (object_class, PROP_AUTOMATIC_PRESENCE,
+      param_spec);
+
+  param_spec = g_param_spec_boolean ("connect-automatically",
+      "connect automatically", "ConnectAutomatically property",
+      FALSE,
+      G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+  g_object_class_install_property (object_class, PROP_CONNECT_AUTO, param_spec);
+
+  param_spec = g_param_spec_boxed ("connection", "connection",
+      "Connection property",
+      DBUS_TYPE_G_OBJECT_PATH,
+      G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+  g_object_class_install_property (object_class, PROP_CONNECTION, param_spec);
+
+  param_spec = g_param_spec_uint ("connection-status", "connection status",
+      "ConnectionStatus property",
+      0, NUM_TP_CONNECTION_STATUSES, TP_CONNECTION_STATUS_DISCONNECTED,
+      G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+  g_object_class_install_property (object_class, PROP_CONNECTION_STATUS,
+      param_spec);
+
+  param_spec = g_param_spec_uint ("connection-status-reason",
+      "connection status reason", "ConnectionStatusReason property",
+      0, NUM_TP_CONNECTION_STATUS_REASONS,
+      TP_CONNECTION_STATUS_REASON_NONE_SPECIFIED,
+      G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+  g_object_class_install_property (object_class, PROP_CONNECTION_STATUS_REASON,
+      param_spec);
+
+  param_spec = g_param_spec_boxed ("current-presence", "current presence",
+      "CurrentPresence property",
+      TP_STRUCT_TYPE_SIMPLE_PRESENCE,
+      G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+  g_object_class_install_property (object_class, PROP_CURRENT_PRESENCE,
+      param_spec);
+
+  param_spec = g_param_spec_boxed ("requested-presence", "requested presence",
+      "RequestedPresence property",
+      TP_STRUCT_TYPE_SIMPLE_PRESENCE,
+      G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+  g_object_class_install_property (object_class, PROP_REQUESTED_PRESENCE,
+      param_spec);
+
+  param_spec = g_param_spec_string ("normalized-name", "normalized name",
+      "NormalizedName property",
+      NULL,
+      G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+  g_object_class_install_property (object_class, PROP_NORMALIZED_NAME,
+      param_spec);
+
+  param_spec = g_param_spec_boolean ("has-been-online", "has been online",
+      "HasBeenOnline property",
+      FALSE,
+      G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+  g_object_class_install_property (object_class, PROP_HAS_BEEN_ONLINE,
+      param_spec);
+
+  klass->dbus_props_class.interfaces = prop_interfaces;
+  tp_dbus_properties_mixin_class_init (object_class,
+      G_STRUCT_OFFSET (TpTestAccountClass, dbus_props_class));
+}
+
+TpTestAccount *
+tp_test_account_new (const gchar *connection_path)
+{
+  return g_object_new (TP_TEST_TYPE_ACCOUNT,
+      "connection", connection_path, NULL);
+}
diff --git a/tests/lib/telepathy/contactlist/account.h b/tests/lib/telepathy/contactlist/account.h
new file mode 100644
index 0000000..271cbab
--- /dev/null
+++ b/tests/lib/telepathy/contactlist/account.h
@@ -0,0 +1,60 @@
+/*
+ * account.h - header for a simple account service.
+ *
+ * Copyright (C) 2010 Collabora Ltd. <http://www.collabora.co.uk/>
+ *
+ * Copying and distribution of this file, with or without modification,
+ * are permitted in any medium without royalty provided the copyright
+ * notice and this notice are preserved.
+ *
+ * Copied from telepathy-glib/tests/lib/simple-account.h.
+ */
+
+#ifndef __TP_TEST_ACCOUNT_H__
+#define __TP_TEST_ACCOUNT_H__
+
+#include <glib-object.h>
+#include <telepathy-glib/dbus-properties-mixin.h>
+
+
+G_BEGIN_DECLS
+
+typedef struct _TpTestAccount TpTestAccount;
+typedef struct _TpTestAccountClass TpTestAccountClass;
+typedef struct _TpTestAccountPrivate TpTestAccountPrivate;
+
+struct _TpTestAccountClass {
+    GObjectClass parent_class;
+    TpDBusPropertiesMixinClass dbus_props_class;
+};
+
+struct _TpTestAccount {
+    GObject parent;
+
+    TpTestAccountPrivate *priv;
+};
+
+GType tp_test_account_get_type (void);
+
+/* TYPE MACROS */
+#define TP_TEST_TYPE_ACCOUNT \
+  (tp_test_account_get_type ())
+#define TP_TEST_ACCOUNT(obj) \
+  (G_TYPE_CHECK_INSTANCE_CAST((obj), TP_TEST_TYPE_ACCOUNT, \
+                              TpTestAccount))
+#define TP_TEST_ACCOUNT_CLASS(klass) \
+  (G_TYPE_CHECK_CLASS_CAST((klass), TP_TEST_TYPE_ACCOUNT, \
+                           TpTestAccountClass))
+#define TP_TEST_IS_ACCOUNT(obj) \
+  (G_TYPE_CHECK_INSTANCE_TYPE((obj), TP_TEST_TYPE_ACCOUNT))
+#define TP_TEST_IS_ACCOUNT_CLASS(klass) \
+  (G_TYPE_CHECK_CLASS_TYPE((klass), TP_TEST_TYPE_ACCOUNT))
+#define TP_TEST_ACCOUNT_GET_CLASS(obj) \
+  (G_TYPE_INSTANCE_GET_CLASS ((obj), TP_TEST_TYPE_ACCOUNT, \
+                              TpTestAccountClass))
+
+TpTestAccount *tp_test_account_new (const gchar *connection_path);
+
+G_END_DECLS
+
+#endif /* #ifndef __TP_TEST_ACCOUNT_H__ */
diff --git a/tests/lib/telepathy/contactlist/conn.c b/tests/lib/telepathy/contactlist/conn.c
new file mode 100644
index 0000000..2e0677a
--- /dev/null
+++ b/tests/lib/telepathy/contactlist/conn.c
@@ -0,0 +1,614 @@
+/*
+ * conn.c - an tp_test connection
+ *
+ * Copyright © 2007-2009 Collabora Ltd. <http://www.collabora.co.uk/>
+ * Copyright © 2007-2009 Nokia Corporation
+ *
+ * Copying and distribution of this file, with or without modification,
+ * are permitted in any medium without royalty provided the copyright
+ * notice and this notice are preserved.
+ */
+
+#include "conn.h"
+
+#include <string.h>
+
+#include <dbus/dbus-glib.h>
+
+#include <telepathy-glib/telepathy-glib.h>
+#include <telepathy-glib/handle-repo-dynamic.h>
+#include <telepathy-glib/handle-repo-static.h>
+
+#include "contact-list-manager.h"
+
+static void init_aliasing (gpointer, gpointer);
+
+G_DEFINE_TYPE_WITH_CODE (TpTestContactListConnection,
+    tp_test_contact_list_connection,
+    TP_TYPE_BASE_CONNECTION,
+    G_IMPLEMENT_INTERFACE (TP_TYPE_SVC_CONNECTION_INTERFACE_ALIASING,
+      init_aliasing);
+    G_IMPLEMENT_INTERFACE (TP_TYPE_SVC_CONNECTION_INTERFACE_CONTACTS,
+      tp_contacts_mixin_iface_init);
+    G_IMPLEMENT_INTERFACE (TP_TYPE_SVC_CONNECTION_INTERFACE_PRESENCE,
+      tp_presence_mixin_iface_init);
+    G_IMPLEMENT_INTERFACE (TP_TYPE_SVC_CONNECTION_INTERFACE_SIMPLE_PRESENCE,
+      tp_presence_mixin_simple_presence_iface_init))
+
+enum
+{
+  PROP_ACCOUNT = 1,
+  PROP_SIMULATION_DELAY,
+  N_PROPS
+};
+
+struct _TpTestContactListConnectionPrivate
+{
+  gchar *account;
+  guint simulation_delay;
+  TpTestContactListManager *list_manager;
+  gboolean away;
+};
+
+static void
+tp_test_contact_list_connection_init (TpTestContactListConnection *self)
+{
+  self->priv = G_TYPE_INSTANCE_GET_PRIVATE (self,
+      TP_TEST_TYPE_CONTACT_LIST_CONNECTION,
+      TpTestContactListConnectionPrivate);
+}
+
+static void
+get_property (GObject *object,
+              guint property_id,
+              GValue *value,
+              GParamSpec *spec)
+{
+  TpTestContactListConnection *self =
+    TP_TEST_CONTACT_LIST_CONNECTION (object);
+
+  switch (property_id)
+    {
+    case PROP_ACCOUNT:
+      g_value_set_string (value, self->priv->account);
+      break;
+
+    case PROP_SIMULATION_DELAY:
+      g_value_set_uint (value, self->priv->simulation_delay);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, spec);
+    }
+}
+
+static void
+set_property (GObject *object,
+              guint property_id,
+              const GValue *value,
+              GParamSpec *spec)
+{
+  TpTestContactListConnection *self =
+    TP_TEST_CONTACT_LIST_CONNECTION (object);
+
+  switch (property_id)
+    {
+    case PROP_ACCOUNT:
+      g_free (self->priv->account);
+      self->priv->account = g_value_dup_string (value);
+      break;
+
+    case PROP_SIMULATION_DELAY:
+      self->priv->simulation_delay = g_value_get_uint (value);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, spec);
+    }
+}
+
+static void
+finalize (GObject *object)
+{
+  TpTestContactListConnection *self =
+    TP_TEST_CONTACT_LIST_CONNECTION (object);
+
+  tp_contacts_mixin_finalize (object);
+  g_free (self->priv->account);
+
+  G_OBJECT_CLASS (tp_test_contact_list_connection_parent_class)->finalize (
+      object);
+}
+
+static gchar *
+get_unique_connection_name (TpBaseConnection *conn)
+{
+  TpTestContactListConnection *self = TP_TEST_CONTACT_LIST_CONNECTION (conn);
+
+  return g_strdup_printf ("%s %p", self->priv->account, self);
+}
+
+gchar *
+tp_test_contact_list_normalize_contact (TpHandleRepoIface *repo,
+                                        const gchar *id,
+                                        gpointer context,
+                                        GError **error)
+{
+  if (id[0] == '\0')
+    {
+      g_set_error (error, TP_ERRORS, TP_ERROR_INVALID_HANDLE,
+          "Contact ID must not be empty");
+      return NULL;
+    }
+
+  return g_utf8_normalize (id, -1, G_NORMALIZE_ALL_COMPOSE);
+}
+
+static gchar *
+tp_test_contact_list_normalize_group (TpHandleRepoIface *repo,
+                                      const gchar *id,
+                                      gpointer context,
+                                      GError **error)
+{
+  if (id[0] == '\0')
+    {
+      g_set_error (error, TP_ERRORS, TP_ERROR_INVALID_HANDLE,
+          "Contact group name cannot be empty");
+      return NULL;
+    }
+
+  return g_utf8_normalize (id, -1, G_NORMALIZE_ALL_COMPOSE);
+}
+
+static void
+create_handle_repos (TpBaseConnection *conn,
+                     TpHandleRepoIface *repos[NUM_TP_HANDLE_TYPES])
+{
+  repos[TP_HANDLE_TYPE_CONTACT] = tp_dynamic_handle_repo_new
+      (TP_HANDLE_TYPE_CONTACT, tp_test_contact_list_normalize_contact, NULL);
+
+  repos[TP_HANDLE_TYPE_LIST] = tp_static_handle_repo_new
+      (TP_HANDLE_TYPE_LIST, tp_test_contact_lists ());
+
+  repos[TP_HANDLE_TYPE_GROUP] = tp_dynamic_handle_repo_new
+      (TP_HANDLE_TYPE_GROUP, tp_test_contact_list_normalize_group, NULL);
+}
+
+static void
+alias_updated_cb (TpTestContactListManager *manager,
+                  TpHandle contact,
+                  TpTestContactListConnection *self)
+{
+  GPtrArray *aliases;
+  GValueArray *pair;
+
+  pair = g_value_array_new (2);
+  g_value_array_append (pair, NULL);
+  g_value_array_append (pair, NULL);
+  g_value_init (pair->values + 0, G_TYPE_UINT);
+  g_value_init (pair->values + 1, G_TYPE_STRING);
+  g_value_set_uint (pair->values + 0, contact);
+  g_value_set_string (pair->values + 1,
+      tp_test_contact_list_manager_get_alias (manager, contact));
+
+  aliases = g_ptr_array_sized_new (1);
+  g_ptr_array_add (aliases, pair);
+
+  tp_svc_connection_interface_aliasing_emit_aliases_changed (self, aliases);
+
+  g_ptr_array_free (aliases, TRUE);
+  g_value_array_free (pair);
+}
+
+static void
+presence_updated_cb (TpTestContactListManager *manager,
+                     TpHandle contact,
+                     TpTestContactListConnection *self)
+{
+  TpBaseConnection *base = (TpBaseConnection *) self;
+  TpPresenceStatus *status;
+
+  /* we ignore the presence indicated by the contact list for our own handle */
+  if (contact == base->self_handle)
+    return;
+
+  status = tp_presence_status_new (
+      tp_test_contact_list_manager_get_presence (manager, contact),
+      NULL);
+  tp_presence_mixin_emit_one_presence_update ((GObject *) self,
+      contact, status);
+  tp_presence_status_free (status);
+}
+
+static GPtrArray *
+create_channel_managers (TpBaseConnection *conn)
+{
+  TpTestContactListConnection *self =
+    TP_TEST_CONTACT_LIST_CONNECTION (conn);
+  GPtrArray *ret = g_ptr_array_sized_new (1);
+
+  self->priv->list_manager =
+    TP_TEST_CONTACT_LIST_MANAGER (g_object_new (
+          TP_TEST_TYPE_CONTACT_LIST_MANAGER,
+          "connection", conn,
+          "simulation-delay", self->priv->simulation_delay,
+          NULL));
+
+  g_signal_connect (self->priv->list_manager, "alias-updated",
+      G_CALLBACK (alias_updated_cb), self);
+  g_signal_connect (self->priv->list_manager, "presence-updated",
+      G_CALLBACK (presence_updated_cb), self);
+
+  g_ptr_array_add (ret, self->priv->list_manager);
+
+  return ret;
+}
+
+static gboolean
+start_connecting (TpBaseConnection *conn,
+                  GError **error)
+{
+  TpTestContactListConnection *self = TP_TEST_CONTACT_LIST_CONNECTION (conn);
+  TpHandleRepoIface *contact_repo = tp_base_connection_get_handles (conn,
+      TP_HANDLE_TYPE_CONTACT);
+
+  /* In a real connection manager we'd ask the underlying implementation to
+   * start connecting, then go to state CONNECTED when finished, but here
+   * we can do it immediately. */
+
+  conn->self_handle = tp_handle_ensure (contact_repo, self->priv->account,
+      NULL, error);
+
+  if (conn->self_handle == 0)
+    return FALSE;
+
+  tp_base_connection_change_status (conn, TP_CONNECTION_STATUS_CONNECTED,
+      TP_CONNECTION_STATUS_REASON_REQUESTED);
+
+  return TRUE;
+}
+
+static void
+shut_down (TpBaseConnection *conn)
+{
+  /* In a real connection manager we'd ask the underlying implementation to
+   * start shutting down, then call this function when finished, but here
+   * we can do it immediately. */
+  tp_base_connection_finish_shutdown (conn);
+}
+
+static void
+aliasing_fill_contact_attributes (GObject *object,
+                                  const GArray *contacts,
+                                  GHashTable *attributes)
+{
+  TpTestContactListConnection *self =
+    TP_TEST_CONTACT_LIST_CONNECTION (object);
+  guint i;
+
+  for (i = 0; i < contacts->len; i++)
+    {
+      TpHandle contact = g_array_index (contacts, guint, i);
+
+      tp_contacts_mixin_set_contact_attribute (attributes, contact,
+          TP_TOKEN_CONNECTION_INTERFACE_ALIASING_ALIAS,
+          tp_g_value_slice_new_string (
+            tp_test_contact_list_manager_get_alias (self->priv->list_manager,
+              contact)));
+    }
+}
+
+static void
+constructed (GObject *object)
+{
+  TpBaseConnection *base = TP_BASE_CONNECTION (object);
+  void (*chain_up) (GObject *) =
+    G_OBJECT_CLASS (tp_test_contact_list_connection_parent_class)->constructed;
+
+  if (chain_up != NULL)
+    chain_up (object);
+
+  tp_contacts_mixin_init (object,
+      G_STRUCT_OFFSET (TpTestContactListConnection, contacts_mixin));
+  tp_base_connection_register_with_contacts_mixin (base);
+  tp_contacts_mixin_add_contact_attributes_iface (object,
+      TP_IFACE_CONNECTION_INTERFACE_ALIASING,
+      aliasing_fill_contact_attributes);
+
+  tp_presence_mixin_init (object,
+      G_STRUCT_OFFSET (TpTestContactListConnection, presence_mixin));
+  tp_presence_mixin_simple_presence_register_with_contacts_mixin (object);
+}
+
+static gboolean
+status_available (GObject *object,
+                  guint index_)
+{
+  TpBaseConnection *base = TP_BASE_CONNECTION (object);
+
+  if (base->status != TP_CONNECTION_STATUS_CONNECTED)
+    return FALSE;
+
+  return TRUE;
+}
+
+static GHashTable *
+get_contact_statuses (GObject *object,
+                      const GArray *contacts,
+                      GError **error)
+{
+  TpTestContactListConnection *self =
+    TP_TEST_CONTACT_LIST_CONNECTION (object);
+  TpBaseConnection *base = TP_BASE_CONNECTION (object);
+  guint i;
+  GHashTable *result = g_hash_table_new_full (g_direct_hash, g_direct_equal,
+      NULL, (GDestroyNotify) tp_presence_status_free);
+
+  for (i = 0; i < contacts->len; i++)
+    {
+      TpHandle contact = g_array_index (contacts, guint, i);
+      TpTestContactListPresence presence;
+      GHashTable *parameters;
+
+      /* we get our own status from the connection, and everyone else's status
+       * from the contact lists */
+      if (contact == base->self_handle)
+        {
+          presence = (self->priv->away ? TP_TEST_CONTACT_LIST_PRESENCE_AWAY
+              : TP_TEST_CONTACT_LIST_PRESENCE_AVAILABLE);
+        }
+      else
+        {
+          presence = tp_test_contact_list_manager_get_presence (
+              self->priv->list_manager, contact);
+        }
+
+      parameters = g_hash_table_new_full (g_str_hash,
+          g_str_equal, NULL, (GDestroyNotify) tp_g_value_slice_free);
+      g_hash_table_insert (result, GUINT_TO_POINTER (contact),
+          tp_presence_status_new (presence, parameters));
+      g_hash_table_destroy (parameters);
+    }
+
+  return result;
+}
+
+static gboolean
+set_own_status (GObject *object,
+                const TpPresenceStatus *status,
+                GError **error)
+{
+  TpTestContactListConnection *self =
+    TP_TEST_CONTACT_LIST_CONNECTION (object);
+  TpBaseConnection *base = TP_BASE_CONNECTION (object);
+  GHashTable *presences;
+
+  if (status->index == TP_TEST_CONTACT_LIST_PRESENCE_AWAY)
+    {
+      if (self->priv->away)
+        return TRUE;
+
+      self->priv->away = TRUE;
+    }
+  else
+    {
+      if (!self->priv->away)
+        return TRUE;
+
+      self->priv->away = FALSE;
+    }
+
+  presences = g_hash_table_new_full (g_direct_hash, g_direct_equal,
+      NULL, NULL);
+  g_hash_table_insert (presences, GUINT_TO_POINTER (base->self_handle),
+      (gpointer) status);
+  tp_presence_mixin_emit_presence_update (object, presences);
+  g_hash_table_destroy (presences);
+  return TRUE;
+}
+
+static void
+tp_test_contact_list_connection_class_init (
+    TpTestContactListConnectionClass *klass)
+{
+  static const gchar *interfaces_always_present[] = {
+      TP_IFACE_CONNECTION_INTERFACE_ALIASING,
+      TP_IFACE_CONNECTION_INTERFACE_CONTACTS,
+      TP_IFACE_CONNECTION_INTERFACE_PRESENCE,
+      TP_IFACE_CONNECTION_INTERFACE_REQUESTS,
+      TP_IFACE_CONNECTION_INTERFACE_SIMPLE_PRESENCE,
+      NULL };
+  TpBaseConnectionClass *base_class = (TpBaseConnectionClass *) klass;
+  GObjectClass *object_class = (GObjectClass *) klass;
+  GParamSpec *param_spec;
+
+  object_class->get_property = get_property;
+  object_class->set_property = set_property;
+  object_class->constructed = constructed;
+  object_class->finalize = finalize;
+  g_type_class_add_private (klass,
+      sizeof (TpTestContactListConnectionPrivate));
+
+  base_class->create_handle_repos = create_handle_repos;
+  base_class->get_unique_connection_name = get_unique_connection_name;
+  base_class->create_channel_managers = create_channel_managers;
+  base_class->start_connecting = start_connecting;
+  base_class->shut_down = shut_down;
+  base_class->interfaces_always_present = interfaces_always_present;
+
+  param_spec = g_param_spec_string ("account", "Account name",
+      "The username of this user", NULL,
+      G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE |
+      G_PARAM_STATIC_NAME | G_PARAM_STATIC_NICK | G_PARAM_STATIC_BLURB);
+  g_object_class_install_property (object_class, PROP_ACCOUNT, param_spec);
+
+  param_spec = g_param_spec_uint ("simulation-delay", "Simulation delay",
+      "Delay between simulated network events",
+      0, G_MAXUINT32, 1000,
+      G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+  g_object_class_install_property (object_class, PROP_SIMULATION_DELAY,
+      param_spec);
+
+  tp_contacts_mixin_class_init (object_class,
+      G_STRUCT_OFFSET (TpTestContactListConnectionClass, contacts_mixin));
+  tp_presence_mixin_class_init (object_class,
+      G_STRUCT_OFFSET (TpTestContactListConnectionClass, presence_mixin),
+      status_available, get_contact_statuses, set_own_status,
+      tp_test_contact_list_presence_statuses ());
+  tp_presence_mixin_simple_presence_init_dbus_properties (object_class);
+}
+
+static void
+get_alias_flags (TpSvcConnectionInterfaceAliasing *aliasing,
+                 DBusGMethodInvocation *context)
+{
+  TpBaseConnection *base = TP_BASE_CONNECTION (aliasing);
+
+  TP_BASE_CONNECTION_ERROR_IF_NOT_CONNECTED (base, context);
+  tp_svc_connection_interface_aliasing_return_from_get_alias_flags (context,
+      TP_CONNECTION_ALIAS_FLAG_USER_SET);
+}
+
+static void
+get_aliases (TpSvcConnectionInterfaceAliasing *aliasing,
+             const GArray *contacts,
+             DBusGMethodInvocation *context)
+{
+  TpTestContactListConnection *self =
+    TP_TEST_CONTACT_LIST_CONNECTION (aliasing);
+  TpBaseConnection *base = TP_BASE_CONNECTION (aliasing);
+  TpHandleRepoIface *contact_repo = tp_base_connection_get_handles (base,
+      TP_HANDLE_TYPE_CONTACT);
+  GHashTable *result;
+  GError *error = NULL;
+  guint i;
+
+  TP_BASE_CONNECTION_ERROR_IF_NOT_CONNECTED (base, context);
+
+  if (!tp_handles_are_valid (contact_repo, contacts, FALSE, &error))
+    {
+      dbus_g_method_return_error (context, error);
+      g_error_free (error);
+      return;
+    }
+
+  result = g_hash_table_new_full (g_direct_hash, g_direct_equal, NULL, NULL);
+
+  for (i = 0; i < contacts->len; i++)
+    {
+      TpHandle contact = g_array_index (contacts, TpHandle, i);
+      const gchar *alias = tp_test_contact_list_manager_get_alias (
+          self->priv->list_manager, contact);
+
+      g_hash_table_insert (result, GUINT_TO_POINTER (contact),
+          (gchar *) alias);
+    }
+
+  tp_svc_connection_interface_aliasing_return_from_get_aliases (context,
+      result);
+  g_hash_table_destroy (result);
+}
+
+static void
+request_aliases (TpSvcConnectionInterfaceAliasing *aliasing,
+                 const GArray *contacts,
+                 DBusGMethodInvocation *context)
+{
+  TpTestContactListConnection *self =
+    TP_TEST_CONTACT_LIST_CONNECTION (aliasing);
+  TpBaseConnection *base = TP_BASE_CONNECTION (aliasing);
+  TpHandleRepoIface *contact_repo = tp_base_connection_get_handles (base,
+      TP_HANDLE_TYPE_CONTACT);
+  GPtrArray *result;
+  gchar **strings;
+  GError *error = NULL;
+  guint i;
+
+  TP_BASE_CONNECTION_ERROR_IF_NOT_CONNECTED (base, context);
+
+  if (!tp_handles_are_valid (contact_repo, contacts, FALSE, &error))
+    {
+      dbus_g_method_return_error (context, error);
+      g_error_free (error);
+      return;
+    }
+
+  result = g_ptr_array_sized_new (contacts->len + 1);
+
+  for (i = 0; i < contacts->len; i++)
+    {
+      TpHandle contact = g_array_index (contacts, TpHandle, i);
+      const gchar *alias = tp_test_contact_list_manager_get_alias (
+          self->priv->list_manager, contact);
+
+      g_ptr_array_add (result, (gchar *) alias);
+    }
+
+  g_ptr_array_add (result, NULL);
+  strings = (gchar **) g_ptr_array_free (result, FALSE);
+  tp_svc_connection_interface_aliasing_return_from_request_aliases (context,
+      (const gchar **) strings);
+  g_free (strings);
+}
+
+static void
+set_aliases (TpSvcConnectionInterfaceAliasing *aliasing,
+             GHashTable *aliases,
+             DBusGMethodInvocation *context)
+{
+  TpTestContactListConnection *self =
+    TP_TEST_CONTACT_LIST_CONNECTION (aliasing);
+  TpBaseConnection *base = TP_BASE_CONNECTION (aliasing);
+  TpHandleRepoIface *contact_repo = tp_base_connection_get_handles (base,
+      TP_HANDLE_TYPE_CONTACT);
+  GHashTableIter iter;
+  gpointer key, value;
+
+  g_hash_table_iter_init (&iter, aliases);
+
+  while (g_hash_table_iter_next (&iter, &key, &value))
+    {
+      GError *error = NULL;
+
+      if (!tp_handle_is_valid (contact_repo, GPOINTER_TO_UINT (key),
+            &error))
+        {
+          dbus_g_method_return_error (context, error);
+          g_error_free (error);
+          return;
+        }
+    }
+
+  g_hash_table_iter_init (&iter, aliases);
+
+  while (g_hash_table_iter_next (&iter, &key, &value))
+    {
+      tp_test_contact_list_manager_set_alias (self->priv->list_manager,
+          GPOINTER_TO_UINT (key), value);
+    }
+
+  tp_svc_connection_interface_aliasing_return_from_set_aliases (context);
+}
+
+static void
+init_aliasing (gpointer iface,
+               gpointer iface_data G_GNUC_UNUSED)
+{
+  TpSvcConnectionInterfaceAliasingClass *klass = iface;
+
+#define IMPLEMENT(x) tp_svc_connection_interface_aliasing_implement_##x (\
+    klass, x)
+  IMPLEMENT(get_alias_flags);
+  IMPLEMENT(request_aliases);
+  IMPLEMENT(get_aliases);
+  IMPLEMENT(set_aliases);
+#undef IMPLEMENT
+}
+
+TpTestContactListConnection *
+tp_test_contact_list_connection_new (const gchar *account,
+    const gchar *protocol)
+{
+  return g_object_new (TP_TEST_TYPE_CONTACT_LIST_CONNECTION, "account", account,
+      "protocol", protocol, NULL);
+}
diff --git a/tests/lib/telepathy/contactlist/conn.h b/tests/lib/telepathy/contactlist/conn.h
new file mode 100644
index 0000000..0d46d67
--- /dev/null
+++ b/tests/lib/telepathy/contactlist/conn.h
@@ -0,0 +1,68 @@
+/*
+ * conn.h - header for an example connection
+ *
+ * Copyright © 2007-2009 Collabora Ltd. <http://www.collabora.co.uk/>
+ * Copyright © 2007-2009 Nokia Corporation
+ *
+ * Copying and distribution of this file, with or without modification,
+ * are permitted in any medium without royalty provided the copyright
+ * notice and this notice are preserved.
+ */
+
+#ifndef __TP_TEST_CONTACT_LIST_CONN_H__
+#define __TP_TEST_CONTACT_LIST_CONN_H__
+
+#include <glib-object.h>
+#include <telepathy-glib/base-connection.h>
+#include <telepathy-glib/contacts-mixin.h>
+#include <telepathy-glib/presence-mixin.h>
+
+G_BEGIN_DECLS
+
+typedef struct _TpTestContactListConnection TpTestContactListConnection;
+typedef struct _TpTestContactListConnectionClass
+    TpTestContactListConnectionClass;
+typedef struct _TpTestContactListConnectionPrivate
+    TpTestContactListConnectionPrivate;
+
+struct _TpTestContactListConnectionClass {
+    TpBaseConnectionClass parent_class;
+    TpPresenceMixinClass presence_mixin;
+    TpContactsMixinClass contacts_mixin;
+};
+
+struct _TpTestContactListConnection {
+    TpBaseConnection parent;
+    TpPresenceMixin presence_mixin;
+    TpContactsMixin contacts_mixin;
+
+    TpTestContactListConnectionPrivate *priv;
+};
+
+GType tp_test_contact_list_connection_get_type (void);
+
+#define TP_TEST_TYPE_CONTACT_LIST_CONNECTION \
+  (tp_test_contact_list_connection_get_type ())
+#define TP_TEST_CONTACT_LIST_CONNECTION(obj) \
+  (G_TYPE_CHECK_INSTANCE_CAST((obj), TP_TEST_TYPE_CONTACT_LIST_CONNECTION, \
+                              TpTestContactListConnection))
+#define TP_TEST_CONTACT_LIST_CONNECTION_CLASS(klass) \
+  (G_TYPE_CHECK_CLASS_CAST((klass), TP_TEST_TYPE_CONTACT_LIST_CONNECTION, \
+                           TpTestContactListConnectionClass))
+#define TP_TEST_IS_CONTACT_LIST_CONNECTION(obj) \
+  (G_TYPE_CHECK_INSTANCE_TYPE((obj), TP_TEST_TYPE_CONTACT_LIST_CONNECTION))
+#define TP_TEST_IS_CONTACT_LIST_CONNECTION_CLASS(klass) \
+  (G_TYPE_CHECK_CLASS_TYPE((klass), TP_TEST_TYPE_CONTACT_LIST_CONNECTION))
+#define TP_TEST_CONTACT_LIST_CONNECTION_GET_CLASS(obj) \
+  (G_TYPE_INSTANCE_GET_CLASS ((obj), TP_TEST_TYPE_CONTACT_LIST_CONNECTION, \
+                              TpTestContactListConnectionClass))
+
+gchar *tp_test_contact_list_normalize_contact (TpHandleRepoIface *repo,
+    const gchar *id, gpointer context, GError **error);
+
+TpTestContactListConnection *tp_test_contact_list_connection_new (
+    const gchar *account, const gchar *protocol);
+
+G_END_DECLS
+
+#endif
diff --git a/tests/lib/telepathy/contactlist/contact-list-manager.c b/tests/lib/telepathy/contactlist/contact-list-manager.c
new file mode 100644
index 0000000..317dfcd
--- /dev/null
+++ b/tests/lib/telepathy/contactlist/contact-list-manager.c
@@ -0,0 +1,1649 @@
+/*
+ * Example channel manager for contact lists
+ *
+ * Copyright © 2007-2009 Collabora Ltd. <http://www.collabora.co.uk/>
+ * Copyright © 2007-2009 Nokia Corporation
+ *
+ * Copying and distribution of this file, with or without modification,
+ * are permitted in any medium without royalty provided the copyright
+ * notice and this notice are preserved.
+ */
+
+#include "contact-list-manager.h"
+
+#include <string.h>
+
+#include <dbus/dbus-glib.h>
+
+#include <telepathy-glib/telepathy-glib.h>
+
+#include "contact-list.h"
+
+/* elements 0, 1... of this array must be kept in sync with elements 1, 2...
+ * of the enum TpTestContactList in contact-list-manager.h */
+static const gchar *_contact_lists[NUM_TP_TEST_CONTACT_LISTS + 1] = {
+    "subscribe",
+    "publish",
+    "stored",
+    NULL
+};
+
+const gchar **
+tp_test_contact_lists (void)
+{
+  return _contact_lists;
+}
+
+/* this array must be kept in sync with the enum
+ * TpTestContactListPresence in contact-list-manager.h */
+static const TpPresenceStatusSpec _statuses[] = {
+      { "offline", TP_CONNECTION_PRESENCE_TYPE_OFFLINE, FALSE, NULL },
+      { "unknown", TP_CONNECTION_PRESENCE_TYPE_UNKNOWN, FALSE, NULL },
+      { "error", TP_CONNECTION_PRESENCE_TYPE_ERROR, FALSE, NULL },
+      { "away", TP_CONNECTION_PRESENCE_TYPE_AWAY, TRUE, NULL },
+      { "available", TP_CONNECTION_PRESENCE_TYPE_AVAILABLE, TRUE, NULL },
+      { NULL }
+};
+
+const TpPresenceStatusSpec *
+tp_test_contact_list_presence_statuses (void)
+{
+  return _statuses;
+}
+
+typedef struct {
+    gchar *alias;
+
+    guint subscribe:1;
+    guint publish:1;
+    guint subscribe_requested:1;
+    guint publish_requested:1;
+
+    TpHandleSet *tags;
+
+} TpTestContactDetails;
+
+static TpTestContactDetails *
+tp_test_contact_details_new (void)
+{
+  return g_slice_new0 (TpTestContactDetails);
+}
+
+static void
+tp_test_contact_details_destroy (gpointer p)
+{
+  TpTestContactDetails *d = p;
+
+  if (d->tags != NULL)
+    tp_handle_set_destroy (d->tags);
+
+  g_free (d->alias);
+  g_slice_free (TpTestContactDetails, d);
+}
+
+static void channel_manager_iface_init (gpointer, gpointer);
+
+G_DEFINE_TYPE_WITH_CODE (TpTestContactListManager,
+    tp_test_contact_list_manager,
+    G_TYPE_OBJECT,
+    G_IMPLEMENT_INTERFACE (TP_TYPE_CHANNEL_MANAGER,
+      channel_manager_iface_init))
+
+enum
+{
+  ALIAS_UPDATED,
+  PRESENCE_UPDATED,
+  N_SIGNALS
+};
+
+static guint signals[N_SIGNALS] = { 0 };
+
+enum
+{
+  PROP_CONNECTION = 1,
+  PROP_SIMULATION_DELAY,
+  N_PROPS
+};
+
+struct _TpTestContactListManagerPrivate
+{
+  TpBaseConnection *conn;
+  guint simulation_delay;
+  TpHandleRepoIface *contact_repo;
+  TpHandleRepoIface *group_repo;
+
+  TpHandleSet *contacts;
+  /* GUINT_TO_POINTER (handle borrowed from contacts)
+   *    => TpTestContactDetails */
+  GHashTable *contact_details;
+
+  TpTestContactList *lists[NUM_TP_TEST_CONTACT_LISTS];
+
+  /* GUINT_TO_POINTER (handle borrowed from channel) => TpTestContactGroup */
+  GHashTable *groups;
+
+  /* borrowed TpExportableChannel => GSList of gpointer (request tokens) that
+   * will be satisfied by that channel when the contact list has been
+   * downloaded. The requests are in reverse chronological order */
+  GHashTable *queued_requests;
+
+  gulong status_changed_id;
+};
+
+static void
+tp_test_contact_list_manager_init (TpTestContactListManager *self)
+{
+  self->priv = G_TYPE_INSTANCE_GET_PRIVATE (self,
+      TP_TEST_TYPE_CONTACT_LIST_MANAGER, TpTestContactListManagerPrivate);
+
+  self->priv->contact_details = g_hash_table_new_full (g_direct_hash,
+      g_direct_equal, NULL, tp_test_contact_details_destroy);
+  self->priv->groups = g_hash_table_new_full (g_direct_hash, g_direct_equal,
+      NULL, g_object_unref);
+  self->priv->queued_requests = g_hash_table_new_full (g_direct_hash,
+      g_direct_equal, NULL, NULL);
+
+  /* initialized properly in constructed() */
+  self->priv->contact_repo = NULL;
+  self->priv->group_repo = NULL;
+  self->priv->contacts = NULL;
+}
+
+static void
+tp_test_contact_list_manager_close_all (TpTestContactListManager *self)
+{
+  guint i;
+
+  if (self->priv->queued_requests != NULL)
+    {
+      GHashTable *tmp = self->priv->queued_requests;
+      GHashTableIter iter;
+      gpointer key, value;
+
+      self->priv->queued_requests = NULL;
+      g_hash_table_iter_init (&iter, tmp);
+
+      while (g_hash_table_iter_next (&iter, &key, &value))
+        {
+          GSList *requests = value;
+          GSList *l;
+
+          requests = g_slist_reverse (requests);
+
+          for (l = requests; l != NULL; l = l->next)
+            {
+              tp_channel_manager_emit_request_failed (self,
+                  l->data, TP_ERRORS, TP_ERROR_DISCONNECTED,
+                  "Unable to complete channel request due to disconnection");
+            }
+
+          g_slist_free (requests);
+          g_hash_table_iter_steal (&iter);
+        }
+
+      g_hash_table_destroy (tmp);
+    }
+
+  if (self->priv->contacts != NULL)
+    {
+      tp_handle_set_destroy (self->priv->contacts);
+      self->priv->contacts = NULL;
+    }
+
+  if (self->priv->contact_details != NULL)
+    {
+      GHashTable *tmp = self->priv->contact_details;
+
+      self->priv->contact_details = NULL;
+      g_hash_table_destroy (tmp);
+    }
+
+  if (self->priv->groups != NULL)
+    {
+      GHashTable *tmp = self->priv->groups;
+
+      self->priv->groups = NULL;
+      g_hash_table_destroy (tmp);
+    }
+
+  for (i = 0; i < NUM_TP_TEST_CONTACT_LISTS; i++)
+    {
+      if (self->priv->lists[i] != NULL)
+        {
+          TpTestContactList *list = self->priv->lists[i];
+
+          /* set self->priv->lists[i] to NULL here so list_closed_cb does
+           * not try to delete the list again */
+          self->priv->lists[i] = NULL;
+          g_object_unref (list);
+        }
+    }
+
+  if (self->priv->status_changed_id != 0)
+    {
+      g_signal_handler_disconnect (self->priv->conn,
+          self->priv->status_changed_id);
+      self->priv->status_changed_id = 0;
+    }
+}
+
+static void
+dispose (GObject *object)
+{
+  TpTestContactListManager *self = TP_TEST_CONTACT_LIST_MANAGER (object);
+
+  tp_test_contact_list_manager_close_all (self);
+  g_assert (self->priv->groups == NULL);
+  g_assert (self->priv->lists[0] == NULL);
+  g_assert (self->priv->queued_requests == NULL);
+
+  ((GObjectClass *) tp_test_contact_list_manager_parent_class)->dispose (
+    object);
+}
+
+static void
+get_property (GObject *object,
+              guint property_id,
+              GValue *value,
+              GParamSpec *pspec)
+{
+  TpTestContactListManager *self = TP_TEST_CONTACT_LIST_MANAGER (object);
+
+  switch (property_id)
+    {
+    case PROP_CONNECTION:
+      g_value_set_object (value, self->priv->conn);
+      break;
+
+    case PROP_SIMULATION_DELAY:
+      g_value_set_uint (value, self->priv->simulation_delay);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+    }
+}
+
+static void
+set_property (GObject *object,
+              guint property_id,
+              const GValue *value,
+              GParamSpec *pspec)
+{
+  TpTestContactListManager *self = TP_TEST_CONTACT_LIST_MANAGER (object);
+
+  switch (property_id)
+    {
+    case PROP_CONNECTION:
+      /* We don't ref the connection, because it owns a reference to the
+       * manager, and it guarantees that the manager's lifetime is
+       * less than its lifetime */
+      self->priv->conn = g_value_get_object (value);
+      break;
+
+    case PROP_SIMULATION_DELAY:
+      self->priv->simulation_delay = g_value_get_uint (value);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+    }
+}
+
+static void
+satisfy_queued_requests (TpExportableChannel *channel,
+                         gpointer user_data)
+{
+  TpTestContactListManager *self = TP_TEST_CONTACT_LIST_MANAGER (user_data);
+  GSList *requests = g_hash_table_lookup (self->priv->queued_requests,
+      channel);
+
+  /* this is all fine even if requests is NULL */
+  g_hash_table_steal (self->priv->queued_requests, channel);
+  requests = g_slist_reverse (requests);
+  tp_channel_manager_emit_new_channel (self, channel, requests);
+  g_slist_free (requests);
+}
+
+static TpTestContactDetails *
+lookup_contact (TpTestContactListManager *self,
+                TpHandle contact)
+{
+  return g_hash_table_lookup (self->priv->contact_details,
+      GUINT_TO_POINTER (contact));
+}
+
+static TpTestContactDetails *
+ensure_contact (TpTestContactListManager *self,
+                TpHandle contact,
+                gboolean *created)
+{
+  TpTestContactDetails *ret = lookup_contact (self, contact);
+
+  if (ret == NULL)
+    {
+      tp_handle_set_add (self->priv->contacts, contact);
+
+      ret = tp_test_contact_details_new ();
+      ret->alias = g_strdup (tp_handle_inspect (self->priv->contact_repo,
+            contact));
+
+      g_hash_table_insert (self->priv->contact_details,
+          GUINT_TO_POINTER (contact), ret);
+
+      if (created != NULL)
+        *created = TRUE;
+    }
+  else if (created != NULL)
+    {
+      *created = FALSE;
+    }
+
+  return ret;
+}
+
+static void
+tp_test_contact_list_manager_foreach_channel (TpChannelManager *manager,
+                                              TpExportableChannelFunc callback,
+                                              gpointer user_data)
+{
+  TpTestContactListManager *self = TP_TEST_CONTACT_LIST_MANAGER (manager);
+  GHashTableIter iter;
+  gpointer handle, channel;
+  guint i;
+
+  for (i = 0; i < NUM_TP_TEST_CONTACT_LISTS; i++)
+    {
+      if (self->priv->lists[i] != NULL)
+        callback (TP_EXPORTABLE_CHANNEL (self->priv->lists[i]), user_data);
+    }
+
+  g_hash_table_iter_init (&iter, self->priv->groups);
+
+  while (g_hash_table_iter_next (&iter, &handle, &channel))
+    {
+      callback (TP_EXPORTABLE_CHANNEL (channel), user_data);
+    }
+}
+
+static TpTestContactGroup *ensure_group (TpTestContactListManager *self,
+    TpHandle handle);
+
+static TpTestContactList *ensure_list (TpTestContactListManager *self,
+    TpTestContactListHandle handle);
+
+static gboolean
+receive_contact_lists (gpointer p)
+{
+  TpTestContactListManager *self = p;
+  TpHandle handle, cambridge, montreal, francophones;
+  TpTestContactDetails *d;
+  TpIntSet *set, *cam_set, *mtl_set, *fr_set;
+  TpIntSetFastIter iter;
+  TpTestContactList *subscribe, *publish, *stored;
+  TpTestContactGroup *cambridge_group, *montreal_group,
+      *francophones_group;
+
+  if (self->priv->groups == NULL)
+    {
+      /* connection already disconnected, so don't process the
+       * "data from the server" */
+      return FALSE;
+    }
+
+  /* In a real CM we'd have received a contact list from the server at this
+   * point. But this isn't a real CM, so we have to make one up... */
+
+  g_message ("Receiving roster from server");
+
+  subscribe = ensure_list (self, TP_TEST_CONTACT_LIST_SUBSCRIBE);
+  publish = ensure_list (self, TP_TEST_CONTACT_LIST_PUBLISH);
+  stored = ensure_list (self, TP_TEST_CONTACT_LIST_STORED);
+
+  cambridge = tp_handle_ensure (self->priv->group_repo, "Cambridge", NULL,
+      NULL);
+  montreal = tp_handle_ensure (self->priv->group_repo, "Montreal", NULL,
+      NULL);
+  francophones = tp_handle_ensure (self->priv->group_repo, "Francophones",
+      NULL, NULL);
+
+  cambridge_group = ensure_group (self, cambridge);
+  montreal_group = ensure_group (self, montreal);
+  francophones_group = ensure_group (self, francophones);
+
+  /* Add various people who are already subscribing and publishing */
+
+  set = tp_intset_new ();
+  cam_set = tp_intset_new ();
+  mtl_set = tp_intset_new ();
+  fr_set = tp_intset_new ();
+
+  handle = tp_handle_ensure (self->priv->contact_repo, "sjoerd example com",
+      NULL, NULL);
+  tp_intset_add (set, handle);
+  tp_intset_add (cam_set, handle);
+  d = ensure_contact (self, handle, NULL);
+  g_free (d->alias);
+  d->alias = g_strdup ("Sjoerd");
+  d->subscribe = TRUE;
+  d->publish = TRUE;
+  d->tags = tp_handle_set_new (self->priv->group_repo);
+  tp_handle_set_add (d->tags, cambridge);
+  tp_handle_unref (self->priv->contact_repo, handle);
+
+  handle = tp_handle_ensure (self->priv->contact_repo, "guillaume example com",
+      NULL, NULL);
+  tp_intset_add (set, handle);
+  tp_intset_add (cam_set, handle);
+  tp_intset_add (fr_set, handle);
+  d = ensure_contact (self, handle, NULL);
+  g_free (d->alias);
+  d->alias = g_strdup ("Guillaume");
+  d->subscribe = TRUE;
+  d->publish = TRUE;
+  d->tags = tp_handle_set_new (self->priv->group_repo);
+  tp_handle_set_add (d->tags, cambridge);
+  tp_handle_set_add (d->tags, francophones);
+  tp_handle_unref (self->priv->contact_repo, handle);
+
+  handle = tp_handle_ensure (self->priv->contact_repo, "olivier example com",
+      NULL, NULL);
+  tp_intset_add (set, handle);
+  tp_intset_add (mtl_set, handle);
+  tp_intset_add (fr_set, handle);
+  d = ensure_contact (self, handle, NULL);
+  g_free (d->alias);
+  d->alias = g_strdup ("Olivier");
+  d->subscribe = TRUE;
+  d->publish = TRUE;
+  d->tags = tp_handle_set_new (self->priv->group_repo);
+  tp_handle_set_add (d->tags, montreal);
+  tp_handle_set_add (d->tags, francophones);
+  tp_handle_unref (self->priv->contact_repo, handle);
+
+  handle = tp_handle_ensure (self->priv->contact_repo, "travis example com",
+      NULL, NULL);
+  tp_intset_add (set, handle);
+  d = ensure_contact (self, handle, NULL);
+  g_free (d->alias);
+  d->alias = g_strdup ("Travis");
+  d->subscribe = TRUE;
+  d->publish = TRUE;
+  tp_handle_unref (self->priv->contact_repo, handle);
+
+  tp_group_mixin_change_members ((GObject *) subscribe, "",
+      set, NULL, NULL, NULL,
+      0, TP_CHANNEL_GROUP_CHANGE_REASON_NONE);
+  tp_group_mixin_change_members ((GObject *) publish, "",
+      set, NULL, NULL, NULL,
+      0, TP_CHANNEL_GROUP_CHANGE_REASON_NONE);
+  tp_group_mixin_change_members ((GObject *) stored, "",
+      set, NULL, NULL, NULL,
+      0, TP_CHANNEL_GROUP_CHANGE_REASON_NONE);
+
+  tp_intset_fast_iter_init (&iter, set);
+
+  while (tp_intset_fast_iter_next (&iter, &handle))
+    {
+      g_signal_emit (self, signals[ALIAS_UPDATED], 0, handle);
+      g_signal_emit (self, signals[PRESENCE_UPDATED], 0, handle);
+    }
+
+  tp_intset_destroy (set);
+
+  /* Add a couple of people whose presence we've requested. They are
+   * remote-pending in subscribe */
+
+  set = tp_intset_new ();
+
+  handle = tp_handle_ensure (self->priv->contact_repo, "geraldine example com",
+      NULL, NULL);
+  tp_intset_add (set, handle);
+  tp_intset_add (cam_set, handle);
+  tp_intset_add (fr_set, handle);
+  d = ensure_contact (self, handle, NULL);
+  g_free (d->alias);
+  d->alias = g_strdup ("Géraldine");
+  d->subscribe_requested = TRUE;
+  d->tags = tp_handle_set_new (self->priv->group_repo);
+  tp_handle_set_add (d->tags, cambridge);
+  tp_handle_set_add (d->tags, francophones);
+  tp_handle_unref (self->priv->contact_repo, handle);
+
+  handle = tp_handle_ensure (self->priv->contact_repo, "helen example com",
+      NULL, NULL);
+  tp_intset_add (set, handle);
+  tp_intset_add (cam_set, handle);
+  d = ensure_contact (self, handle, NULL);
+  g_free (d->alias);
+  d->alias = g_strdup ("Helen");
+  d->subscribe_requested = TRUE;
+  d->tags = tp_handle_set_new (self->priv->group_repo);
+  tp_handle_set_add (d->tags, cambridge);
+  tp_handle_unref (self->priv->contact_repo, handle);
+
+  tp_group_mixin_change_members ((GObject *) subscribe, "",
+      NULL, NULL, NULL, set,
+      0, TP_CHANNEL_GROUP_CHANGE_REASON_NONE);
+  tp_group_mixin_change_members ((GObject *) stored, "",
+      set, NULL, NULL, NULL,
+      0, TP_CHANNEL_GROUP_CHANGE_REASON_NONE);
+
+  tp_intset_fast_iter_init (&iter, set);
+
+  while (tp_intset_fast_iter_next (&iter, &handle))
+    {
+      g_signal_emit (self, signals[ALIAS_UPDATED], 0, handle);
+      g_signal_emit (self, signals[PRESENCE_UPDATED], 0, handle);
+    }
+
+  tp_intset_destroy (set);
+
+  /* Receive a couple of authorization requests too. These people are
+   * local-pending in publish */
+
+  handle = tp_handle_ensure (self->priv->contact_repo, "wim example com",
+      NULL, NULL);
+  d = ensure_contact (self, handle, NULL);
+  g_free (d->alias);
+  d->alias = g_strdup ("Wim");
+  d->publish_requested = TRUE;
+  tp_handle_unref (self->priv->contact_repo, handle);
+
+  set = tp_intset_new_containing (handle);
+  tp_group_mixin_change_members ((GObject *) publish,
+      "I'm more metal than you!",
+      NULL, NULL, set, NULL,
+      handle, TP_CHANNEL_GROUP_CHANGE_REASON_NONE);
+  tp_group_mixin_change_members ((GObject *) stored, "",
+      set, NULL, NULL, NULL,
+      handle, TP_CHANNEL_GROUP_CHANGE_REASON_NONE);
+  tp_intset_destroy (set);
+  g_signal_emit (self, signals[ALIAS_UPDATED], 0, handle);
+  g_signal_emit (self, signals[PRESENCE_UPDATED], 0, handle);
+
+  handle = tp_handle_ensure (self->priv->contact_repo, "christian example com",
+      NULL, NULL);
+  d = ensure_contact (self, handle, NULL);
+  g_free (d->alias);
+  d->alias = g_strdup ("Christian");
+  d->publish_requested = TRUE;
+  tp_handle_unref (self->priv->contact_repo, handle);
+
+  set = tp_intset_new_containing (handle);
+  tp_group_mixin_change_members ((GObject *) publish,
+      "I have some fermented herring for you",
+      NULL, NULL, set, NULL,
+      handle, TP_CHANNEL_GROUP_CHANGE_REASON_NONE);
+  tp_group_mixin_change_members ((GObject *) stored, "",
+      set, NULL, NULL, NULL,
+      handle, TP_CHANNEL_GROUP_CHANGE_REASON_NONE);
+  tp_intset_destroy (set);
+  g_signal_emit (self, signals[ALIAS_UPDATED], 0, handle);
+  g_signal_emit (self, signals[PRESENCE_UPDATED], 0, handle);
+
+  tp_group_mixin_change_members ((GObject *) cambridge_group, "",
+      cam_set, NULL, NULL, NULL,
+      0, TP_CHANNEL_GROUP_CHANGE_REASON_NONE);
+  tp_group_mixin_change_members ((GObject *) montreal_group, "",
+      mtl_set, NULL, NULL, NULL,
+      0, TP_CHANNEL_GROUP_CHANGE_REASON_NONE);
+  tp_group_mixin_change_members ((GObject *) francophones_group, "",
+      fr_set, NULL, NULL, NULL,
+      0, TP_CHANNEL_GROUP_CHANGE_REASON_NONE);
+
+  tp_intset_destroy (fr_set);
+  tp_intset_destroy (cam_set);
+  tp_intset_destroy (mtl_set);
+
+  tp_handle_unref (self->priv->group_repo, cambridge);
+  tp_handle_unref (self->priv->group_repo, montreal);
+  tp_handle_unref (self->priv->group_repo, francophones);
+
+  /* Now we've received the roster, we can satisfy all the queued requests */
+
+  tp_test_contact_list_manager_foreach_channel ((TpChannelManager *) self,
+      satisfy_queued_requests, self);
+
+  g_assert (g_hash_table_size (self->priv->queued_requests) == 0);
+  g_hash_table_destroy (self->priv->queued_requests);
+  self->priv->queued_requests = NULL;
+
+  return FALSE;
+}
+
+static void
+status_changed_cb (TpBaseConnection *conn,
+                   guint status,
+                   guint reason,
+                   TpTestContactListManager *self)
+{
+  switch (status)
+    {
+    case TP_CONNECTION_STATUS_CONNECTED:
+        {
+          /* Do network I/O to get the contact list. This connection manager
+           * doesn't really have a server, so simulate a small network delay
+           * then invent a contact list */
+          g_timeout_add_full (G_PRIORITY_DEFAULT,
+              2 * self->priv->simulation_delay, receive_contact_lists,
+              g_object_ref (self), g_object_unref);
+        }
+      break;
+
+    case TP_CONNECTION_STATUS_DISCONNECTED:
+        {
+          tp_test_contact_list_manager_close_all (self);
+        }
+      break;
+    }
+}
+
+static void
+constructed (GObject *object)
+{
+  TpTestContactListManager *self = TP_TEST_CONTACT_LIST_MANAGER (object);
+  void (*chain_up) (GObject *) =
+      ((GObjectClass *) tp_test_contact_list_manager_parent_class)->constructed;
+
+  if (chain_up != NULL)
+    {
+      chain_up (object);
+    }
+
+  self->priv->contact_repo = tp_base_connection_get_handles (self->priv->conn,
+      TP_HANDLE_TYPE_CONTACT);
+  self->priv->group_repo = tp_base_connection_get_handles (self->priv->conn,
+      TP_HANDLE_TYPE_GROUP);
+  self->priv->contacts = tp_handle_set_new (self->priv->contact_repo);
+
+  self->priv->status_changed_id = g_signal_connect (self->priv->conn,
+      "status-changed", (GCallback) status_changed_cb, self);
+}
+
+static void
+tp_test_contact_list_manager_class_init (TpTestContactListManagerClass *klass)
+{
+  GParamSpec *param_spec;
+  GObjectClass *object_class = (GObjectClass *) klass;
+
+  object_class->constructed = constructed;
+  object_class->dispose = dispose;
+  object_class->get_property = get_property;
+  object_class->set_property = set_property;
+
+  param_spec = g_param_spec_object ("connection", "Connection object",
+      "The connection that owns this channel manager",
+      TP_TYPE_BASE_CONNECTION,
+      G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE |
+      G_PARAM_STATIC_NAME | G_PARAM_STATIC_NICK | G_PARAM_STATIC_BLURB);
+  g_object_class_install_property (object_class, PROP_CONNECTION, param_spec);
+
+  param_spec = g_param_spec_uint ("simulation-delay", "Simulation delay",
+      "Delay between simulated network events",
+      0, G_MAXUINT32, 1000,
+      G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+  g_object_class_install_property (object_class, PROP_SIMULATION_DELAY,
+      param_spec);
+
+  g_type_class_add_private (klass, sizeof (TpTestContactListManagerPrivate));
+
+  signals[ALIAS_UPDATED] = g_signal_new ("alias-updated",
+      G_TYPE_FROM_CLASS (klass),
+      G_SIGNAL_RUN_LAST,
+      0,
+      NULL, NULL,
+      g_cclosure_marshal_VOID__UINT, G_TYPE_NONE, 1, G_TYPE_UINT);
+
+  signals[PRESENCE_UPDATED] = g_signal_new ("presence-updated",
+      G_TYPE_FROM_CLASS (klass),
+      G_SIGNAL_RUN_LAST,
+      0,
+      NULL, NULL,
+      g_cclosure_marshal_VOID__UINT, G_TYPE_NONE, 1, G_TYPE_UINT);
+}
+
+static void
+list_closed_cb (TpTestContactList *chan,
+                TpTestContactListManager *self)
+{
+  TpHandle handle;
+
+  tp_channel_manager_emit_channel_closed_for_object (self,
+      TP_EXPORTABLE_CHANNEL (chan));
+
+  g_object_get (chan,
+      "handle", &handle,
+      NULL);
+
+  if (self->priv->lists[handle] == NULL)
+    return;
+
+  g_assert (chan == self->priv->lists[handle]);
+  g_object_unref (self->priv->lists[handle]);
+  self->priv->lists[handle] = NULL;
+}
+
+static void
+group_closed_cb (TpTestContactGroup *chan,
+                 TpTestContactListManager *self)
+{
+  tp_channel_manager_emit_channel_closed_for_object (self,
+      TP_EXPORTABLE_CHANNEL (chan));
+
+  if (self->priv->groups != NULL)
+    {
+      TpHandle handle;
+
+      g_object_get (chan,
+          "handle", &handle,
+          NULL);
+
+      g_hash_table_remove (self->priv->groups, GUINT_TO_POINTER (handle));
+    }
+}
+
+static TpTestContactListBase *
+new_channel (TpTestContactListManager *self,
+             TpHandleType handle_type,
+             TpHandle handle,
+             gpointer request_token)
+{
+  TpTestContactListBase *chan;
+  gchar *object_path;
+  GType type;
+  GSList *requests = NULL;
+
+  if (handle_type == TP_HANDLE_TYPE_LIST)
+    {
+      /* Some Telepathy clients wrongly assume that contact lists of type LIST
+       * have object paths ending with "/subscribe", "/publish" etc. -
+       * telepathy-spec has no such guarantee, so in this tp_test we break
+       * those clients. Please read the spec when implementing it :-) */
+      object_path = g_strdup_printf ("%s/%sContactList",
+          self->priv->conn->object_path, _contact_lists[handle - 1]);
+      type = TP_TEST_TYPE_CONTACT_LIST;
+    }
+  else
+    {
+      /* Using Group%u (with handle as the value of %u) would be OK here too,
+       * but we'll encode the group name into the object path to be kind
+       * to people reading debug logs. */
+      gchar *id = tp_escape_as_identifier (tp_handle_inspect (
+            self->priv->group_repo, handle));
+
+      g_assert (handle_type == TP_HANDLE_TYPE_GROUP);
+      object_path = g_strdup_printf ("%s/Group/%s",
+          self->priv->conn->object_path, id);
+      type = TP_TEST_TYPE_CONTACT_GROUP;
+
+      g_free (id);
+    }
+
+  chan = g_object_new (type,
+      "connection", self->priv->conn,
+      "manager", self,
+      "object-path", object_path,
+      "handle-type", handle_type,
+      "handle", handle,
+      NULL);
+
+  g_free (object_path);
+
+  if (handle_type == TP_HANDLE_TYPE_LIST)
+    {
+      g_signal_connect (chan, "closed", (GCallback) list_closed_cb, self);
+      g_assert (self->priv->lists[handle] == NULL);
+      self->priv->lists[handle] = TP_TEST_CONTACT_LIST (chan);
+    }
+  else
+    {
+      g_signal_connect (chan, "closed", (GCallback) group_closed_cb, self);
+
+      g_assert (g_hash_table_lookup (self->priv->groups,
+            GUINT_TO_POINTER (handle)) == NULL);
+      g_hash_table_insert (self->priv->groups, GUINT_TO_POINTER (handle),
+          TP_TEST_CONTACT_GROUP (chan));
+    }
+
+  if (self->priv->queued_requests == NULL)
+    {
+      if (request_token != NULL)
+        requests = g_slist_prepend (requests, request_token);
+
+      tp_channel_manager_emit_new_channel (self, TP_EXPORTABLE_CHANNEL (chan),
+          requests);
+      g_slist_free (requests);
+    }
+  else if (request_token != NULL)
+    {
+      /* initial contact list not received yet, so we have to wait for it */
+      requests = g_hash_table_lookup (self->priv->queued_requests, chan);
+      g_hash_table_steal (self->priv->queued_requests, chan);
+      requests = g_slist_prepend (requests, request_token);
+      g_hash_table_insert (self->priv->queued_requests, chan, requests);
+    }
+
+  return chan;
+}
+
+static TpTestContactList *
+ensure_list (TpTestContactListManager *self,
+             TpTestContactListHandle handle)
+{
+  if (self->priv->lists[handle] == NULL)
+    {
+      new_channel (self, TP_HANDLE_TYPE_LIST, handle, NULL);
+      g_assert (self->priv->lists[handle] != NULL);
+    }
+
+  return self->priv->lists[handle];
+}
+
+static TpTestContactGroup *
+ensure_group (TpTestContactListManager *self,
+              TpHandle handle)
+{
+  TpTestContactGroup *group = g_hash_table_lookup (self->priv->groups,
+      GUINT_TO_POINTER (handle));
+
+  if (group == NULL)
+    {
+      group = TP_TEST_CONTACT_GROUP (new_channel (self, TP_HANDLE_TYPE_GROUP,
+            handle, NULL));
+    }
+
+  return group;
+}
+
+static const gchar * const fixed_properties[] = {
+    TP_PROP_CHANNEL_CHANNEL_TYPE,
+    TP_PROP_CHANNEL_TARGET_HANDLE_TYPE,
+    NULL
+};
+
+static const gchar * const allowed_properties[] = {
+    TP_PROP_CHANNEL_TARGET_HANDLE,
+    TP_PROP_CHANNEL_TARGET_ID,
+    NULL
+};
+
+static void
+tp_test_contact_list_manager_foreach_channel_class (TpChannelManager *manager,
+    TpChannelManagerChannelClassFunc func,
+    gpointer user_data)
+{
+    GHashTable *table = tp_asv_new (
+        TP_PROP_CHANNEL_CHANNEL_TYPE,
+            G_TYPE_STRING, TP_IFACE_CHANNEL_TYPE_CONTACT_LIST,
+        TP_PROP_CHANNEL_TARGET_HANDLE_TYPE, G_TYPE_UINT, TP_HANDLE_TYPE_LIST,
+        NULL);
+
+    func (manager, table, allowed_properties, user_data);
+
+    g_hash_table_insert (table, TP_PROP_CHANNEL_TARGET_HANDLE_TYPE,
+        tp_g_value_slice_new_uint (TP_HANDLE_TYPE_GROUP));
+    func (manager, table, allowed_properties, user_data);
+
+    g_hash_table_destroy (table);
+}
+
+static gboolean
+tp_test_contact_list_manager_request (TpTestContactListManager *self,
+                                      gpointer request_token,
+                                      GHashTable *request_properties,
+                                      gboolean require_new)
+{
+  TpHandleType handle_type;
+  TpHandle handle;
+  TpTestContactListBase *chan;
+  GError *error = NULL;
+
+  if (tp_strdiff (tp_asv_get_string (request_properties,
+          TP_PROP_CHANNEL_CHANNEL_TYPE),
+      TP_IFACE_CHANNEL_TYPE_CONTACT_LIST))
+    {
+      return FALSE;
+    }
+
+  handle_type = tp_asv_get_uint32 (request_properties,
+      TP_PROP_CHANNEL_TARGET_HANDLE_TYPE, NULL);
+
+  if (handle_type != TP_HANDLE_TYPE_LIST &&
+      handle_type != TP_HANDLE_TYPE_GROUP)
+    {
+      return FALSE;
+    }
+
+  handle = tp_asv_get_uint32 (request_properties,
+      TP_PROP_CHANNEL_TARGET_HANDLE, NULL);
+  g_assert (handle != 0);
+
+  if (tp_channel_manager_asv_has_unknown_properties (request_properties,
+        fixed_properties, allowed_properties, &error))
+    {
+      goto error;
+    }
+
+  if (handle_type == TP_HANDLE_TYPE_LIST)
+    {
+      /* telepathy-glib has already checked that the handle is valid */
+      g_assert (handle < NUM_TP_TEST_CONTACT_LISTS);
+
+      chan = TP_TEST_CONTACT_LIST_BASE (self->priv->lists[handle]);
+    }
+  else
+    {
+      chan = g_hash_table_lookup (self->priv->groups,
+          GUINT_TO_POINTER (handle));
+    }
+
+  if (chan == NULL)
+    {
+      new_channel (self, handle_type, handle, request_token);
+    }
+  else if (require_new)
+    {
+      g_set_error (&error, TP_ERRORS, TP_ERROR_NOT_AVAILABLE,
+          "A ContactList channel for type #%u, handle #%u already exists",
+          handle_type, handle);
+      goto error;
+    }
+  else
+    {
+      tp_channel_manager_emit_request_already_satisfied (self,
+          request_token, TP_EXPORTABLE_CHANNEL (chan));
+    }
+
+  return TRUE;
+
+error:
+  tp_channel_manager_emit_request_failed (self, request_token,
+      error->domain, error->code, error->message);
+  g_error_free (error);
+  return TRUE;
+}
+
+static gboolean
+tp_test_contact_list_manager_create_channel (TpChannelManager *manager,
+                                             gpointer request_token,
+                                             GHashTable *request_properties)
+{
+    return tp_test_contact_list_manager_request (
+        TP_TEST_CONTACT_LIST_MANAGER (manager), request_token,
+        request_properties, TRUE);
+}
+
+static gboolean
+tp_test_contact_list_manager_ensure_channel (TpChannelManager *manager,
+                                             gpointer request_token,
+                                             GHashTable *request_properties)
+{
+    return tp_test_contact_list_manager_request (
+        TP_TEST_CONTACT_LIST_MANAGER (manager), request_token,
+        request_properties, FALSE);
+}
+
+static void
+channel_manager_iface_init (gpointer g_iface,
+                            gpointer data G_GNUC_UNUSED)
+{
+  TpChannelManagerIface *iface = g_iface;
+
+  iface->foreach_channel = tp_test_contact_list_manager_foreach_channel;
+  iface->foreach_channel_class =
+      tp_test_contact_list_manager_foreach_channel_class;
+  iface->create_channel = tp_test_contact_list_manager_create_channel;
+  iface->ensure_channel = tp_test_contact_list_manager_ensure_channel;
+  /* In this channel manager, Request has the same semantics as Ensure */
+  iface->request_channel = tp_test_contact_list_manager_ensure_channel;
+}
+
+static void
+send_updated_roster (TpTestContactListManager *self,
+                     TpHandle contact)
+{
+  TpTestContactDetails *d = g_hash_table_lookup (self->priv->contact_details,
+      GUINT_TO_POINTER (contact));
+  const gchar *identifier = tp_handle_inspect (self->priv->contact_repo,
+      contact);
+
+  /* In a real connection manager, we'd transmit these new details to the
+   * server, rather than just printing messages. */
+
+  if (d == NULL)
+    {
+      g_message ("Deleting contact %s from server", identifier);
+    }
+  else
+    {
+      g_message ("Transmitting new state of contact %s to server", identifier);
+      g_message ("\talias = %s", d->alias);
+      g_message ("\tcan see our presence = %s",
+          d->publish ? "yes" :
+          (d->publish_requested ? "no, but has requested it" : "no"));
+      g_message ("\tsends us presence = %s",
+          d->subscribe ? "yes" :
+          (d->subscribe_requested ? "no, but we have requested it" : "no"));
+
+      if (d->tags == NULL || tp_handle_set_size (d->tags) == 0)
+        {
+          g_message ("\tnot in any groups");
+        }
+      else
+        {
+          TpIntSet *set = tp_handle_set_peek (d->tags);
+          TpIntSetFastIter iter;
+          TpHandle member;
+
+          tp_intset_fast_iter_init (&iter, set);
+
+          while (tp_intset_fast_iter_next (&iter, &member))
+            {
+              g_message ("\tin group: %s",
+                  tp_handle_inspect (self->priv->group_repo, member));
+            }
+        }
+    }
+}
+
+gboolean
+tp_test_contact_list_manager_add_to_group (TpTestContactListManager *self,
+                                           GObject *channel,
+                                           TpHandle group,
+                                           TpHandle member,
+                                           const gchar *message,
+                                           GError **error)
+{
+  gboolean updated;
+  TpTestContactDetails *d = ensure_contact (self, member, &updated);
+  TpTestContactList *stored = self->priv->lists[
+    TP_TEST_CONTACT_LIST_STORED];
+
+  if (d->tags == NULL)
+    d->tags = tp_handle_set_new (self->priv->group_repo);
+
+  if (!tp_handle_set_is_member (d->tags, group))
+    {
+      tp_handle_set_add (d->tags, group);
+      updated = TRUE;
+    }
+
+  if (updated)
+    {
+      TpIntSet *added = tp_intset_new_containing (member);
+
+      send_updated_roster (self, member);
+      tp_group_mixin_change_members (channel, "", added, NULL, NULL, NULL,
+          self->priv->conn->self_handle, TP_CHANNEL_GROUP_CHANGE_REASON_NONE);
+      tp_group_mixin_change_members ((GObject *) stored, "",
+          added, NULL, NULL, NULL,
+          self->priv->conn->self_handle, TP_CHANNEL_GROUP_CHANGE_REASON_NONE);
+      tp_intset_destroy (added);
+    }
+
+  return TRUE;
+}
+
+gboolean
+tp_test_contact_list_manager_remove_from_group (
+    TpTestContactListManager *self,
+    GObject *channel,
+    TpHandle group,
+    TpHandle member,
+    const gchar *message,
+    GError **error)
+{
+  TpTestContactDetails *d = lookup_contact (self, member);
+
+  /* If not on the roster or not in any groups, we have nothing to do */
+  if (d == NULL || d->tags == NULL)
+    return TRUE;
+
+  if (tp_handle_set_remove (d->tags, group))
+    {
+      TpIntSet *removed = tp_intset_new_containing (member);
+
+      send_updated_roster (self, member);
+      tp_group_mixin_change_members (channel, "", NULL, removed, NULL, NULL,
+          self->priv->conn->self_handle, TP_CHANNEL_GROUP_CHANGE_REASON_NONE);
+      tp_intset_destroy (removed);
+    }
+
+  return TRUE;
+}
+
+typedef struct {
+    TpTestContactListManager *self;
+    TpHandle contact;
+} SelfAndContact;
+
+static SelfAndContact *
+self_and_contact_new (TpTestContactListManager *self,
+                      TpHandle contact)
+{
+  SelfAndContact *ret = g_slice_new0 (SelfAndContact);
+
+  ret->self = g_object_ref (self);
+  ret->contact = contact;
+  tp_handle_ref (self->priv->contact_repo, contact);
+  return ret;
+}
+
+static void
+self_and_contact_destroy (gpointer p)
+{
+  SelfAndContact *s = p;
+
+  tp_handle_unref (s->self->priv->contact_repo, s->contact);
+  g_object_unref (s->self);
+  g_slice_free (SelfAndContact, s);
+}
+
+static void
+receive_auth_request (TpTestContactListManager *self,
+                      TpHandle contact)
+{
+  TpTestContactDetails *d;
+  TpIntSet *set;
+  TpTestContactList *publish = self->priv->lists[
+    TP_TEST_CONTACT_LIST_PUBLISH];
+  TpTestContactList *stored = self->priv->lists[
+    TP_TEST_CONTACT_LIST_STORED];
+
+  /* if shutting down, do nothing */
+  if (publish == NULL)
+    return;
+
+  /* A remote contact has asked to see our presence.
+   *
+   * In a real connection manager this would be the result of incoming
+   * data from the server. */
+
+  g_message ("From server: %s has sent us a publish request",
+      tp_handle_inspect (self->priv->contact_repo, contact));
+
+  d = ensure_contact (self, contact, NULL);
+
+  if (d->publish)
+    return;
+
+  d->publish_requested = TRUE;
+
+  set = tp_intset_new_containing (contact);
+  tp_group_mixin_change_members ((GObject *) publish,
+      "May I see your presence, please?",
+      NULL, NULL, set, NULL,
+      contact, TP_CHANNEL_GROUP_CHANGE_REASON_NONE);
+  tp_group_mixin_change_members ((GObject *) stored, "",
+      set, NULL, NULL, NULL,
+      contact, TP_CHANNEL_GROUP_CHANGE_REASON_NONE);
+  tp_intset_destroy (set);
+}
+
+static gboolean
+receive_authorized (gpointer p)
+{
+  SelfAndContact *s = p;
+  TpTestContactDetails *d;
+  TpIntSet *set;
+  TpTestContactList *subscribe = s->self->priv->lists[
+    TP_TEST_CONTACT_LIST_SUBSCRIBE];
+  TpTestContactList *stored = s->self->priv->lists[
+    TP_TEST_CONTACT_LIST_STORED];
+
+  /* A remote contact has accepted our request to see their presence.
+   *
+   * In a real connection manager this would be the result of incoming
+   * data from the server. */
+
+  g_message ("From server: %s has accepted our subscribe request",
+      tp_handle_inspect (s->self->priv->contact_repo, s->contact));
+
+  d = ensure_contact (s->self, s->contact, NULL);
+
+  /* if we were already subscribed to them, then nothing really happened */
+  if (d->subscribe)
+    return FALSE;
+
+  d->subscribe_requested = FALSE;
+  d->subscribe = TRUE;
+
+  set = tp_intset_new_containing (s->contact);
+  tp_group_mixin_change_members ((GObject *) subscribe, "",
+      set, NULL, NULL, NULL,
+      s->contact, TP_CHANNEL_GROUP_CHANGE_REASON_NONE);
+  tp_group_mixin_change_members ((GObject *) stored, "",
+      set, NULL, NULL, NULL,
+      s->contact, TP_CHANNEL_GROUP_CHANGE_REASON_NONE);
+  tp_intset_destroy (set);
+
+  /* their presence changes to something other than UNKNOWN */
+  g_signal_emit (s->self, signals[PRESENCE_UPDATED], 0, s->contact);
+
+  /* if we're not publishing to them, also pretend they have asked us to
+   * do so */
+  if (!d->publish)
+    {
+      receive_auth_request (s->self, s->contact);
+    }
+
+  return FALSE;
+}
+
+static gboolean
+receive_unauthorized (gpointer p)
+{
+  SelfAndContact *s = p;
+  TpTestContactDetails *d;
+  TpIntSet *set;
+  TpTestContactList *subscribe = s->self->priv->lists[
+    TP_TEST_CONTACT_LIST_SUBSCRIBE];
+
+  /* if shutting down, do nothing */
+  if (subscribe == NULL)
+    return FALSE;
+
+  /* A remote contact has rejected our request to see their presence.
+   *
+   * In a real connection manager this would be the result of incoming
+   * data from the server. */
+
+  g_message ("From server: %s has rejected our subscribe request",
+      tp_handle_inspect (s->self->priv->contact_repo, s->contact));
+
+  d = ensure_contact (s->self, s->contact, NULL);
+
+  if (!d->subscribe && !d->subscribe_requested)
+    return FALSE;
+
+  d->subscribe_requested = FALSE;
+  d->subscribe = FALSE;
+
+  set = tp_intset_new_containing (s->contact);
+  tp_group_mixin_change_members ((GObject *) subscribe, "Say 'please'!",
+      NULL, set, NULL, NULL,
+      s->contact, TP_CHANNEL_GROUP_CHANGE_REASON_NONE);
+  tp_intset_destroy (set);
+
+  /* their presence changes to UNKNOWN */
+  g_signal_emit (s->self, signals[PRESENCE_UPDATED], 0, s->contact);
+
+  return FALSE;
+}
+
+gboolean
+tp_test_contact_list_manager_add_to_list (TpTestContactListManager *self,
+                                          GObject *channel,
+                                          TpTestContactListHandle list,
+                                          TpHandle member,
+                                          const gchar *message,
+                                          GError **error)
+{
+  TpIntSet *set;
+  TpTestContactList *stored = self->priv->lists[TP_TEST_CONTACT_LIST_STORED];
+
+  switch (list)
+    {
+    case TP_TEST_CONTACT_LIST_SUBSCRIBE:
+      /* we would like to see member's presence */
+        {
+          gboolean created;
+          TpTestContactDetails *d = ensure_contact (self, member, &created);
+          gchar *message_lc;
+
+          /* if they already authorized us, it's a no-op */
+          if (d->subscribe)
+            return TRUE;
+
+          /* In a real connection manager we'd start a network request here */
+          g_message ("Transmitting authorization request to %s: %s",
+              tp_handle_inspect (self->priv->contact_repo, member),
+              message);
+
+          if (created || !d->subscribe_requested)
+            {
+              d->subscribe_requested = TRUE;
+              send_updated_roster (self, member);
+            }
+
+          set = tp_intset_new_containing (member);
+          tp_group_mixin_change_members (channel, message,
+              NULL, NULL, NULL, set,
+              self->priv->conn->self_handle,
+              TP_CHANNEL_GROUP_CHANGE_REASON_NONE);
+          /* subscribing to someone implicitly puts them on Stored, too */
+          tp_group_mixin_change_members ((GObject *) stored, "",
+              set, NULL, NULL, NULL,
+              self->priv->conn->self_handle,
+              TP_CHANNEL_GROUP_CHANGE_REASON_NONE);
+          tp_intset_destroy (set);
+
+          /* Pretend that after a delay, the contact notices the request
+           * and allows or rejects it. In this tp_test connection manager,
+           * empty requests are allowed, as are requests that contain "please"
+           * case-insensitively. All other requests are denied. */
+          message_lc = g_ascii_strdown (message, -1);
+
+          if (message[0] == '\0' || strstr (message_lc, "please") != NULL)
+            {
+              g_timeout_add_full (G_PRIORITY_DEFAULT,
+                  self->priv->simulation_delay, receive_authorized,
+                  self_and_contact_new (self, member),
+                  self_and_contact_destroy);
+            }
+          else
+            {
+              g_timeout_add_full (G_PRIORITY_DEFAULT,
+                  self->priv->simulation_delay,
+                  receive_unauthorized,
+                  self_and_contact_new (self, member),
+                  self_and_contact_destroy);
+            }
+
+          g_free (message_lc);
+        }
+      return TRUE;
+
+    case TP_TEST_CONTACT_LIST_PUBLISH:
+      /* We would like member to see our presence. This is meaningless,
+       * unless they have asked for it. */
+        {
+          TpTestContactDetails *d = lookup_contact (self, member);
+
+          if (d == NULL || !d->publish_requested)
+            {
+              /* the group mixin won't actually allow this to be reached,
+               * because of the flags we set */
+              g_set_error (error, TP_ERRORS, TP_ERROR_NOT_AVAILABLE,
+                  "Can't unilaterally send presence to %s",
+                  tp_handle_inspect (self->priv->contact_repo, member));
+              return FALSE;
+            }
+
+          if (!d->publish)
+            {
+              d->publish = TRUE;
+              d->publish_requested = FALSE;
+              send_updated_roster (self, member);
+
+              set = tp_intset_new_containing (member);
+              tp_group_mixin_change_members (channel, "",
+                  set, NULL, NULL, NULL,
+                  self->priv->conn->self_handle,
+                  TP_CHANNEL_GROUP_CHANGE_REASON_NONE);
+              tp_group_mixin_change_members ((GObject *) stored, "",
+                  set, NULL, NULL, NULL,
+                  self->priv->conn->self_handle,
+                  TP_CHANNEL_GROUP_CHANGE_REASON_NONE);
+              tp_intset_destroy (set);
+            }
+        }
+      return TRUE;
+
+    case TP_TEST_CONTACT_LIST_STORED:
+      /* we would like member to be on the roster */
+        {
+          gboolean created;
+
+          ensure_contact (self, member, &created);
+
+          if (created)
+            send_updated_roster (self, member);
+
+          set = tp_intset_new_containing (member);
+          tp_group_mixin_change_members (channel, "",
+              set, NULL, NULL, NULL, self->priv->conn->self_handle,
+              TP_CHANNEL_GROUP_CHANGE_REASON_NONE);
+          tp_intset_destroy (set);
+        }
+      return TRUE;
+
+    default:
+      g_return_val_if_reached (FALSE);
+    }
+}
+
+static gboolean
+auth_request_cb (gpointer p)
+{
+  SelfAndContact *s = p;
+
+  receive_auth_request (s->self, s->contact);
+
+  return FALSE;
+}
+
+gboolean
+tp_test_contact_list_manager_remove_from_list (TpTestContactListManager *self,
+                                               GObject *channel,
+                                               TpTestContactListHandle list,
+                                               TpHandle member,
+                                               const gchar *message,
+                                               GError **error)
+{
+  TpIntSet *set;
+
+  switch (list)
+    {
+    case TP_TEST_CONTACT_LIST_PUBLISH:
+      /* we would like member not to see our presence any more, or we
+       * would like to reject a request from them to see our presence */
+        {
+          TpTestContactDetails *d = lookup_contact (self, member);
+
+          if (d != NULL)
+            {
+              if (d->publish_requested)
+                {
+                  g_message ("Rejecting authorization request from %s",
+                      tp_handle_inspect (self->priv->contact_repo, member));
+                  d->publish_requested = FALSE;
+
+                  set = tp_intset_new_containing (member);
+                  tp_group_mixin_change_members (channel, "",
+                      NULL, set, NULL, NULL,
+                      self->priv->conn->self_handle,
+                      TP_CHANNEL_GROUP_CHANGE_REASON_NONE);
+                  tp_intset_destroy (set);
+                }
+              else if (d->publish)
+                {
+                  g_message ("Removing authorization from %s",
+                      tp_handle_inspect (self->priv->contact_repo, member));
+                  d->publish = FALSE;
+
+                  set = tp_intset_new_containing (member);
+                  tp_group_mixin_change_members (channel, "",
+                      NULL, set, NULL, NULL,
+                      self->priv->conn->self_handle,
+                      TP_CHANNEL_GROUP_CHANGE_REASON_NONE);
+                  tp_intset_destroy (set);
+
+                  /* Pretend that after a delay, the contact notices the change
+                   * and asks for our presence again */
+                  g_timeout_add_full (G_PRIORITY_DEFAULT,
+                      self->priv->simulation_delay, auth_request_cb,
+                      self_and_contact_new (self, member),
+                      self_and_contact_destroy);
+                }
+              else
+                {
+                  /* nothing to do, avoid "updating the roster" */
+                  return TRUE;
+                }
+
+              send_updated_roster (self, member);
+            }
+        }
+      return TRUE;
+
+    case TP_TEST_CONTACT_LIST_SUBSCRIBE:
+      /* we would like to avoid receiving member's presence any more,
+       * or we would like to cancel an outstanding request for their
+       * presence */
+        {
+          TpTestContactDetails *d = lookup_contact (self, member);
+
+          if (d != NULL)
+            {
+              if (d->subscribe_requested)
+                {
+                  g_message ("Cancelling our authorization request to %s",
+                      tp_handle_inspect (self->priv->contact_repo, member));
+                  d->subscribe_requested = FALSE;
+
+                  set = tp_intset_new_containing (member);
+                  tp_group_mixin_change_members (channel, "",
+                      NULL, set, NULL, NULL,
+                      self->priv->conn->self_handle,
+                      TP_CHANNEL_GROUP_CHANGE_REASON_NONE);
+                  tp_intset_destroy (set);
+                }
+              else if (d->subscribe)
+                {
+                  g_message ("We no longer want presence from %s",
+                      tp_handle_inspect (self->priv->contact_repo, member));
+                  d->subscribe = FALSE;
+
+                  set = tp_intset_new_containing (member);
+                  tp_group_mixin_change_members (channel, "",
+                      NULL, set, NULL, NULL,
+                      self->priv->conn->self_handle,
+                      TP_CHANNEL_GROUP_CHANGE_REASON_NONE);
+                  tp_intset_destroy (set);
+
+                  /* since they're no longer on the subscribe list, we can't
+                   * see their presence, so emit a signal changing it to
+                   * UNKNOWN */
+                  g_signal_emit (self, signals[PRESENCE_UPDATED], 0, member);
+                }
+              else
+                {
+                  /* nothing to do, avoid "updating the roster" */
+                  return TRUE;
+                }
+
+              send_updated_roster (self, member);
+            }
+        }
+      return TRUE;
+
+    case TP_TEST_CONTACT_LIST_STORED:
+      /* we would like to remove member from the roster altogether */
+        {
+          TpTestContactDetails *d = lookup_contact (self, member);
+
+          if (d != NULL)
+            {
+              g_hash_table_remove (self->priv->contact_details,
+                  GUINT_TO_POINTER (member));
+              send_updated_roster (self, member);
+
+              set = tp_intset_new_containing (member);
+              tp_group_mixin_change_members (channel, "",
+                  NULL, set, NULL, NULL,
+                  self->priv->conn->self_handle,
+                  TP_CHANNEL_GROUP_CHANGE_REASON_NONE);
+              tp_group_mixin_change_members (
+                  (GObject *) self->priv->lists[TP_TEST_CONTACT_LIST_SUBSCRIBE],
+                  "", NULL, set, NULL, NULL,
+                  self->priv->conn->self_handle,
+                  TP_CHANNEL_GROUP_CHANGE_REASON_NONE);
+              tp_group_mixin_change_members (
+                  (GObject *) self->priv->lists[TP_TEST_CONTACT_LIST_PUBLISH],
+                  "", NULL, set, NULL, NULL,
+                  self->priv->conn->self_handle,
+                  TP_CHANNEL_GROUP_CHANGE_REASON_NONE);
+              tp_intset_destroy (set);
+
+              tp_handle_set_remove (self->priv->contacts, member);
+
+              /* since they're no longer on the subscribe list, we can't
+               * see their presence, so emit a signal changing it to
+               * UNKNOWN */
+              g_signal_emit (self, signals[PRESENCE_UPDATED], 0, member);
+
+            }
+        }
+      return TRUE;
+
+    default:
+      g_return_val_if_reached (FALSE);
+    }
+}
+
+TpTestContactListPresence
+tp_test_contact_list_manager_get_presence (TpTestContactListManager *self,
+                                           TpHandle contact)
+{
+  TpTestContactDetails *d = lookup_contact (self, contact);
+  const gchar *id;
+
+  if (d == NULL || !d->subscribe)
+    {
+      /* we don't know the presence of people not on the subscribe list,
+       * by definition */
+      return TP_TEST_CONTACT_LIST_PRESENCE_UNKNOWN;
+    }
+
+  id = tp_handle_inspect (self->priv->contact_repo, contact);
+
+  /* In this tp_test CM, we fake contacts' presence based on their name:
+   * contacts in the first half of the alphabet are available, the rest
+   * (including non-alphabetic and non-ASCII initial letters) are away. */
+  if ((id[0] >= 'A' && id[0] <= 'M') || (id[0] >= 'a' && id[0] <= 'm'))
+    {
+      return TP_TEST_CONTACT_LIST_PRESENCE_AVAILABLE;
+    }
+
+  return TP_TEST_CONTACT_LIST_PRESENCE_AWAY;
+}
+
+const gchar *
+tp_test_contact_list_manager_get_alias (TpTestContactListManager *self,
+                                        TpHandle contact)
+{
+  TpTestContactDetails *d = lookup_contact (self, contact);
+
+  if (d == NULL)
+    {
+      /* we don't have a user-defined alias for people not on the roster */
+      return tp_handle_inspect (self->priv->contact_repo, contact);
+    }
+
+  return d->alias;
+}
+
+void
+tp_test_contact_list_manager_set_alias (TpTestContactListManager *self,
+                                        TpHandle contact,
+                                        const gchar *alias)
+{
+  gboolean created;
+  TpTestContactDetails *d = ensure_contact (self, contact, &created);
+  TpTestContactList *stored = self->priv->lists[
+    TP_TEST_CONTACT_LIST_STORED];
+  gchar *old = d->alias;
+  TpIntSet *set;
+
+  /* FIXME: if stored list hasn't been retrieved yet, queue the change for
+   * later */
+
+  /* if shutting down, do nothing */
+  if (stored == NULL)
+    return;
+
+  d->alias = g_strdup (alias);
+
+  if (created || tp_strdiff (old, alias))
+    send_updated_roster (self, contact);
+
+  g_free (old);
+
+  set = tp_intset_new_containing (contact);
+  tp_group_mixin_change_members ((GObject *) stored, "",
+      set, NULL, NULL, NULL, self->priv->conn->self_handle,
+      TP_CHANNEL_GROUP_CHANGE_REASON_NONE);
+  tp_intset_destroy (set);
+}
diff --git a/tests/lib/telepathy/contactlist/contact-list-manager.h b/tests/lib/telepathy/contactlist/contact-list-manager.h
new file mode 100644
index 0000000..7dd6c16
--- /dev/null
+++ b/tests/lib/telepathy/contactlist/contact-list-manager.h
@@ -0,0 +1,107 @@
+/*
+ * Example channel manager for contact lists
+ *
+ * Copyright © 2007-2009 Collabora Ltd. <http://www.collabora.co.uk/>
+ * Copyright © 2007-2009 Nokia Corporation
+ *
+ * Copying and distribution of this file, with or without modification,
+ * are permitted in any medium without royalty provided the copyright
+ * notice and this notice are preserved.
+ */
+
+#ifndef __TP_TEST_CONTACT_LIST_MANAGER_H__
+#define __TP_TEST_CONTACT_LIST_MANAGER_H__
+
+#include <glib-object.h>
+
+#include <telepathy-glib/channel-manager.h>
+#include <telepathy-glib/handle.h>
+#include <telepathy-glib/presence-mixin.h>
+
+G_BEGIN_DECLS
+
+typedef struct _TpTestContactListManager TpTestContactListManager;
+typedef struct _TpTestContactListManagerClass TpTestContactListManagerClass;
+typedef struct _TpTestContactListManagerPrivate TpTestContactListManagerPrivate;
+
+struct _TpTestContactListManagerClass {
+    GObjectClass parent_class;
+};
+
+struct _TpTestContactListManager {
+    GObject parent;
+
+    TpTestContactListManagerPrivate *priv;
+};
+
+GType tp_test_contact_list_manager_get_type (void);
+
+#define TP_TEST_TYPE_CONTACT_LIST_MANAGER \
+  (tp_test_contact_list_manager_get_type ())
+#define TP_TEST_CONTACT_LIST_MANAGER(obj) \
+  (G_TYPE_CHECK_INSTANCE_CAST((obj), TP_TEST_TYPE_CONTACT_LIST_MANAGER, \
+                              TpTestContactListManager))
+#define TP_TEST_CONTACT_LIST_MANAGER_CLASS(klass) \
+  (G_TYPE_CHECK_CLASS_CAST((klass), TP_TEST_TYPE_CONTACT_LIST_MANAGER, \
+                           TpTestContactListManagerClass))
+#define TP_TEST_IS_CONTACT_LIST_MANAGER(obj) \
+  (G_TYPE_CHECK_INSTANCE_TYPE((obj), TP_TEST_TYPE_CONTACT_LIST_MANAGER))
+#define TP_TEST_IS_CONTACT_LIST_MANAGER_CLASS(klass) \
+  (G_TYPE_CHECK_CLASS_TYPE((klass), TP_TEST_TYPE_CONTACT_LIST_MANAGER))
+#define TP_TEST_CONTACT_LIST_MANAGER_GET_CLASS(obj) \
+  (G_TYPE_INSTANCE_GET_CLASS ((obj), TP_TEST_TYPE_CONTACT_LIST_MANAGER, \
+                              TpTestContactListManagerClass))
+
+gboolean tp_test_contact_list_manager_add_to_group (
+    TpTestContactListManager *self, GObject *channel,
+    TpHandle group, TpHandle member, const gchar *message, GError **error);
+
+gboolean tp_test_contact_list_manager_remove_from_group (
+    TpTestContactListManager *self, GObject *channel,
+    TpHandle group, TpHandle member, const gchar *message, GError **error);
+
+/* elements 1, 2... of this enum must be kept in sync with elements 0, 1...
+ * of the array _contact_lists in contact-list-manager.h */
+typedef enum {
+    INVALID_TP_TEST_CONTACT_LIST,
+    TP_TEST_CONTACT_LIST_SUBSCRIBE = 1,
+    TP_TEST_CONTACT_LIST_PUBLISH,
+    TP_TEST_CONTACT_LIST_STORED,
+    NUM_TP_TEST_CONTACT_LISTS
+} TpTestContactListHandle;
+
+/* this enum must be kept in sync with the array _statuses in
+ * contact-list-manager.c */
+typedef enum {
+    TP_TEST_CONTACT_LIST_PRESENCE_OFFLINE = 0,
+    TP_TEST_CONTACT_LIST_PRESENCE_UNKNOWN,
+    TP_TEST_CONTACT_LIST_PRESENCE_ERROR,
+    TP_TEST_CONTACT_LIST_PRESENCE_AWAY,
+    TP_TEST_CONTACT_LIST_PRESENCE_AVAILABLE
+} TpTestContactListPresence;
+
+const TpPresenceStatusSpec *tp_test_contact_list_presence_statuses (
+    void);
+
+gboolean tp_test_contact_list_manager_add_to_list (
+    TpTestContactListManager *self, GObject *channel,
+    TpTestContactListHandle list, TpHandle member, const gchar *message,
+    GError **error);
+
+gboolean tp_test_contact_list_manager_remove_from_list (
+    TpTestContactListManager *self, GObject *channel,
+    TpTestContactListHandle list, TpHandle member, const gchar *message,
+    GError **error);
+
+const gchar **tp_test_contact_lists (void);
+
+TpTestContactListPresence tp_test_contact_list_manager_get_presence (
+    TpTestContactListManager *self, TpHandle contact);
+const gchar *tp_test_contact_list_manager_get_alias (
+    TpTestContactListManager *self, TpHandle contact);
+void tp_test_contact_list_manager_set_alias (
+    TpTestContactListManager *self, TpHandle contact, const gchar *alias);
+
+G_END_DECLS
+
+#endif
diff --git a/tests/lib/telepathy/contactlist/contact-list.c b/tests/lib/telepathy/contactlist/contact-list.c
new file mode 100644
index 0000000..0f6eaea
--- /dev/null
+++ b/tests/lib/telepathy/contactlist/contact-list.c
@@ -0,0 +1,632 @@
+/*
+ * An example ContactList channel with handle type LIST or GROUP
+ *
+ * Copyright © 2009 Collabora Ltd. <http://www.collabora.co.uk/>
+ * Copyright © 2009 Nokia Corporation
+ *
+ * Copying and distribution of this file, with or without modification,
+ * are permitted in any medium without royalty provided the copyright
+ * notice and this notice are preserved.
+ */
+
+#include "contact-list.h"
+
+#include <telepathy-glib/telepathy-glib.h>
+#include <telepathy-glib/channel-iface.h>
+#include <telepathy-glib/exportable-channel.h>
+#include <telepathy-glib/svc-channel.h>
+
+#include "contact-list-manager.h"
+
+static void channel_iface_init (gpointer iface, gpointer data);
+static void list_channel_iface_init (gpointer iface, gpointer data);
+static void group_channel_iface_init (gpointer iface, gpointer data);
+
+/* Abstract base class */
+G_DEFINE_TYPE_WITH_CODE (TpTestContactListBase, tp_test_contact_list_base,
+    G_TYPE_OBJECT,
+    G_IMPLEMENT_INTERFACE (TP_TYPE_SVC_CHANNEL, channel_iface_init);
+    G_IMPLEMENT_INTERFACE (TP_TYPE_SVC_CHANNEL_TYPE_CONTACT_LIST, NULL);
+    G_IMPLEMENT_INTERFACE (TP_TYPE_SVC_CHANNEL_INTERFACE_GROUP,
+      tp_group_mixin_iface_init);
+    G_IMPLEMENT_INTERFACE (TP_TYPE_SVC_DBUS_PROPERTIES,
+      tp_dbus_properties_mixin_iface_init);
+    G_IMPLEMENT_INTERFACE (TP_TYPE_EXPORTABLE_CHANNEL, NULL);
+    G_IMPLEMENT_INTERFACE (TP_TYPE_CHANNEL_IFACE, NULL))
+
+/* Subclass for handle type LIST */
+G_DEFINE_TYPE_WITH_CODE (TpTestContactList, tp_test_contact_list,
+    TP_TEST_TYPE_CONTACT_LIST_BASE,
+    G_IMPLEMENT_INTERFACE (TP_TYPE_SVC_CHANNEL, list_channel_iface_init))
+
+/* Subclass for handle type GROUP */
+G_DEFINE_TYPE_WITH_CODE (TpTestContactGroup, tp_test_contact_group,
+    TP_TEST_TYPE_CONTACT_LIST_BASE,
+    G_IMPLEMENT_INTERFACE (TP_TYPE_SVC_CHANNEL, group_channel_iface_init))
+
+static const gchar *contact_list_interfaces[] = {
+    TP_IFACE_CHANNEL_INTERFACE_GROUP,
+    NULL
+};
+
+enum
+{
+  PROP_OBJECT_PATH = 1,
+  PROP_CHANNEL_TYPE,
+  PROP_HANDLE_TYPE,
+  PROP_HANDLE,
+  PROP_TARGET_ID,
+  PROP_REQUESTED,
+  PROP_INITIATOR_HANDLE,
+  PROP_INITIATOR_ID,
+  PROP_CONNECTION,
+  PROP_MANAGER,
+  PROP_INTERFACES,
+  PROP_CHANNEL_DESTROYED,
+  PROP_CHANNEL_PROPERTIES,
+  N_PROPS
+};
+
+struct _TpTestContactListBasePrivate
+{
+  TpBaseConnection *conn;
+  TpTestContactListManager *manager;
+  gchar *object_path;
+  TpHandleType handle_type;
+  TpHandle handle;
+
+  /* These are really booleans, but gboolean is signed. Thanks, GLib */
+  unsigned closed:1;
+  unsigned disposed:1;
+};
+
+struct _TpTestContactListPrivate
+{
+  int dummy:1;
+};
+
+struct _TpTestContactGroupPrivate
+{
+  int dummy:1;
+};
+
+static void
+tp_test_contact_list_base_init (TpTestContactListBase *self)
+{
+  self->priv = G_TYPE_INSTANCE_GET_PRIVATE (self,
+      TP_TEST_TYPE_CONTACT_LIST_BASE, TpTestContactListBasePrivate);
+}
+
+static void
+tp_test_contact_list_init (TpTestContactList *self)
+{
+  self->priv = G_TYPE_INSTANCE_GET_PRIVATE (self, TP_TEST_TYPE_CONTACT_LIST,
+      TpTestContactListPrivate);
+}
+
+static void
+tp_test_contact_group_init (TpTestContactGroup *self)
+{
+  self->priv = G_TYPE_INSTANCE_GET_PRIVATE (self, TP_TEST_TYPE_CONTACT_GROUP,
+      TpTestContactGroupPrivate);
+}
+
+static void
+constructed (GObject *object)
+{
+  TpTestContactListBase *self = TP_TEST_CONTACT_LIST_BASE (object);
+  void (*chain_up) (GObject *) =
+    ((GObjectClass *) tp_test_contact_list_base_parent_class)->constructed;
+  TpHandleRepoIface *contact_repo = tp_base_connection_get_handles
+      (self->priv->conn, TP_HANDLE_TYPE_CONTACT);
+  TpHandle self_handle = self->priv->conn->self_handle;
+  TpHandleRepoIface *handle_repo = tp_base_connection_get_handles
+      (self->priv->conn, self->priv->handle_type);
+
+  if (chain_up != NULL)
+    chain_up (object);
+
+  g_assert (TP_IS_BASE_CONNECTION (self->priv->conn));
+  g_assert (TP_TEST_IS_CONTACT_LIST_MANAGER (self->priv->manager));
+
+  tp_dbus_daemon_register_object (
+      tp_base_connection_get_dbus_daemon (self->priv->conn),
+      self->priv->object_path, self);
+
+  tp_handle_ref (handle_repo, self->priv->handle);
+  tp_group_mixin_init (object, G_STRUCT_OFFSET (TpTestContactListBase, group),
+      contact_repo, self_handle);
+  /* Both the subclasses have full support for telepathy-spec 0.17.6. */
+  tp_group_mixin_change_flags (object,
+      TP_CHANNEL_GROUP_FLAG_PROPERTIES, 0);
+}
+
+static void
+list_constructed (GObject *object)
+{
+  TpTestContactList *self = TP_TEST_CONTACT_LIST (object);
+  void (*chain_up) (GObject *) =
+    ((GObjectClass *) tp_test_contact_list_parent_class)->constructed;
+
+  if (chain_up != NULL)
+    chain_up (object);
+
+  g_assert (self->parent.priv->handle_type == TP_HANDLE_TYPE_LIST);
+
+  switch (self->parent.priv->handle)
+    {
+    case TP_TEST_CONTACT_LIST_PUBLISH:
+      /* We can stop publishing presence to people, but we can't
+       * start sending people our presence unless they ask for it.
+       *
+       * (We can accept people's requests to see our presence - but that's
+       * always allowed, so there's no flag.)
+       */
+      tp_group_mixin_change_flags (object,
+          TP_CHANNEL_GROUP_FLAG_CAN_REMOVE, 0);
+      break;
+    case TP_TEST_CONTACT_LIST_STORED:
+      /* We can add people to our roster (not that that's very useful without
+       * also adding them to subscribe), and we can remove them altogether
+       * (which implicitly removes them from subscribe, publish, and all
+       * user-defined groups).
+       */
+      tp_group_mixin_change_flags (object,
+          TP_CHANNEL_GROUP_FLAG_CAN_ADD | TP_CHANNEL_GROUP_FLAG_CAN_REMOVE, 0);
+      break;
+    case TP_TEST_CONTACT_LIST_SUBSCRIBE:
+      /* We can ask people to show us their presence, attaching a message.
+       * We can also cancel (rescind) requests that they haven't replied to,
+       * and stop receiving their presence after they allow it.
+       */
+      tp_group_mixin_change_flags (object,
+          TP_CHANNEL_GROUP_FLAG_CAN_ADD | TP_CHANNEL_GROUP_FLAG_MESSAGE_ADD |
+          TP_CHANNEL_GROUP_FLAG_CAN_REMOVE |
+          TP_CHANNEL_GROUP_FLAG_CAN_RESCIND,
+          0);
+      break;
+    default:
+      g_assert_not_reached ();
+    }
+}
+
+static void
+group_constructed (GObject *object)
+{
+  TpTestContactGroup *self = TP_TEST_CONTACT_GROUP (object);
+  void (*chain_up) (GObject *) =
+    ((GObjectClass *) tp_test_contact_group_parent_class)->constructed;
+
+  if (chain_up != NULL)
+    chain_up (object);
+
+  g_assert (self->parent.priv->handle_type == TP_HANDLE_TYPE_GROUP);
+
+  /* We can add people to user-defined groups, and also remove them. */
+  tp_group_mixin_change_flags (object,
+      TP_CHANNEL_GROUP_FLAG_CAN_ADD | TP_CHANNEL_GROUP_FLAG_CAN_REMOVE, 0);
+}
+
+
+static void
+get_property (GObject *object,
+              guint property_id,
+              GValue *value,
+              GParamSpec *pspec)
+{
+  TpTestContactListBase *self = TP_TEST_CONTACT_LIST_BASE (object);
+
+  switch (property_id)
+    {
+    case PROP_OBJECT_PATH:
+      g_value_set_string (value, self->priv->object_path);
+      break;
+    case PROP_CHANNEL_TYPE:
+      g_value_set_static_string (value, TP_IFACE_CHANNEL_TYPE_CONTACT_LIST);
+      break;
+    case PROP_HANDLE_TYPE:
+      g_value_set_uint (value, self->priv->handle_type);
+      break;
+    case PROP_HANDLE:
+      g_value_set_uint (value, self->priv->handle);
+      break;
+    case PROP_TARGET_ID:
+        {
+          TpHandleRepoIface *handle_repo = tp_base_connection_get_handles (
+              self->priv->conn, self->priv->handle_type);
+
+          g_value_set_string (value,
+              tp_handle_inspect (handle_repo, self->priv->handle));
+        }
+      break;
+    case PROP_REQUESTED:
+      g_value_set_boolean (value, FALSE);
+      break;
+    case PROP_INITIATOR_HANDLE:
+      g_value_set_uint (value, 0);
+      break;
+    case PROP_INITIATOR_ID:
+      g_value_set_static_string (value, "");
+      break;
+    case PROP_CONNECTION:
+      g_value_set_object (value, self->priv->conn);
+      break;
+    case PROP_MANAGER:
+      g_value_set_object (value, self->priv->manager);
+      break;
+    case PROP_INTERFACES:
+      g_value_set_boxed (value, contact_list_interfaces);
+      break;
+    case PROP_CHANNEL_DESTROYED:
+      g_value_set_boolean (value, self->priv->closed);
+      break;
+    case PROP_CHANNEL_PROPERTIES:
+      g_value_take_boxed (value,
+          tp_dbus_properties_mixin_make_properties_hash (object,
+              TP_IFACE_CHANNEL, "ChannelType",
+              TP_IFACE_CHANNEL, "TargetHandleType",
+              TP_IFACE_CHANNEL, "TargetHandle",
+              TP_IFACE_CHANNEL, "TargetID",
+              TP_IFACE_CHANNEL, "InitiatorHandle",
+              TP_IFACE_CHANNEL, "InitiatorID",
+              TP_IFACE_CHANNEL, "Requested",
+              TP_IFACE_CHANNEL, "Interfaces",
+              NULL));
+      break;
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+      break;
+    }
+}
+
+static void
+set_property (GObject *object,
+              guint property_id,
+              const GValue *value,
+              GParamSpec *pspec)
+{
+  TpTestContactListBase *self = TP_TEST_CONTACT_LIST_BASE (object);
+
+  switch (property_id)
+    {
+    case PROP_OBJECT_PATH:
+      g_free (self->priv->object_path);
+      self->priv->object_path = g_value_dup_string (value);
+      break;
+    case PROP_HANDLE:
+      /* we don't ref it here because we don't necessarily have access to the
+       * repository (or even type) yet - instead we ref it in the constructor.
+       */
+      self->priv->handle = g_value_get_uint (value);
+      break;
+    case PROP_HANDLE_TYPE:
+      self->priv->handle_type = g_value_get_uint (value);
+      break;
+    case PROP_CHANNEL_TYPE:
+      /* this property is writable in the interface, but not actually
+       * meaningfully changable on this channel, so we do nothing */
+      break;
+    case PROP_CONNECTION:
+      self->priv->conn = g_value_get_object (value);
+      break;
+    case PROP_MANAGER:
+      self->priv->manager = g_value_get_object (value);
+      break;
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+      break;
+    }
+}
+
+static void
+dispose (GObject *object)
+{
+  TpTestContactListBase *self = TP_TEST_CONTACT_LIST_BASE (object);
+
+  if (self->priv->disposed)
+    return;
+
+  self->priv->disposed = TRUE;
+
+  if (!self->priv->closed)
+    {
+      self->priv->closed = TRUE;
+      tp_svc_channel_emit_closed (self);
+    }
+
+  ((GObjectClass *) tp_test_contact_list_base_parent_class)->dispose (object);
+}
+
+static void
+finalize (GObject *object)
+{
+  TpTestContactListBase *self = TP_TEST_CONTACT_LIST_BASE (object);
+  TpHandleRepoIface *handle_repo = tp_base_connection_get_handles
+      (self->priv->conn, self->priv->handle_type);
+
+  tp_handle_unref (handle_repo, self->priv->handle);
+  g_free (self->priv->object_path);
+  tp_group_mixin_finalize (object);
+
+  ((GObjectClass *) tp_test_contact_list_base_parent_class)->finalize (object);
+}
+
+static gboolean
+group_add_member (GObject *object,
+                  TpHandle handle,
+                  const gchar *message,
+                  GError **error)
+{
+  TpTestContactListBase *self = TP_TEST_CONTACT_LIST_BASE (object);
+
+  return tp_test_contact_list_manager_add_to_group (self->priv->manager,
+      object, self->priv->handle, handle, message, error);
+}
+
+static gboolean
+group_remove_member (GObject *object,
+                     TpHandle handle,
+                     const gchar *message,
+                     GError **error)
+{
+  TpTestContactListBase *self = TP_TEST_CONTACT_LIST_BASE (object);
+
+  return tp_test_contact_list_manager_remove_from_group (self->priv->manager,
+      object, self->priv->handle, handle, message, error);
+}
+
+static gboolean
+list_add_member (GObject *object,
+                 TpHandle handle,
+                 const gchar *message,
+                 GError **error)
+{
+  TpTestContactListBase *self = TP_TEST_CONTACT_LIST_BASE (object);
+
+  return tp_test_contact_list_manager_add_to_list (self->priv->manager,
+      object, self->priv->handle, handle, message, error);
+}
+
+static gboolean
+list_remove_member (GObject *object,
+                    TpHandle handle,
+                    const gchar *message,
+                    GError **error)
+{
+  TpTestContactListBase *self = TP_TEST_CONTACT_LIST_BASE (object);
+
+  return tp_test_contact_list_manager_remove_from_list (self->priv->manager,
+      object, self->priv->handle, handle, message, error);
+}
+
+static void
+tp_test_contact_list_base_class_init (TpTestContactListBaseClass *klass)
+{
+  static TpDBusPropertiesMixinPropImpl channel_props[] = {
+      { "TargetHandleType", "handle-type", NULL },
+      { "TargetHandle", "handle", NULL },
+      { "ChannelType", "channel-type", NULL },
+      { "Interfaces", "interfaces", NULL },
+      { "TargetID", "target-id", NULL },
+      { "Requested", "requested", NULL },
+      { "InitiatorHandle", "initiator-handle", NULL },
+      { "InitiatorID", "initiator-id", NULL },
+      { NULL }
+  };
+  static TpDBusPropertiesMixinIfaceImpl prop_interfaces[] = {
+      { TP_IFACE_CHANNEL,
+        tp_dbus_properties_mixin_getter_gobject_properties,
+        NULL,
+        channel_props,
+      },
+      { NULL }
+  };
+  GObjectClass *object_class = (GObjectClass *) klass;
+  GParamSpec *param_spec;
+
+  g_type_class_add_private (klass, sizeof (TpTestContactListBasePrivate));
+
+  object_class->constructed = constructed;
+  object_class->set_property = set_property;
+  object_class->get_property = get_property;
+  object_class->dispose = dispose;
+  object_class->finalize = finalize;
+
+  g_object_class_override_property (object_class, PROP_OBJECT_PATH,
+      "object-path");
+  g_object_class_override_property (object_class, PROP_CHANNEL_TYPE,
+      "channel-type");
+  g_object_class_override_property (object_class, PROP_HANDLE_TYPE,
+      "handle-type");
+  g_object_class_override_property (object_class, PROP_HANDLE, "handle");
+
+  g_object_class_override_property (object_class, PROP_CHANNEL_DESTROYED,
+      "channel-destroyed");
+  g_object_class_override_property (object_class, PROP_CHANNEL_PROPERTIES,
+      "channel-properties");
+
+  param_spec = g_param_spec_object ("connection", "TpBaseConnection object",
+      "Connection object that owns this channel",
+      TP_TYPE_BASE_CONNECTION,
+      G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+  g_object_class_install_property (object_class, PROP_CONNECTION, param_spec);
+
+  param_spec = g_param_spec_object ("manager", "TpTestContactListManager",
+      "TpTestContactListManager object that owns this channel",
+      TP_TEST_TYPE_CONTACT_LIST_MANAGER,
+      G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+  g_object_class_install_property (object_class, PROP_MANAGER, param_spec);
+
+  param_spec = g_param_spec_boxed ("interfaces", "Extra D-Bus interfaces",
+      "Additional Channel.Interface.* interfaces",
+      G_TYPE_STRV,
+      G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+  g_object_class_install_property (object_class, PROP_INTERFACES, param_spec);
+
+  param_spec = g_param_spec_string ("target-id", "Chatroom's ID",
+      "The string obtained by inspecting the MUC's handle",
+      NULL,
+      G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+  g_object_class_install_property (object_class, PROP_TARGET_ID, param_spec);
+
+  param_spec = g_param_spec_uint ("initiator-handle", "Initiator's handle",
+      "The contact who initiated the channel",
+      0, G_MAXUINT32, 0,
+      G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+  g_object_class_install_property (object_class, PROP_INITIATOR_HANDLE,
+      param_spec);
+
+  param_spec = g_param_spec_string ("initiator-id", "Initiator's ID",
+      "The string obtained by inspecting the initiator-handle",
+      NULL,
+      G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+  g_object_class_install_property (object_class, PROP_INITIATOR_ID,
+      param_spec);
+
+  param_spec = g_param_spec_boolean ("requested", "Requested?",
+      "True if this channel was requested by the local user",
+      FALSE,
+      G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+  g_object_class_install_property (object_class, PROP_REQUESTED, param_spec);
+
+  klass->dbus_properties_class.interfaces = prop_interfaces;
+  tp_dbus_properties_mixin_class_init (object_class,
+      G_STRUCT_OFFSET (TpTestContactListBaseClass, dbus_properties_class));
+
+  /* Group mixin is initialized separately for each subclass - they have
+   *  different callbacks */
+}
+
+static void
+tp_test_contact_list_class_init (TpTestContactListClass *klass)
+{
+  GObjectClass *object_class = (GObjectClass *) klass;
+
+  g_type_class_add_private (klass, sizeof (TpTestContactListPrivate));
+
+  object_class->constructed = list_constructed;
+
+  tp_group_mixin_class_init (object_class,
+      G_STRUCT_OFFSET (TpTestContactListBaseClass, group_class),
+      list_add_member,
+      list_remove_member);
+  tp_group_mixin_init_dbus_properties (object_class);
+}
+
+static void
+tp_test_contact_group_class_init (TpTestContactGroupClass *klass)
+{
+  GObjectClass *object_class = (GObjectClass *) klass;
+
+  g_type_class_add_private (klass, sizeof (TpTestContactGroupPrivate));
+
+  object_class->constructed = group_constructed;
+
+  tp_group_mixin_class_init (object_class,
+      G_STRUCT_OFFSET (TpTestContactListBaseClass, group_class),
+      group_add_member,
+      group_remove_member);
+  tp_group_mixin_init_dbus_properties (object_class);
+}
+
+static void
+list_channel_close (TpSvcChannel *iface G_GNUC_UNUSED,
+                    DBusGMethodInvocation *context)
+{
+  GError e = { TP_ERRORS, TP_ERROR_NOT_IMPLEMENTED,
+      "ContactList channels with handle type LIST may not be closed" };
+
+  dbus_g_method_return_error (context, &e);
+}
+
+static void
+group_channel_close (TpSvcChannel *iface,
+                     DBusGMethodInvocation *context)
+{
+  TpTestContactGroup *self = TP_TEST_CONTACT_GROUP (iface);
+  TpTestContactListBase *base = TP_TEST_CONTACT_LIST_BASE (iface);
+
+  if (tp_handle_set_size (base->group.members) > 0)
+    {
+      GError e = { TP_ERRORS, TP_ERROR_NOT_AVAILABLE,
+          "Non-empty groups may not be deleted (closed)" };
+
+      dbus_g_method_return_error (context, &e);
+      return;
+    }
+
+  if (!base->priv->closed)
+    {
+      /* If this was a real connection manager we'd delete the group here,
+       * if such a concept existed in the protocol (in XMPP, it doesn't).
+       *
+       * Afterwards, close the channel:
+       */
+      base->priv->closed = TRUE;
+      tp_svc_channel_emit_closed (self);
+    }
+
+  tp_svc_channel_return_from_close (context);
+}
+
+static void
+channel_get_channel_type (TpSvcChannel *iface G_GNUC_UNUSED,
+                          DBusGMethodInvocation *context)
+{
+  tp_svc_channel_return_from_get_channel_type (context,
+      TP_IFACE_CHANNEL_TYPE_CONTACT_LIST);
+}
+
+static void
+channel_get_handle (TpSvcChannel *iface,
+                    DBusGMethodInvocation *context)
+{
+  TpTestContactListBase *self = TP_TEST_CONTACT_LIST_BASE (iface);
+
+  tp_svc_channel_return_from_get_handle (context, self->priv->handle_type,
+      self->priv->handle);
+}
+
+static void
+channel_get_interfaces (TpSvcChannel *iface G_GNUC_UNUSED,
+                        DBusGMethodInvocation *context)
+{
+  tp_svc_channel_return_from_get_interfaces (context,
+      contact_list_interfaces);
+}
+
+static void
+channel_iface_init (gpointer iface,
+                    gpointer data)
+{
+  TpSvcChannelClass *klass = iface;
+
+#define IMPLEMENT(x) tp_svc_channel_implement_##x (klass, channel_##x)
+  /* close is implemented in subclasses, so don't IMPLEMENT (close); */
+  IMPLEMENT (get_channel_type);
+  IMPLEMENT (get_handle);
+  IMPLEMENT (get_interfaces);
+#undef IMPLEMENT
+}
+
+static void
+list_channel_iface_init (gpointer iface,
+                         gpointer data G_GNUC_UNUSED)
+{
+  TpSvcChannelClass *klass = iface;
+
+#define IMPLEMENT(x) tp_svc_channel_implement_##x (klass, list_channel_##x)
+  IMPLEMENT (close);
+#undef IMPLEMENT
+}
+
+static void
+group_channel_iface_init (gpointer iface,
+                          gpointer data G_GNUC_UNUSED)
+{
+  TpSvcChannelClass *klass = iface;
+
+#define IMPLEMENT(x) tp_svc_channel_implement_##x (klass, group_channel_##x)
+  IMPLEMENT (close);
+#undef IMPLEMENT
+}
diff --git a/tests/lib/telepathy/contactlist/contact-list.h b/tests/lib/telepathy/contactlist/contact-list.h
new file mode 100644
index 0000000..2d7d26b
--- /dev/null
+++ b/tests/lib/telepathy/contactlist/contact-list.h
@@ -0,0 +1,118 @@
+/*
+ * TpTest ContactList channels with handle type LIST or GROUP
+ *
+ * Copyright © 2009 Collabora Ltd. <http://www.collabora.co.uk/>
+ * Copyright © 2009 Nokia Corporation
+ *
+ * Copying and distribution of this file, with or without modification,
+ * are permitted in any medium without royalty provided the copyright
+ * notice and this notice are preserved.
+ */
+
+#ifndef TP_TEST_CONTACT_LIST_H
+#define TP_TEST_CONTACT_LIST_H
+
+#include <glib-object.h>
+
+#include <telepathy-glib/base-connection.h>
+#include <telepathy-glib/group-mixin.h>
+
+G_BEGIN_DECLS
+
+typedef struct _TpTestContactListBase TpTestContactListBase;
+typedef struct _TpTestContactListBaseClass TpTestContactListBaseClass;
+typedef struct _TpTestContactListBasePrivate TpTestContactListBasePrivate;
+
+typedef struct _TpTestContactList TpTestContactList;
+typedef struct _TpTestContactListClass TpTestContactListClass;
+typedef struct _TpTestContactListPrivate TpTestContactListPrivate;
+
+typedef struct _TpTestContactGroup TpTestContactGroup;
+typedef struct _TpTestContactGroupClass TpTestContactGroupClass;
+typedef struct _TpTestContactGroupPrivate TpTestContactGroupPrivate;
+
+GType tp_test_contact_list_base_get_type (void);
+GType tp_test_contact_list_get_type (void);
+GType tp_test_contact_group_get_type (void);
+
+#define TP_TEST_TYPE_CONTACT_LIST_BASE \
+  (tp_test_contact_list_base_get_type ())
+#define TP_TEST_CONTACT_LIST_BASE(obj) \
+  (G_TYPE_CHECK_INSTANCE_CAST ((obj), TP_TEST_TYPE_CONTACT_LIST_BASE, \
+                               TpTestContactListBase))
+#define TP_TEST_CONTACT_LIST_BASE_CLASS(klass) \
+  (G_TYPE_CHECK_CLASS_CAST ((klass), TP_TEST_TYPE_CONTACT_LIST_BASE, \
+                            TpTestContactListBaseClass))
+#define TP_TEST_IS_CONTACT_LIST_BASE(obj) \
+  (G_TYPE_CHECK_INSTANCE_TYPE ((obj), TP_TEST_TYPE_CONTACT_LIST_BASE))
+#define TP_TEST_IS_CONTACT_LIST_BASE_CLASS(klass) \
+  (G_TYPE_CHECK_CLASS_TYPE ((klass), TP_TEST_TYPE_CONTACT_LIST_BASE))
+#define TP_TEST_CONTACT_LIST_BASE_GET_CLASS(obj) \
+  (G_TYPE_INSTANCE_GET_CLASS ((obj), TP_TEST_TYPE_CONTACT_LIST_BASE, \
+                              TpTestContactListBaseClass))
+
+#define TP_TEST_TYPE_CONTACT_LIST \
+  (tp_test_contact_list_get_type ())
+#define TP_TEST_CONTACT_LIST(obj) \
+  (G_TYPE_CHECK_INSTANCE_CAST ((obj), TP_TEST_TYPE_CONTACT_LIST, \
+                               TpTestContactList))
+#define TP_TEST_CONTACT_LIST_CLASS(klass) \
+  (G_TYPE_CHECK_CLASS_CAST ((klass), TP_TEST_TYPE_CONTACT_LIST, \
+                            TpTestContactListClass))
+#define TP_TEST_IS_CONTACT_LIST(obj) \
+  (G_TYPE_CHECK_INSTANCE_TYPE ((obj), TP_TEST_TYPE_CONTACT_LIST))
+#define TP_TEST_IS_CONTACT_LIST_CLASS(klass) \
+  (G_TYPE_CHECK_CLASS_TYPE ((klass), TP_TEST_TYPE_CONTACT_LIST))
+#define TP_TEST_CONTACT_LIST_GET_CLASS(obj) \
+  (G_TYPE_INSTANCE_GET_CLASS ((obj), TP_TEST_TYPE_CONTACT_LIST, \
+                              TpTestContactListClass))
+
+#define TP_TEST_TYPE_CONTACT_GROUP \
+  (tp_test_contact_group_get_type ())
+#define TP_TEST_CONTACT_GROUP(obj) \
+  (G_TYPE_CHECK_INSTANCE_CAST ((obj), TP_TEST_TYPE_CONTACT_GROUP, \
+                               TpTestContactGroup))
+#define TP_TEST_CONTACT_GROUP_CLASS(klass) \
+  (G_TYPE_CHECK_CLASS_CAST ((klass), TP_TEST_TYPE_CONTACT_GROUP, \
+                            TpTestContactGroupClass))
+#define TP_TEST_IS_CONTACT_GROUP(obj) \
+  (G_TYPE_CHECK_INSTANCE_TYPE ((obj), TP_TEST_TYPE_CONTACT_GROUP))
+#define TP_TEST_IS_CONTACT_GROUP_CLASS(klass) \
+  (G_TYPE_CHECK_CLASS_TYPE ((klass), TP_TEST_TYPE_CONTACT_GROUP))
+#define TP_TEST_CONTACT_GROUP_GET_CLASS(obj) \
+  (G_TYPE_INSTANCE_GET_CLASS ((obj), TP_TEST_TYPE_CONTACT_GROUP, \
+                              TpTestContactGroupClass))
+
+struct _TpTestContactListBaseClass {
+    GObjectClass parent_class;
+    TpGroupMixinClass group_class;
+    TpDBusPropertiesMixinClass dbus_properties_class;
+};
+
+struct _TpTestContactListClass {
+    TpTestContactListBaseClass parent_class;
+};
+
+struct _TpTestContactGroupClass {
+    TpTestContactListBaseClass parent_class;
+};
+
+struct _TpTestContactListBase {
+    GObject parent;
+    TpGroupMixin group;
+    TpTestContactListBasePrivate *priv;
+};
+
+struct _TpTestContactList {
+    TpTestContactListBase parent;
+    TpTestContactListPrivate *priv;
+};
+
+struct _TpTestContactGroup {
+    TpTestContactListBase parent;
+    TpTestContactGroupPrivate *priv;
+};
+
+G_END_DECLS
+
+#endif
diff --git a/tests/lib/telepathy/contactlist/manager-file.py b/tests/lib/telepathy/contactlist/manager-file.py
new file mode 100755
index 0000000..c1d3680
--- /dev/null
+++ b/tests/lib/telepathy/contactlist/manager-file.py
@@ -0,0 +1,23 @@
+# Input for tools/manager-file.py
+
+MANAGER = 'tp_test_contact_list'
+PARAMS = {
+        'tp_test' : {
+            'account': {
+                'dtype': 's',
+                'flags': 'required register',
+                'filter': 'account_param_filter',
+                # 'filter_data': 'NULL',
+                # 'default': ...,
+                # 'struct_field': '...',
+                # 'setter_data': 'NULL',
+                },
+            'simulation-delay': {
+                'dtype': 'u',
+                'default': 1000,
+                },
+            },
+        }
+STRUCTS = {
+        'tp_test': 'TpTestParams'
+        }
diff --git a/tests/lib/telepathy/contactlist/session.conf.in b/tests/lib/telepathy/contactlist/session.conf.in
new file mode 100644
index 0000000..0388c32
--- /dev/null
+++ b/tests/lib/telepathy/contactlist/session.conf.in
@@ -0,0 +1,54 @@
+<!DOCTYPE busconfig PUBLIC
+ "-//freedesktop//DTD D-BUS Bus Configuration 1.0//EN"
+ "http://www.freedesktop.org/standards/dbus/1.0/busconfig.dtd";>
+<busconfig>
+  <!-- Our well-known bus type, don't change this -->
+  <type>session</type>
+
+  <!-- If we fork, keep the user's original umask to avoid affecting
+       the behavior of child processes. -->
+  <keep_umask/>
+
+  <listen>unix:tmpdir=/tmp</listen>
+
+  <!-- Search for .service files in our special services dir -->
+  <servicedir>@abs_top_srcdir@/tests/lib/backends/telepathy/contactlist/_gen</servicedir>
+
+  <policy context="default">
+    <!-- Allow everything to be sent -->
+    <allow send_destination="*" eavesdrop="true"/>
+    <!-- Allow everything to be received -->
+    <allow eavesdrop="true"/>
+    <!-- Allow anyone to own anything -->
+    <allow own="*"/>
+  </policy>
+
+  <!-- raise the service start timeout to 40 seconds as it can timeout
+       on the live cd on slow machines -->
+  <limit name="service_start_timeout">60000</limit>
+
+  <include if_selinux_enabled="yes" selinux_root_relative="yes">contexts/dbus_contexts</include>
+
+  <!-- For the session bus, override the default relatively-low limits
+       with essentially infinite limits, since the bus is just running
+       as the user anyway, using up bus resources is not something we need
+       to worry about. In some cases, we do set the limits lower than
+       "all available memory" if exceeding the limit is almost certainly a bug,
+       having the bus enforce a limit is nicer than a huge memory leak. But the
+       intent is that these limits should never be hit. -->
+
+  <!-- the memory limits are 1G instead of say 4G because they can't exceed 32-bit signed int max -->
+  <limit name="max_incoming_bytes">1000000000</limit>
+  <limit name="max_outgoing_bytes">1000000000</limit>
+  <limit name="max_message_size">1000000000</limit>
+  <limit name="service_start_timeout">120000</limit>
+  <limit name="auth_timeout">240000</limit>
+  <limit name="max_completed_connections">100000</limit>
+  <limit name="max_incomplete_connections">10000</limit>
+  <limit name="max_connections_per_user">100000</limit>
+  <limit name="max_pending_service_starts">10000</limit>
+  <limit name="max_names_per_connection">50000</limit>
+  <limit name="max_match_rules_per_connection">50000</limit>
+  <limit name="max_replies_per_connection">50000</limit>
+
+</busconfig>
diff --git a/tests/lib/telepathy/contactlist/tp-test-contactlist.deps b/tests/lib/telepathy/contactlist/tp-test-contactlist.deps
new file mode 100644
index 0000000..0d850c2
--- /dev/null
+++ b/tests/lib/telepathy/contactlist/tp-test-contactlist.deps
@@ -0,0 +1,5 @@
+gio-2.0
+dbus-glib-1
+gobject-2.0
+gio-2.0
+telepathy-glib
diff --git a/tests/lib/telepathy/contactlist/tp-test-contactlist.h b/tests/lib/telepathy/contactlist/tp-test-contactlist.h
new file mode 100644
index 0000000..19088b9
--- /dev/null
+++ b/tests/lib/telepathy/contactlist/tp-test-contactlist.h
@@ -0,0 +1,10 @@
+#ifndef __EXAMPLE_CONTACTLIST_H__
+#define __EXAMPLE_CONTACTLIST_H__
+
+#include <account-manager.h>
+#include <account.h>
+#include <conn.h>
+#include <contact-list-manager.h>
+#include <contact-list.h>
+
+#endif /* __EXAMPLE_CONTACTLIST_H__ */
diff --git a/tests/telepathy/Makefile.am b/tests/telepathy/Makefile.am
new file mode 100644
index 0000000..39b6896
--- /dev/null
+++ b/tests/telepathy/Makefile.am
@@ -0,0 +1,63 @@
+AM_CPPFLAGS = \
+	$(GLIB_CFLAGS) \
+	$(GEE_CFLAGS) \
+	$(TP_GLIB_CFLAGS) \
+	-I$(top_srcdir)/folks \
+	-I$(top_srcdir)/backends/telepathy \
+	-I$(top_srcdir)/tests/lib/telepathy/contactlist \
+	-include $(CONFIG_HEADER) \
+	$(NULL)
+
+LDADD = \
+	$(GLIB_LIBS) \
+	$(GEE_LIBS) \
+	$(TP_GLIB_LIBS) \
+	$(NULL)
+
+RUN_WITH_PRIVATE_BUS = $(top_srcdir)/tests/tools/with-session-bus.sh
+
+VALAFLAGS += \
+	--vapidir=$(top_builddir)/tests/lib/telepathy/contactlist/ \
+	--vapidir=. \
+	--vapidir=$(top_srcdir)/folks \
+	--vapidir=$(top_srcdir)/backends/telepathy/lib \
+	--pkg gobject-2.0 \
+	--pkg gio-2.0 \
+	--pkg gee-1.0 \
+	--pkg gmodule-2.0 \
+	--pkg dbus-glib-1 \
+	--pkg telepathy-glib \
+	--pkg folks \
+	--pkg folks-telepathy \
+	--pkg tp-test-contactlist \
+	$(NULL)
+
+TESTS = \
+	test-contact-retrieval \
+	$(NULL)
+
+noinst_PROGRAMS = \
+	contact-retrieval \
+	$(NULL)
+
+contact_retrieval_SOURCES = \
+	test-case.vala \
+	contact-retrieval.vala \
+	$(NULL)
+
+contact_retrieval_LDADD = \
+	$(top_builddir)/tests/lib/telepathy/contactlist/libtp-test-contactlist.la \
+	$(top_builddir)/folks/libfolks.la
+
+CLEANFILES = \
+	$(TESTS) \
+	$(NULL)
+
+test-contact-retrieval: contact-retrieval
+	{ echo "#!/bin/sh" && \
+	echo -n "$(RUN_WITH_PRIVATE_BUS) " && \
+	echo "--config-file=$(top_srcdir)/tests/lib/telepathy/contactlist/session.conf -- ./$<"; } \
+	> $@
+	chmod +x $@
+
+-include $(top_srcdir)/git.mk
diff --git a/tests/telepathy/contact-retrieval.vala b/tests/telepathy/contact-retrieval.vala
new file mode 100644
index 0000000..61d68bf
--- /dev/null
+++ b/tests/telepathy/contact-retrieval.vala
@@ -0,0 +1,191 @@
+using DBus;
+using TelepathyGLib;
+using TpTest;
+using Tpf;
+using Folks;
+using Gee;
+
+public class ContactRetrievalTests : Folks.TestCase
+{
+  private DBusDaemon daemon;
+  private TpTest.Account account;
+  private TpTest.AccountManager account_manager;
+  private TpTest.ContactListConnection conn;
+  private MainLoop main_loop;
+  private string bus_name;
+  private string object_path;
+
+  public ContactRetrievalTests ()
+    {
+      base ("ContactRetrieval");
+
+      this.add_test ("aggregator", this.test_aggregator);
+    }
+
+  public override void set_up ()
+    {
+      this.main_loop = new GLib.MainLoop (null, false);
+
+      try
+        {
+          this.daemon = DBusDaemon.dup ();
+        }
+      catch (GLib.Error e)
+        {
+          error ("Couldn't get D-Bus daemon: %s", e.message);
+        }
+
+      /* Set up a contact list connection */
+      this.conn = new TpTest.ContactListConnection ("me example com",
+          "protocol");
+
+      try
+        {
+          this.conn.register ("cm", out this.bus_name, out this.object_path);
+        }
+      catch (GLib.Error e)
+        {
+          error ("Failed to register connection %p.", this.conn);
+        }
+
+      var handle_repo = this.conn.get_handles (HandleType.CONTACT);
+      Handle self_handle = 0;
+      try
+        {
+          self_handle = TelepathyGLib.handle_ensure (handle_repo,
+              "me example com", null);
+        }
+      catch (GLib.Error e)
+        {
+          error ("Couldn't ensure self handle '%s': %s", "me example com",
+              e.message);
+        }
+
+      this.conn.set_self_handle (self_handle);
+      this.conn.change_status (ConnectionStatus.CONNECTED,
+          ConnectionStatusReason.REQUESTED);
+
+      /* Create an account */
+      this.account = new TpTest.Account (this.object_path);
+      this.daemon.register_object (
+          TelepathyGLib.ACCOUNT_OBJECT_PATH_BASE + "cm/protocol/account",
+          this.account);
+
+      /* Create an account manager */
+      try
+        {
+          this.daemon.request_name (TelepathyGLib.ACCOUNT_MANAGER_BUS_NAME,
+              false);
+        }
+      catch (GLib.Error e)
+        {
+          error ("Couldn't request account manager bus name '%s': %s",
+              TelepathyGLib.ACCOUNT_MANAGER_BUS_NAME, e.message);
+        }
+
+      this.account_manager = new TpTest.AccountManager ();
+      this.daemon.register_object (TelepathyGLib.ACCOUNT_MANAGER_OBJECT_PATH,
+          this.account_manager);
+    }
+
+  public override void tear_down ()
+    {
+      this.conn.change_status (ConnectionStatus.DISCONNECTED,
+          ConnectionStatusReason.REQUESTED);
+
+      this.daemon.unregister_object (this.account_manager);
+      this.account_manager = null;
+
+      try
+        {
+          this.daemon.release_name (TelepathyGLib.ACCOUNT_MANAGER_BUS_NAME);
+        }
+      catch (GLib.Error e)
+        {
+          error ("Couldn't release account manager bus name '%s': %s",
+              TelepathyGLib.ACCOUNT_MANAGER_BUS_NAME, e.message);
+        }
+
+      this.daemon.unregister_object (this.account);
+      this.account = null;
+
+      this.conn = null;
+      this.daemon = null;
+      this.bus_name = null;
+      this.object_path = null;
+
+      Timeout.add_seconds (5, () =>
+        {
+          this.main_loop.quit ();
+          this.main_loop = null;
+          return false;
+        });
+
+      /* Run the main loop to process the carnage and destruction */
+      this.main_loop.run ();
+    }
+
+  public void test_aggregator ()
+    {
+      var main_loop = new GLib.MainLoop (null, false);
+
+      /* Ignore the error caused by not running the logger */
+      Test.log_set_fatal_handler ((d, l, m) =>
+        {
+          return !m.has_suffix ("couldn't get list of favourite contacts: " +
+              "The name org.freedesktop.Telepathy.Logger was not provided by " +
+              "any .service files");
+        });
+
+      /* Create a set of the individuals we expect to see */
+      HashSet<string> expected_individuals = new HashSet<string> (str_hash,
+          str_equal);
+
+      string prefix = "telepathy:protocol:";
+      expected_individuals.add (prefix + "travis example com");
+      expected_individuals.add (prefix + "olivier example com");
+      expected_individuals.add (prefix + "guillaume example com");
+      expected_individuals.add (prefix + "sjoerd example com");
+      expected_individuals.add (prefix + "christian example com");
+      expected_individuals.add (prefix + "wim example com");
+      expected_individuals.add (prefix + "helen example com");
+      expected_individuals.add (prefix + "geraldine example com");
+
+      /* Set up the aggregator */
+      var aggregator = new IndividualAggregator ();
+      aggregator.individuals_changed.connect ((added, removed, m, a, r) =>
+        {
+          foreach (Individual i in added)
+            expected_individuals.remove (i.id);
+
+          assert (removed == null);
+        });
+      aggregator.prepare ();
+
+      /* Kill the main loop after a few seconds. If there are still individuals
+       * in the set of expected individuals, the aggregator has either failed
+       * or been too slow (which we can consider to be failure). */
+      Timeout.add_seconds (3, () =>
+        {
+          main_loop.quit ();
+          return false;
+        });
+
+      main_loop.run ();
+
+      /* We should have enumerated exactly the individuals in the set */
+      assert (expected_individuals.size == 0);
+    }
+}
+
+public int main (string[] args)
+{
+  Test.init (ref args);
+
+  TestSuite root = TestSuite.get_root ();
+  root.add_suite (new ContactRetrievalTests ().get_suite ());
+
+  Test.run ();
+
+  return 0;
+}
diff --git a/tests/telepathy/test-case.vala b/tests/telepathy/test-case.vala
new file mode 100644
index 0000000..0015ef4
--- /dev/null
+++ b/tests/telepathy/test-case.vala
@@ -0,0 +1,82 @@
+/* testcase.vala
+ *
+ * Copyright (C) 2009 Julien Peeters
+ *
+ * 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
+ *
+ * Author:
+ * 	Julien Peeters <contact julienpeeters fr>
+ *
+ * Copied from libgee/tests/testcase.vala.
+ */
+
+public abstract class Folks.TestCase : Object {
+
+	private GLib.TestSuite suite;
+	private Adaptor[] adaptors = new Adaptor[0];
+
+	public delegate void TestMethod ();
+
+	public TestCase (string name) {
+		this.suite = new GLib.TestSuite (name);
+	}
+
+	public void add_test (string name, TestMethod test) {
+		var adaptor = new Adaptor (name, test, this);
+		this.adaptors += adaptor;
+
+		this.suite.add (new GLib.TestCase (adaptor.name,
+		                                   adaptor.set_up,
+		                                   adaptor.run,
+		                                   adaptor.tear_down ));
+	}
+
+	public virtual void set_up () {
+	}
+
+	public virtual void tear_down () {
+	}
+
+	public GLib.TestSuite get_suite () {
+		return this.suite;
+	}
+
+	private class Adaptor {
+
+		public string name { get; private set; }
+		private TestMethod test;
+		private TestCase test_case;
+
+		public Adaptor (string name,
+		                TestMethod test,
+		                TestCase test_case) {
+			this.name = name;
+			this.test = test;
+			this.test_case = test_case;
+		}
+
+		public void set_up (void* fixture) {
+			this.test_case.set_up ();
+		}
+
+		public void run (void* fixture) {
+			this.test ();
+		}
+
+		public void tear_down (void* fixture) {
+			this.test_case.tear_down ();
+		}
+	}
+}
diff --git a/tests/tools/with-session-bus.sh b/tests/tools/with-session-bus.sh
new file mode 100755
index 0000000..063bd7e
--- /dev/null
+++ b/tests/tools/with-session-bus.sh
@@ -0,0 +1,94 @@
+#!/bin/sh
+# with-session-bus.sh - run a program with a temporary D-Bus session daemon
+#
+# The canonical location of this program is the telepathy-glib tools/
+# directory, please synchronize any changes with that copy.
+#
+# Copyright (C) 2007-2008 Collabora Ltd. <http://www.collabora.co.uk/>
+#
+# Copying and distribution of this file, with or without modification,
+# are permitted in any medium without royalty provided the copyright
+# notice and this notice are preserved.
+
+set -e
+
+me=with-session-bus
+
+dbus_daemon_args="--print-address=5 --print-pid=6 --fork"
+sleep=0
+
+usage ()
+{
+  echo "usage: $me [options] -- program [program_options]" >&2
+  echo "Requires write access to the current directory." >&2
+  echo "" >&2
+  echo "If \$WITH_SESSION_BUS_FORK_DBUS_MONITOR is set, fork dbus-monitor" >&2
+  echo "with the arguments in \$WITH_SESSION_BUS_FORK_DBUS_MONITOR_OPT." >&2
+  echo "The output of dbus-monitor is saved in $me-<pid>.dbus-monitor-logs" >&2
+  exit 2
+}
+
+while test "z$1" != "z--"; do
+  case "$1" in
+  --sleep=*)
+    sleep="$1"
+    sleep="${sleep#--sleep=}"
+    shift
+    ;;
+  --session)
+    dbus_daemon_args="$dbus_daemon_args --session"
+    shift
+    ;;
+  --config-file=*)
+    # FIXME: assumes config file doesn't contain any special characters
+    dbus_daemon_args="$dbus_daemon_args $1"
+    shift
+    ;;
+  *)
+    usage
+    ;;
+  esac
+done
+shift
+if test "z$1" = "z"; then usage; fi
+
+exec 5> $me-$$.address
+exec 6> $me-$$.pid
+
+cleanup ()
+{
+  pid=`head -n1 $me-$$.pid`
+  if test -n "$pid" ; then
+    echo "Killing temporary bus daemon: $pid" >&2
+    kill -INT "$pid"
+  fi
+  rm -f $me-$$.address
+  rm -f $me-$$.pid
+}
+
+trap cleanup INT HUP TERM
+dbus-daemon $dbus_daemon_args
+
+{ echo -n "Temporary bus daemon is "; cat $me-$$.address; } >&2
+{ echo -n "Temporary bus daemon PID is "; head -n1 $me-$$.pid; } >&2
+
+e=0
+DBUS_SESSION_BUS_ADDRESS="`cat $me-$$.address`"
+export DBUS_SESSION_BUS_ADDRESS
+
+if [ -n "$WITH_SESSION_BUS_FORK_DBUS_MONITOR" ] ; then
+  echo -n "Forking dbus-monitor $WITH_SESSION_BUS_FORK_DBUS_MONITOR_OPT" >&2
+  dbus-monitor $WITH_SESSION_BUS_FORK_DBUS_MONITOR_OPT \
+        > $me-$$.dbus-monitor-logs 2>&1 &
+fi
+
+"$@" || e=$?
+
+if test $sleep != 0; then
+  sleep $sleep
+fi
+
+trap - INT HUP TERM
+cleanup
+
+exit $e
diff --git a/tools/manager-file.py b/tools/manager-file.py
new file mode 100755
index 0000000..45f6404
--- /dev/null
+++ b/tools/manager-file.py
@@ -0,0 +1,175 @@
+#!/usr/bin/python
+
+# manager-file.py: generate .manager files and TpCMParamSpec arrays from the
+# same data (should be suitable for all connection managers that don't have
+# plugins)
+#
+# The master copy of this program is in the telepathy-glib repository -
+# please make any changes there.
+#
+# Copyright (c) Collabora Ltd. <http://www.collabora.co.uk/>
+#
+# 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 St, Fifth Floor, Boston, MA  02110-1301  USA
+
+import re
+import sys
+
+_NOT_C_STR = re.compile(r'[^A-Za-z0-9_-]')
+
+def c_string(x):
+    # whitelist-based brute force and ignorance - escape nearly all punctuation
+    return '"' + _NOT_C_STR.sub(lambda c: r'\x%02x' % ord(c), x) + '"'
+
+def desktop_string(x):
+    return x.replace(' ', r'\s').replace('\n', r'\n').replace('\r', r'\r').replace('\t', r'\t')
+
+supported = list('sbuiqn')
+
+fdefaultencoders = {
+        's': desktop_string,
+        'b': (lambda b: b and '1' or '0'),
+        'u': (lambda n: '%u' % n),
+        'i': (lambda n: '%d' % n),
+        'q': (lambda n: '%u' % n),
+        'n': (lambda n: '%d' % n),
+        }
+for x in supported: assert x in fdefaultencoders
+
+gtypes = {
+        's': 'G_TYPE_STRING',
+        'b': 'G_TYPE_BOOLEAN',
+        'u': 'G_TYPE_UINT',
+        'i': 'G_TYPE_INT',
+        'q': 'G_TYPE_UINT',
+        'n': 'G_TYPE_INT',
+}
+for x in supported: assert x in gtypes
+
+gdefaultencoders = {
+        's': c_string,
+        'b': (lambda b: b and 'GINT_TO_POINTER (TRUE)' or 'GINT_TO_POINTER (FALSE)'),
+        'u': (lambda n: 'GUINT_TO_POINTER (%u)' % n),
+        'i': (lambda n: 'GINT_TO_POINTER (%d)' % n),
+        'q': (lambda n: 'GUINT_TO_POINTER (%u)' % n),
+        'n': (lambda n: 'GINT_TO_POINTER (%d)' % n),
+        }
+for x in supported: assert x in gdefaultencoders
+
+gdefaultdefaults = {
+        's': 'NULL',
+        'b': 'GINT_TO_POINTER (FALSE)',
+        'u': 'GUINT_TO_POINTER (0)',
+        'i': 'GINT_TO_POINTER (0)',
+        'q': 'GUINT_TO_POINTER (0)',
+        'n': 'GINT_TO_POINTER (0)',
+        }
+for x in supported: assert x in gdefaultdefaults
+
+gflags = {
+        'has-default': 'TP_CONN_MGR_PARAM_FLAG_HAS_DEFAULT',
+        'register': 'TP_CONN_MGR_PARAM_FLAG_REGISTER',
+        'required': 'TP_CONN_MGR_PARAM_FLAG_REQUIRED',
+        'secret': 'TP_CONN_MGR_PARAM_FLAG_SECRET',
+        'dbus-property': 'TP_CONN_MGR_PARAM_FLAG_DBUS_PROPERTY',
+}
+
+def write_manager(f, manager, protos):
+    # pointless backwards compat section
+    print >> f, '[ConnectionManager]'
+    print >> f, 'BusName=org.freedesktop.Telepathy.ConnectionManager.' + manager
+    print >> f, 'ObjectPath=/org/freedesktop/Telepathy/ConnectionManager/' + manager
+
+    # protocols
+    for proto, params in protos.iteritems():
+        print >> f
+        print >> f, '[Protocol %s]' % proto
+
+        defaults = {}
+
+        for param, info in params.iteritems():
+            dtype = info['dtype']
+            flags = info.get('flags', '').split()
+            struct_field = info.get('struct_field', param.replace('-', '_'))
+            filter = info.get('filter', 'NULL')
+            filter_data = info.get('filter_data', 'NULL')
+            setter_data = 'NULL'
+
+            if 'default' in info:
+                default = fdefaultencoders[dtype](info['default'])
+                defaults[param] = default
+
+            if flags:
+                flags = ' ' + ' '.join(flags)
+            else:
+                flags = ''
+
+            print >> f, 'param-%s=%s%s' % (param, desktop_string(dtype), flags)
+
+        for param, default in defaults.iteritems():
+            print >> f, 'default-%s=%s' % (param, default)
+
+def write_c_params(f, manager, proto, struct, params):
+    print >> f, "static const TpCMParamSpec %s_%s_params[] = {" % (manager, proto)
+
+    for param, info in params.iteritems():
+        dtype = info['dtype']
+        flags = info.get('flags', '').split()
+        struct_field = info.get('struct_field', param.replace('-', '_'))
+        filter = info.get('filter', 'NULL')
+        filter_data = info.get('filter_data', 'NULL')
+        setter_data = 'NULL'
+
+        if 'default' in info:
+            default = gdefaultencoders[dtype](info['default'])
+        else:
+            default = gdefaultdefaults[dtype]
+
+        if flags:
+            flags = ' | '.join([gflags[flag] for flag in flags])
+        else:
+            flags = '0'
+
+        if struct is None or struct_field is None:
+            struct_offset = '0'
+        else:
+            struct_offset = 'G_STRUCT_OFFSET (%s, %s)' % (struct, struct_field)
+
+        print >> f, ('''  { %s, %s, %s,
+    %s,
+    %s, /* default */
+    %s, /* struct offset */
+    %s, /* filter */
+    %s, /* filter data */
+    %s /* setter data */ },''' %
+                (c_string(param), c_string(dtype), gtypes[dtype], flags,
+                    default, struct_offset, filter, filter_data, setter_data))
+
+    print >> f, "  { NULL }"
+    print >> f, "};"
+
+if __name__ == '__main__':
+    environment = {}
+    execfile(sys.argv[1], environment)
+
+    f = open('%s/%s.manager' % (sys.argv[2], environment['MANAGER']), 'w')
+    write_manager(f, environment['MANAGER'], environment['PARAMS'])
+    f.close()
+
+    f = open('%s/param-spec-struct.h' % sys.argv[2], 'w')
+    for protocol in environment['PARAMS']:
+        write_c_params(f, environment['MANAGER'], protocol,
+                environment['STRUCTS'][protocol],
+                environment['PARAMS'][protocol])
+    f.close()



[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]