[folks/648811-dummy-backend-rebase1] dummy: Add a dummy backend



commit 86591e95c7d7fdc281da56c813eac292f224c12f
Author: Philip Withnall <philip withnall collabora co uk>
Date:   Mon Oct 28 09:32:09 2013 +0000

    dummy: Add a dummy backend

 backends/Makefile.am                             |    2 +
 backends/dummy/Makefile.am                       |   40 +
 backends/dummy/backend.mk                        |    1 +
 backends/dummy/dummy-backend-factory.vala        |   49 +
 backends/dummy/lib/Makefile.am                   |  116 +++
 backends/dummy/lib/dummy-backend.vala            |  383 ++++++++
 backends/dummy/lib/dummy-fat-persona.vala        | 1123 ++++++++++++++++++++++
 backends/dummy/lib/dummy-persona-store.vala      |  925 ++++++++++++++++++
 backends/dummy/lib/dummy-persona.vala            |  239 +++++
 backends/dummy/lib/folks-dummy-uninstalled.pc.in |   12 +
 backends/dummy/lib/folks-dummy.deps              |    4 +
 backends/dummy/lib/folks-dummy.pc.in             |   15 +
 configure.ac                                     |    9 +
 tests/Makefile.am                                |    2 +
 tests/dummy/Makefile.am                          |   76 ++
 tests/dummy/add-persona.vala                     |  481 +++++++++
 tests/dummy/individual-retrieval.vala            |  265 +++++
 tests/folks/Makefile.am                          |   10 +
 tests/lib/Makefile.am                            |    2 +
 tests/lib/dummy/Makefile.am                      |   59 ++
 tests/lib/dummy/test-case.vala                   |   97 ++
 tests/lib/test-case.vala                         |    1 +
 tests/lib/test-utils.vala                        |   70 ++
 23 files changed, 3981 insertions(+), 0 deletions(-)
---
diff --git a/backends/Makefile.am b/backends/Makefile.am
index 0da4773..5413767 100644
--- a/backends/Makefile.am
+++ b/backends/Makefile.am
@@ -1,4 +1,5 @@
 SUBDIRS = \
+       dummy \
        key-file \
        $(NULL)
 
@@ -29,6 +30,7 @@ DIST_SUBDIRS = \
        ofono \
        telepathy \
        tracker \
+       dummy \
        $(NULL)
 
 EXTRA_DIST = backend.mk
diff --git a/backends/dummy/Makefile.am b/backends/dummy/Makefile.am
new file mode 100644
index 0000000..e3fbd1d
--- /dev/null
+++ b/backends/dummy/Makefile.am
@@ -0,0 +1,40 @@
+SUBDIRS = lib
+
+BACKEND_NAME = "dummy"
+
+backenddir = $(BACKEND_DIR)/dummy
+backend_LTLIBRARIES = dummy.la
+
+dummy_la_VALAFLAGS = \
+       $(backend_valaflags) \
+       --vapidir=$(top_builddir)/backends/dummy/lib \
+       --pkg folks-dummy \
+       $(NULL)
+
+dummy_la_SOURCES = \
+       $(backend_sources) \
+       dummy-backend-factory.vala \
+       $(NULL)
+
+dummy_la_CPPFLAGS = \
+       $(backend_cppflags) \
+       -I$(top_srcdir)/backends/dummy/lib \
+       -I$(top_srcdir)/backends/dummy/lib/folks \
+       $(NULL)
+
+dummy_la_CFLAGS = \
+       $(backend_cflags) \
+       $(NULL)
+
+dummy_la_LIBADD = \
+       $(backend_libadd) \
+       $(top_builddir)/backends/dummy/lib/libfolks-dummy.la \
+       $(NULL)
+
+dummy_la_LDFLAGS = \
+       -module -avoid-version \
+       $(backend_ldflags) \
+       $(NULL)
+
+-include $(top_srcdir)/backends/backend.mk
+-include $(top_srcdir)/git.mk
diff --git a/backends/dummy/backend.mk b/backends/dummy/backend.mk
new file mode 100644
index 0000000..df1091c
--- /dev/null
+++ b/backends/dummy/backend.mk
@@ -0,0 +1 @@
+BACKEND_NAME = dummy
diff --git a/backends/dummy/dummy-backend-factory.vala b/backends/dummy/dummy-backend-factory.vala
new file mode 100644
index 0000000..441c281
--- /dev/null
+++ b/backends/dummy/dummy-backend-factory.vala
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2009 Zeeshan Ali (Khattak) <zeeshanak gnome org>.
+ * Copyright (C) 2009 Nokia Corporation.
+ * Copyright (C) 2011 Collabora Ltd.
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ *
+ * Authors: Zeeshan Ali (Khattak) <zeeshanak gnome org>
+ *          Travis Reitter <travis reitter collabora co uk>
+ *          Marco Barisione <marco barisione collabora co uk>
+ *          Raul Gutierrez Segales <raul gutierrez segales collabora co uk>
+ *
+ * This file was originally part of Rygel.
+ */
+
+using Folks;
+
+/**
+ * The dummy backend module entry point.
+ *
+ * @backend_store a store to add the dummy backends to
+ * @since UNRELEASED
+ */
+public void module_init (BackendStore backend_store)
+{
+  backend_store.add_backend (new Dummyf.Backend ());
+}
+
+/**
+ * The dummy backend module exit point.
+ *
+ * @param backend_store the store to remove the backends from
+ * @since UNRELEASED
+ */
+public void module_finalize (BackendStore backend_store)
+{
+  /* FIXME: No backend_store.remove_backend() API exists. */
+}
diff --git a/backends/dummy/lib/Makefile.am b/backends/dummy/lib/Makefile.am
new file mode 100644
index 0000000..3d54981
--- /dev/null
+++ b/backends/dummy/lib/Makefile.am
@@ -0,0 +1,116 @@
+BACKEND_NAME = "dummy"
+
+AM_CPPFLAGS = \
+       -I$(top_srcdir) \
+       -I$(top_srcdir)/folks \
+       -include $(CONFIG_HEADER) \
+       -include folks/redeclare-internal-api.h \
+       -DPACKAGE_DATADIR=\"$(pkgdatadir)\" \
+       -DBACKEND_NAME=\"$(BACKEND_NAME)\" \
+       -DG_LOG_DOMAIN=\"$(BACKEND_NAME)\" \
+       $(NULL)
+
+VAPIGENFLAGS += \
+       --vapidir=. \
+       --vapidir=$(top_srcdir)/folks \
+       $(NULL)
+
+folks_dummydir = $(libdir)
+folks_dummy_LTLIBRARIES = libfolks-dummy.la
+
+##################################################################
+# Support library
+##################################################################
+
+pkgconfig_in = folks-dummy.pc.in
+pkgconfigdir = $(libdir)/pkgconfig
+pkgconfig_DATA = $(pkgconfig_in:.in=)
+
+libfolks_dummy_la_vala.stamp:
+
+folks_dummy_valasources = \
+       dummy-backend.vala \
+       dummy-fat-persona.vala \
+       dummy-persona.vala \
+       dummy-persona-store.vala \
+       $(NULL)
+
+libfolks_dummy_la_SOURCES = \
+       $(folks_dummy_valasources) \
+       $(NULL)
+
+# XXX: it would be nice to do something like this below:
+#    $(addprefix --pkg ,$(folks_backend_dummy_deps)) \
+# to factor out repetition, but automake's Vala support doesn't like it
+# because it assumes it can simply match every flag in any _VALAFLAGS string.
+libfolks_dummy_la_VALAFLAGS = \
+       $(AM_VALAFLAGS) \
+       $(ERROR_VALAFLAGS) \
+       --vapidir=. \
+       --vapidir=$(top_srcdir)/folks \
+       --pkg folks \
+       --pkg folks-internal \
+       --pkg gee-0.8 \
+       --pkg gio-2.0 \
+       --pkg gobject-2.0 \
+       --includedir folks \
+       --vapi folks-dummy.vapi \
+       -H folks/folks-dummy.h \
+       $(NULL)
+
+libfolks_dummy_la_CFLAGS = \
+       $(AM_CFLAGS) \
+       $(CODE_COVERAGE_CFLAGS) \
+       $(GIO_CFLAGS) \
+       $(GLIB_CFLAGS) \
+       $(GEE_CFLAGS) \
+       $(NULL)
+
+libfolks_dummy_la_LIBADD = \
+       $(AM_LIBADD) \
+       $(top_builddir)/folks/libfolks.la \
+       $(top_builddir)/folks/libfolks-internal.la \
+       $(GIO_LIBS) \
+       $(GLIB_LIBS) \
+       $(GEE_LIBS) \
+       $(NULL)
+
+# The quoting here is unnecessary but harmless, and has the useful side-effect
+# that vim quickfix mode (:make) doesn't interpret the libtool --mode=link
+# command as an error message in a bizarrely named file
+libfolks_dummy_la_LDFLAGS = \
+       $(AM_LDFLAGS) \
+       $(CODE_COVERAGE_LDFLAGS) \
+       -version-info "$(LT_CURRENT)":"$(LT_REVISION)":"$(LT_AGE)" \
+       -export-symbols-regex "^(DUMMY|dummy)_.*|" \
+       $(NULL)
+
+folks_dummy_includedir = $(includedir)/folks
+folks_dummy_include_HEADERS = \
+       folks/folks-dummy.h \
+       $(NULL)
+
+vapidir = $(datadir)/vala/vapi
+dist_vapi_DATA = \
+       folks-dummy.vapi \
+       folks-dummy.deps \
+       $(NULL)
+
+##################################################################
+# General
+##################################################################
+
+MAINTAINERCLEANFILES = \
+       $(folks_dummy_valasources:.vala=.c) \
+       libfolks_dummy_la_vala.stamp \
+       folks/folks-dummy.h \
+       folks-dummy.vapi \
+       $(NULL)
+
+EXTRA_DIST = \
+       $(pkgconfig_in) \
+       $(MAINTAINERCLEANFILES) \
+       $(NULL)
+
+-include ../backend.mk
+-include $(top_srcdir)/git.mk
diff --git a/backends/dummy/lib/dummy-backend.vala b/backends/dummy/lib/dummy-backend.vala
new file mode 100644
index 0000000..d76463c
--- /dev/null
+++ b/backends/dummy/lib/dummy-backend.vala
@@ -0,0 +1,383 @@
+/*
+ * Copyright (C) 2013 Philip Withnall
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ *
+ * Authors:
+ *       Philip Withnall <philip tecnocode co uk>
+ */
+
+using Gee;
+using GLib;
+using Folks;
+
+extern const string BACKEND_NAME;
+
+/**
+ * A backend which allows { link Dummyf.PersonaStore}s and
+ * { link Dummyf.Persona}s to be programmatically created and manipulated, for
+ * the purposes of testing the core of libfolks itself.
+ *
+ * This backend is not meant to be enabled in production use. The methods on
+ * { link Dummyf.Backend} (and other classes) for programmatically manipulating
+ * the backend's state are considered internal to libfolks and are not stable.
+ *
+ * This backend maintains two sets of persona stores: the set of all persona
+ * stores, and the set of enabled persona stores (which must be a subset of the
+ * former). { link Dummyf.Backend.register_persona_stores} adds persona stores
+ * to the set of all stores. Optionally it also enables them, adding them to the
+ * set of enabled stores. The set of persona stores advertised by the backend as
+ * { link Backend.persona_stores} is the set of enabled stores. libfolks may
+ * internally enable or disable stores using
+ * { link Backend.enable_persona_store}, { link Backend.disable_persona_store}
+ * and { link Backend.set_persona_stores}.
+ *
+ * @since UNRELEASED
+ */
+public class Dummyf.Backend : Folks.Backend
+{
+  private bool _is_prepared = false;
+  private bool _prepare_pending = false; /* used for unprepare() too */
+  private bool _is_quiescent = false;
+
+  private HashMap<string, PersonaStore> _all_persona_stores;
+  private HashMap<string, PersonaStore> _enabled_persona_stores;
+  private Map<string, PersonaStore> _enabled_persona_stores_ro;
+
+  /**
+   * { inheritDoc}
+   *
+   * @since UNRELEASED
+   */
+  public Backend ()
+    {
+      Object ();
+    }
+
+  construct
+    {
+      this._all_persona_stores = new HashMap<string, PersonaStore> ();
+      this._enabled_persona_stores = new HashMap<string, PersonaStore> ();
+      this._enabled_persona_stores_ro =
+          this._enabled_persona_stores.read_only_view;
+    }
+
+  /**
+   * Whether this Backend has been prepared.
+   *
+   * See { link Folks.Backend.is_prepared}.
+   *
+   * @since UNRELEASED
+   */
+  public override bool is_prepared
+    {
+      get { return this._is_prepared; }
+    }
+
+  /**
+   * Whether this Backend has reached a quiescent state.
+   *
+   * See { link Folks.Backend.is_quiescent}.
+   *
+   * @since UNRELEASED
+   */
+  public override bool is_quiescent
+    {
+      get { return this._is_quiescent; }
+    }
+
+  /**
+   * { inheritDoc}
+   *
+   * @since UNRELEASED
+   */
+  public override string name { get { return BACKEND_NAME; } }
+
+  /**
+   * { inheritDoc}
+   *
+   * @since UNRELEASED
+   */
+  public override Map<string, PersonaStore> persona_stores
+    {
+      get { return this._enabled_persona_stores_ro; }
+    }
+
+  /**
+   * { inheritDoc}
+   *
+   * @since UNRELEASED
+   */
+  public override void disable_persona_store (Folks.PersonaStore store)
+    {
+      this._disable_persona_store (store);
+    }
+
+  /**
+   * { inheritDoc}
+   *
+   * @since UNRELEASED
+   */
+  public override void enable_persona_store (Folks.PersonaStore store)
+    {
+      this._enable_persona_store ((Dummyf.PersonaStore) store);
+    }
+
+  /**
+   * { inheritDoc}
+   *
+   * @since UNRELEASED
+   */
+  public override void set_persona_stores (Set<string>? store_ids)
+    {
+      /* If the set is empty, load all unloaded stores then return. */
+      if (store_ids == null)
+        {
+          this.freeze_notify ();
+          foreach (var store in this._all_persona_stores.values)
+            {
+              this._enable_persona_store (store);
+            }
+          this.thaw_notify ();
+
+          return;
+        }
+
+      /* First handle adding any missing persona stores. */
+      this.freeze_notify ();
+
+      foreach (var id in store_ids)
+        {
+          if (!this._enabled_persona_stores.has_key (id))
+            {
+              var store = this._all_persona_stores.get (id);
+              if (store != null)
+                {
+                  this._enable_persona_store (store);
+                }
+            }
+        }
+
+      /* Keep persona stores to remove in a separate array so we don't
+       * invalidate the list we are iterating over. */
+      PersonaStore[] stores_to_remove = {};
+
+      foreach (var store in this._enabled_persona_stores.values)
+        {
+          if (!store_ids.contains (store.id))
+            {
+              stores_to_remove += store;
+            }
+        }
+
+      foreach (var store in stores_to_remove)
+        {
+          this._disable_persona_store (store);
+        }
+
+      this.thaw_notify ();
+    }
+
+  private void _enable_persona_store (PersonaStore store)
+    {
+      if (this._enabled_persona_stores.has_key (store.id))
+        {
+          return;
+        }
+      assert (this._all_persona_stores.has_key (store.id));
+
+      store.removed.connect (this._store_removed_cb);
+
+      this._enabled_persona_stores.set (store.id, store);
+
+      this.persona_store_added (store);
+      this.notify_property ("persona-stores");
+    }
+
+  private void _disable_persona_store (Folks.PersonaStore store)
+    {
+      if (!this._enabled_persona_stores.unset (store.id))
+        {
+          return;
+        }
+      assert (this._all_persona_stores.has_key (store.id));
+
+      this.persona_store_removed (store);
+      this.notify_property ("persona-stores");
+
+      store.removed.disconnect (this._store_removed_cb);
+    }
+
+  private void _store_removed_cb (Folks.PersonaStore store)
+    {
+      this._disable_persona_store (store);
+    }
+
+  /**
+   * { inheritDoc}
+   *
+   * @since UNRELEASED
+   */
+  public override async void prepare () throws GLib.Error
+    {
+      Internal.profiling_start ("preparing Dummy.Backend");
+
+      if (this._is_prepared || this._prepare_pending)
+        {
+          return;
+        }
+
+      try
+        {
+          this._prepare_pending = true;
+          this.freeze_notify ();
+
+          this._is_prepared = true;
+          this.notify_property ("is-prepared");
+
+          this._is_quiescent = true;
+          this.notify_property ("is-quiescent");
+        }
+      finally
+        {
+          this.thaw_notify ();
+          this._prepare_pending = false;
+        }
+
+      Internal.profiling_end ("preparing Dummy.Backend");
+    }
+
+  /**
+   * { inheritDoc}
+   *
+   * @since UNRELEASED
+   */
+  public override async void unprepare () throws GLib.Error
+    {
+      if (!this._is_prepared || this._prepare_pending)
+        {
+          return;
+        }
+
+      try
+        {
+          this._prepare_pending = true;
+          this.freeze_notify ();
+
+          foreach (var persona_store in this._enabled_persona_stores.values)
+            {
+              this._disable_persona_store (persona_store);
+            }
+
+          this._is_quiescent = false;
+          this.notify_property ("is-quiescent");
+
+          this._is_prepared = false;
+          this.notify_property ("is-prepared");
+        }
+      finally
+        {
+          this.thaw_notify ();
+          this._prepare_pending = false;
+        }
+    }
+
+
+  /*
+   * All the functions below here are to be used by testing code rather than by
+   * libfolks clients. They form the interface which would normally be between
+   * the Backend and a web service or backing store of some kind.
+   */
+
+
+  /**
+   * Register and enable some { link Dummyf.PersonaStore}s.
+   *
+   * For each of the persona stores in ``stores``, register it with this
+   * backend. If ``enable_stores`` is ``true``, added stores will also be
+   * enabled, emitting { link Backend.persona_store_added} for each
+   * newly-enabled store. After all addition signals are emitted, a change
+   * notification for { link Backend.persona_stores} will be emitted (but only
+   * if at least one addition signal is emitted).
+   *
+   * Persona stores are identified by their { link PersonaStore.id}; if a store
+   * in ``stores`` has the same ID as a store previously registered through this
+   * method, the duplicate will be ignored (so
+   * { link Backend.persona_store_added} won't be emitted for that store).
+   *
+   * Persona stores must be instances of { link Dummyf.PersonaStore} or
+   * subclasses of it, allowing for different persona store implementations to
+   * be tested.
+   *
+   * @param stores set of persona stores to register
+   * @param enable_stores whether to automatically enable the stores
+   * @since UNRELEASED
+   */
+  public void register_persona_stores (Set<PersonaStore> stores,
+      bool enable_stores = true)
+    {
+      this.freeze_notify ();
+
+      foreach (var store in stores)
+        {
+          if (this._all_persona_stores.has_key (store.id))
+            {
+              continue;
+            }
+
+          this._all_persona_stores.set (store.id, store);
+
+          if (enable_stores == true)
+            {
+              this._enable_persona_store (store);
+            }
+        }
+
+      this.thaw_notify ();
+    }
+
+  /**
+   * Disable and unregister some { link Dummyf.PersonaStores}.
+   *
+   * For each of the persona stores in ``stores``, disable it (if it was
+   * enabled) and unregister it from the backend so that it cannot be re-enabled
+   * using { link Backend.enable_persona_store} or
+   * { link Backend.set_persona_stores}.
+   *
+   * { link Backend.persona_store_removed} will be emitted for all persona
+   * stores in ``stores`` which were previously enabled. After all removal
+   * signals are emitted, a change notification for
+   * { link Backend.persona_stores} will be emitted (but only if at least one
+   * removal signal is emitted).
+   *
+   * @since UNRELEASED
+   */
+  public void unregister_persona_stores (Set<PersonaStore> stores)
+    {
+      this.freeze_notify ();
+
+      foreach (var store in stores)
+        {
+          if (!this._all_persona_stores.has_key (store.id))
+            {
+              continue;
+            }
+
+          this._disable_persona_store (store);
+          this._all_persona_stores.unset (store.id);
+        }
+
+      this.thaw_notify ();
+    }
+}
diff --git a/backends/dummy/lib/dummy-fat-persona.vala b/backends/dummy/lib/dummy-fat-persona.vala
new file mode 100644
index 0000000..7cac9ea
--- /dev/null
+++ b/backends/dummy/lib/dummy-fat-persona.vala
@@ -0,0 +1,1123 @@
+/*
+ * Copyright (C) 2013 Philip Withnall
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ *
+ * Authors:
+ *       Philip Withnall <philip tecnocode co uk>
+ *       Travis Reitter <travis reitter collabora co uk>
+ *       Marco Barisione <marco barisione collabora co uk>
+ *       Raul Gutierrez Segales <raul gutierrez segales collabora co uk>
+ */
+
+using Folks;
+using Gee;
+using GLib;
+
+/**
+ * A persona subclass which represents a single EDS contact. TODO
+ *
+ * Each { link Dummy.Persona} instance represents a single EDS { link E.Contact}.
+ * When the contact is modified (either by this folks client, or a different
+ * client), the { link Dummy.Persona} remains the same, but is assigned a new
+ * { link E.Contact}. It then updates its properties from this new contact.
+ *
+ * @since UNRELEASED
+ */
+public class Dummyf.FatPersona : Dummyf.Persona,
+    AntiLinkable,
+    AvatarDetails,
+    BirthdayDetails,
+    EmailDetails,
+    FavouriteDetails,
+    GenderDetails,
+    GroupDetails,
+    ImDetails,
+    LocalIdDetails,
+    NameDetails,
+    NoteDetails,
+    PhoneDetails,
+    RoleDetails,
+    UrlDetails,
+    PostalAddressDetails,
+    WebServiceDetails
+{
+  /**
+   * Create a new persona.
+   *
+   * Create a new persona for the { link PersonaStore} ``store``, representing
+   * the EDS contact given by ``contact``. TODO
+   *
+   * @param store the store which will contain the persona
+   * @param contact_id TODO
+   * @param is_user TODO
+   *
+   * @since UNRELEASED
+   */
+  public FatPersona (PersonaStore store, string contact_id,
+      bool is_user = false, string[] linkable_properties = {})
+    {
+      base (store, contact_id, is_user, linkable_properties);
+    }
+
+  construct
+    {
+      this._local_ids_ro = this._local_ids.read_only_view;
+      this._postal_addresses_ro = this._postal_addresses.read_only_view;
+      this._email_addresses_ro = this._email_addresses.read_only_view;
+      this._phone_numbers_ro = this._phone_numbers.read_only_view;
+      this._notes_ro = this._notes.read_only_view;
+      this._urls_ro = this._urls.read_only_view;
+      this._groups_ro = this._groups.read_only_view;
+      this._roles_ro = this._roles.read_only_view;
+      this._anti_links_ro = this._anti_links.read_only_view;
+    }
+
+  private HashMultiMap<string, WebServiceFieldDetails> _web_service_addresses =
+      new HashMultiMap<string, WebServiceFieldDetails> (
+          null, null,
+          AbstractFieldDetails<string>.hash_static,
+          AbstractFieldDetails<string>.equal_static);
+
+  /**
+   * { inheritDoc}
+   *
+   * @since UNRELEASED
+   */
+  [CCode (notify = false)]
+  public MultiMap<string, WebServiceFieldDetails> web_service_addresses
+    {
+      get { return this._web_service_addresses; }
+      set { this.change_web_service_addresses.begin (value); }
+    }
+
+  /**
+   * { inheritDoc}
+   *
+   * @since UNRELEASED
+   */
+  public async void change_web_service_addresses (
+      MultiMap<string, WebServiceFieldDetails> web_service_addresses)
+          throws PropertyError
+    {
+      yield this.change_property ("web-service-addresses", () =>
+        {
+          this.update_web_service_addresses (web_service_addresses);
+        });
+    }
+
+  private HashSet<string> _local_ids = new HashSet<string> ();
+  private Set<string> _local_ids_ro;
+
+  /**
+   * { inheritDoc}
+   *
+   * @since UNRELEASED
+   */
+  [CCode (notify = false)]
+  public Set<string> local_ids
+    {
+      get
+        {
+          if (this._local_ids.contains (this.iid) == false)
+            {
+              this._local_ids.add (this.iid);
+            }
+          return this._local_ids_ro;
+        }
+      set { this.change_local_ids.begin (value); }
+    }
+
+  /**
+   * { inheritDoc}
+   *
+   * @since UNRELEASED
+   */
+  public async void change_local_ids (Set<string> local_ids)
+      throws PropertyError
+    {
+      yield this.change_property ("local-ids", () =>
+        {
+          this.update_local_ids (local_ids);
+        });
+    }
+
+  private HashSet<PostalAddressFieldDetails> _postal_addresses =
+      new HashSet<PostalAddressFieldDetails> ();
+  private Set<PostalAddressFieldDetails> _postal_addresses_ro;
+
+  /**
+   * { inheritDoc}
+   *
+   * @since UNRELEASED
+   */
+  [CCode (notify = false)]
+  public Set<PostalAddressFieldDetails> postal_addresses
+    {
+      get { return this._postal_addresses_ro; }
+      set { this.change_postal_addresses.begin (value); }
+    }
+
+  /**
+   * { inheritDoc}
+   *
+   * @since UNRELEASED
+   */
+  public async void change_postal_addresses (
+      Set<PostalAddressFieldDetails> postal_addresses) throws PropertyError
+    {
+      yield this.change_property ("postal-addresses", () =>
+        {
+          this.update_postal_addresses (postal_addresses);
+        });
+    }
+
+  private HashSet<PhoneFieldDetails> _phone_numbers =
+      new HashSet<PhoneFieldDetails> ();
+  private Set<PhoneFieldDetails> _phone_numbers_ro;
+
+  /**
+   * { inheritDoc}
+   *
+   * @since UNRELEASED
+   */
+  [CCode (notify = false)]
+  public Set<PhoneFieldDetails> phone_numbers
+    {
+      get { return this._phone_numbers_ro; }
+      set { this.change_phone_numbers.begin (value); }
+    }
+
+  /**
+   * { inheritDoc}
+   *
+   * @since UNRELEASED
+   */
+  public async void change_phone_numbers (
+      Set<PhoneFieldDetails> phone_numbers) throws PropertyError
+    {
+      yield this.change_property ("phone-numbers", () =>
+        {
+          this.update_phone_numbers (phone_numbers);
+        });
+    }
+
+  private HashSet<EmailFieldDetails>? _email_addresses =
+      new HashSet<EmailFieldDetails> (
+          AbstractFieldDetails<string>.hash_static,
+          AbstractFieldDetails<string>.equal_static);
+  private Set<EmailFieldDetails> _email_addresses_ro;
+
+  /**
+   * { inheritDoc}
+   *
+   * @since UNRELEASED
+   */
+  [CCode (notify = false)]
+  public Set<EmailFieldDetails> email_addresses
+    {
+      get { return this._email_addresses_ro; }
+      set { this.change_email_addresses.begin (value); }
+    }
+
+  /**
+   * { inheritDoc}
+   *
+   * @since UNRELEASED
+   */
+  public async void change_email_addresses (
+      Set<EmailFieldDetails> email_addresses) throws PropertyError
+    {
+      yield this.change_property ("email-addresses", () =>
+        {
+          this.update_email_addresses (email_addresses);
+        });
+    }
+
+  private HashSet<NoteFieldDetails> _notes = new HashSet<NoteFieldDetails> ();
+  private Set<NoteFieldDetails> _notes_ro;
+
+  /**
+   * { inheritDoc}
+   *
+   * @since UNRELEASED
+   */
+  [CCode (notify = false)]
+  public Set<NoteFieldDetails> notes
+    {
+      get { return this._notes_ro; }
+      set { this.change_notes.begin (value); }
+    }
+
+  /**
+   * { inheritDoc}
+   *
+   * @since UNRELEASED
+   */
+  public async void change_notes (Set<NoteFieldDetails> notes)
+      throws PropertyError
+    {
+      yield this.change_property ("notes", () =>
+        {
+          this.update_notes (notes);
+        });
+    }
+
+  private LoadableIcon? _avatar = null;
+
+  /**
+   * { inheritDoc}
+   *
+   * @since UNRELEASED
+   */
+  [CCode (notify = false)]
+  public LoadableIcon? avatar
+    {
+      get { return this._avatar; }
+      set { this.change_avatar.begin (value); }
+    }
+
+  /**
+   * { inheritDoc}
+   *
+   * @since UNRELEASED
+   */
+  public async void change_avatar (LoadableIcon? avatar) throws PropertyError
+    {
+      yield this.change_property ("avatar", () =>
+        {
+          this.update_avatar (avatar);
+        });
+    }
+
+  private StructuredName? _structured_name = null;
+
+  /**
+   * { inheritDoc}
+   *
+   * @since UNRELEASED
+   */
+  [CCode (notify = false)]
+  public StructuredName? structured_name
+    {
+      get { return this._structured_name; }
+      set { this.change_structured_name.begin (value); }
+    }
+
+  /**
+   * { inheritDoc}
+   *
+   * @since UNRELEASED
+   */
+  public async void change_structured_name (StructuredName? structured_name)
+      throws PropertyError
+    {
+      yield this.change_property ("structured-name", () =>
+        {
+          this.update_structured_name (structured_name);
+        });
+    }
+
+  private string _full_name = "";
+
+  /**
+   * { inheritDoc}
+   *
+   * @since UNRELEASED
+   */
+  [CCode (notify = false)]
+  public string full_name
+    {
+      get { return this._full_name; }
+      set { this.change_full_name.begin (value); }
+    }
+
+  /**
+   * { inheritDoc}
+   *
+   * @since UNRELEASED
+   */
+  public async void change_full_name (string full_name) throws PropertyError
+    {
+      yield this.change_property ("full-name", () =>
+        {
+          this.update_full_name (full_name);
+        });
+    }
+
+  private string _nickname = "";
+
+  /**
+   * { inheritDoc}
+   *
+   * @since UNRELEASED
+   */
+  [CCode (notify = false)]
+  public string nickname
+    {
+      get { return this._nickname; }
+      set { this.change_nickname.begin (value); }
+    }
+
+  /**
+   * { inheritDoc}
+   *
+   * @since UNRELEASED
+   */
+  public async void change_nickname (string nickname) throws PropertyError
+    {
+      yield this.change_property ("nickname", () =>
+        {
+          this.update_nickname (nickname);
+        });
+    }
+
+  private Gender _gender = Gender.UNSPECIFIED;
+
+  /**
+   * { inheritDoc}
+   *
+   * @since UNRELEASED
+   */
+  [CCode (notify = false)]
+  public Gender gender
+    {
+      get { return this._gender; }
+      set { this.change_gender.begin (value); }
+    }
+
+  /**
+   * { inheritDoc}
+   *
+   * @since UNRELEASED
+   */
+  public async void change_gender (Gender gender) throws PropertyError
+    {
+      yield this.change_property ("gender", () =>
+        {
+          this.update_gender (gender);
+        });
+    }
+
+  private HashSet<UrlFieldDetails> _urls = new HashSet<UrlFieldDetails> ();
+  private Set<UrlFieldDetails> _urls_ro;
+  /**
+   * { inheritDoc}
+   *
+   * @since UNRELEASED
+   */
+  [CCode (notify = false)]
+  public Set<UrlFieldDetails> urls
+    {
+      get { return this._urls_ro; }
+      set { this.change_urls.begin (value); }
+    }
+
+  /**
+   * { inheritDoc}
+   *
+   * @since UNRELEASED
+   */
+  public async void change_urls (Set<UrlFieldDetails> urls) throws PropertyError
+    {
+      yield this.change_property ("urls", () =>
+        {
+          this.update_urls (urls);
+        });
+    }
+
+  private HashMultiMap<string, ImFieldDetails> _im_addresses =
+      new HashMultiMap<string, ImFieldDetails> (null, null,
+          AbstractFieldDetails<string>.hash_static,
+          AbstractFieldDetails<string>.equal_static);
+
+  /**
+   * { inheritDoc}
+   *
+   * @since UNRELEASED
+   */
+  [CCode (notify = false)]
+  public MultiMap<string, ImFieldDetails> im_addresses
+    {
+      get { return this._im_addresses; }
+      set { this.change_im_addresses.begin (value); }
+    }
+
+  /**
+   * { inheritDoc}
+   *
+   * @since UNRELEASED
+   */
+  public async void change_im_addresses (
+      MultiMap<string, ImFieldDetails> im_addresses) throws PropertyError
+    {
+      yield this.change_property ("im-addresses", () =>
+        {
+          this.update_im_addresses (im_addresses);
+        });
+    }
+
+  private HashSet<string> _groups = new HashSet<string> ();
+  private Set<string> _groups_ro;
+
+  /**
+   * { inheritDoc}
+   *
+   * @since UNRELEASED
+   */
+  [CCode (notify = false)]
+  public Set<string> groups
+    {
+      get { return this._groups_ro; }
+      set { this.change_groups.begin (value); }
+    }
+
+  /**
+   * { inheritDoc}
+   *
+   * @since UNRELEASED
+   */
+  public async void change_group (string group, bool is_member)
+      throws GLib.Error
+    {
+      /* Nothing to do? */
+      if ((is_member == true && this._groups.contains (group) == true) ||
+          (is_member == false && this._groups.contains (group) == false))
+        {
+          return;
+        }
+
+      /* Replace the current set of groups with a modified one. */
+      var new_groups = new HashSet<string> ();
+      foreach (var category_name in this._groups)
+        {
+          new_groups.add (category_name);
+        }
+
+      if (is_member == false)
+        {
+          new_groups.remove (group);
+        }
+      else
+        {
+          new_groups.add (group);
+        }
+
+      yield this.change_groups (new_groups);
+    }
+
+  /**
+   * { inheritDoc}
+   *
+   * @since UNRELEASED
+   */
+  public async void change_groups (Set<string> groups) throws PropertyError
+    {
+      yield this.change_property ("groups", () =>
+        {
+          this.update_groups (groups);
+        });
+    }
+
+  private string? _calendar_event_id = null;
+
+  /**
+   * { inheritDoc}
+   *
+   * @since UNRELEASED
+   */
+  [CCode (notify = false)]
+  public string? calendar_event_id
+    {
+      get { return this._calendar_event_id; }
+      set { this.change_calendar_event_id.begin (value); }
+    }
+
+  /**
+   * { inheritDoc}
+   *
+   * @since UNRELEASED
+   */
+  public async void change_calendar_event_id (string? calendar_event_id)
+      throws PropertyError
+    {
+      yield this.change_property ("calendar-event-id", () =>
+        {
+          this.update_calendar_event_id (calendar_event_id);
+        });
+    }
+
+  private DateTime? _birthday = null;
+  /**
+   * { inheritDoc}
+   *
+   * @since UNRELEASED
+   */
+  [CCode (notify = false)]
+  public DateTime? birthday
+    {
+      get { return this._birthday; }
+      set { this.change_birthday.begin (value); }
+    }
+
+  /**
+   * { inheritDoc}
+   *
+   * @since UNRELEASED
+   */
+  public async void change_birthday (DateTime? bday)
+      throws PropertyError
+    {
+      yield this.change_property ("birthday", () =>
+        {
+          this.update_birthday (birthday);
+        });
+    }
+
+  private HashSet<RoleFieldDetails> _roles = new HashSet<RoleFieldDetails> ();
+  private Set<RoleFieldDetails> _roles_ro;
+
+  /**
+   * { inheritDoc}
+   *
+   * @since UNRELEASED
+   */
+  [CCode (notify = false)]
+  public Set<RoleFieldDetails> roles
+    {
+      get { return this._roles_ro; }
+      set { this.change_roles.begin (value); }
+    }
+
+  /**
+   * { inheritDoc}
+   *
+   * @since UNRELEASED
+   */
+  public async void change_roles (Set<RoleFieldDetails> roles)
+      throws PropertyError
+    {
+      yield this.change_property ("roles", () =>
+        {
+          this.update_roles (roles);
+        });
+    }
+
+  private bool _is_favourite = false;
+
+  /**
+   * Whether this contact is a user-defined favourite.
+   *
+   * @since UNRELEASED
+   */
+  [CCode (notify = false)]
+  public bool is_favourite
+    {
+      get { return this._is_favourite; }
+      set { this.change_is_favourite.begin (value); }
+    }
+
+  /**
+   * { inheritDoc}
+   *
+   * @since UNRELEASED
+   */
+  public async void change_is_favourite (bool is_favourite) throws PropertyError
+    {
+      yield this.change_property ("is-favourite", () =>
+        {
+          this.update_is_favourite (is_favourite);
+        });
+    }
+
+  private HashSet<string> _anti_links = new HashSet<string> ();
+  private Set<string> _anti_links_ro;
+
+  /**
+   * { inheritDoc}
+   *
+   * @since UNRELEASED
+   */
+  [CCode (notify = false)]
+  public Set<string> anti_links
+    {
+      get { return this._anti_links_ro; }
+      set { this.change_anti_links.begin (value); }
+    }
+
+  /**
+   * { inheritDoc}
+   *
+   * @since UNRELEASED
+   */
+  public async void change_anti_links (Set<string> anti_links)
+      throws PropertyError
+    {
+      yield this.change_property ("anti-links", () =>
+        {
+          this.update_anti_links (anti_links);
+        });
+    }
+
+
+  /*
+   * All the functions below here are to be used by testing code rather than by
+   * libfolks clients. They form the interface which would normally be between
+   * the Persona and a web service or backing store of some kind.
+   */
+
+
+  private HashSet<T> _dup_to_hash_set<T> (Set<T> input_set)
+    {
+      var output_set = new HashSet<T> ();
+      output_set.add_all (input_set);
+      return output_set;
+    }
+
+  private HashMultiMap<S, T> _dup_to_hash_multi_map<S, T> (
+      MultiMap<S, T> input_multi_map)
+    {
+      var output_multi_map = new HashMultiMap<S, T> ();
+
+      foreach (var k in input_multi_map.get_keys ())
+        {
+          var values = input_multi_map.get (k);
+          foreach (var v in values)
+            {
+              output_multi_map.set (k, v);
+            }
+        }
+
+      return output_multi_map;
+    }
+
+  /**
+   * Update persona's gender.
+   *
+   * This simulates a backing-store-side update of the persona's
+   * { link GenderDetails.gender} property. It is intended to be used for
+   * testing code which consumes this property. If the property value changes,
+   * this results in a property change notification on the persona.
+   *
+   * @param gender persona's new gender
+   * @since UNRELEASED
+   */
+  public void update_gender (Gender gender)
+    {
+      if (this._gender != gender)
+        {
+          this._gender = gender;
+          this.notify_property ("gender");
+        }
+    }
+
+  /**
+   * Update persona's birthday calendar event ID.
+   *
+   * This simulates a backing-store-side update of the persona's
+   * { link BirthdayDetails.calendar_event_id} property. It is intended to be
+   * used for testing code which consumes this property. If the property value
+   * changes, this results in a property change notification on the persona.
+   *
+   * @param calendar_event_id persona's new birthday calendar event ID
+   * @since UNRELEASED
+   */
+  public void update_calendar_event_id (string? calendar_event_id)
+    {
+      if (calendar_event_id != this._calendar_event_id)
+        {
+          this._calendar_event_id = calendar_event_id;
+          this.notify_property ("calendar-event-id");
+        }
+    }
+
+  /**
+   * Update persona's birthday.
+   *
+   * This simulates a backing-store-side update of the persona's
+   * { link BirthdayDetails.birthday} property. It is intended to be used for
+   * testing code which consumes this property. If the property value changes,
+   * this results in a property change notification on the persona.
+   *
+   * @param birthday persona's new birthday
+   * @since UNRELEASED
+   */
+  public void update_birthday (DateTime? birthday)
+    {
+      if ((this._birthday == null) != (birthday == null) ||
+          (this._birthday != null && birthday != null &&
+              !((!) this._birthday).equal ((!) birthday)))
+        {
+          this._birthday = birthday;
+          this.notify_property ("birthday");
+        }
+    }
+
+  /**
+   * Update persona's roles.
+   *
+   * This simulates a backing-store-side update of the persona's
+   * { link RoleDetails.roles} property. It is intended to be used for
+   * testing code which consumes this property. If the property value changes,
+   * this results in a property change notification on the persona.
+   *
+   * @param roles persona's new roles
+   * @since UNRELEASED
+   */
+  public void update_roles (Set<RoleFieldDetails> roles)
+    {
+      if (!Folks.Internal.equal_sets<RoleFieldDetails> (roles, this._roles))
+        {
+          this._roles = this._dup_to_hash_set<RoleFieldDetails> (roles);
+          this._roles_ro = this._roles.read_only_view;
+          this.notify_property ("roles");
+        }
+    }
+
+  /**
+   * Update persona's groups.
+   *
+   * This simulates a backing-store-side update of the persona's
+   * { link GroupDetails.groups} property. It is intended to be used for
+   * testing code which consumes this property. If the property value changes,
+   * this results in a property change notification on the persona.
+   *
+   * @param groups persona's new groups
+   * @since UNRELEASED
+   */
+  public void update_groups (Set<string> groups)
+    {
+      if (!Folks.Internal.equal_sets<string> (groups, this._groups))
+        {
+          this._groups = this._dup_to_hash_set<string> (groups);
+          this._groups_ro = this._groups.read_only_view;
+          this.notify_property ("groups");
+        }
+    }
+
+  /**
+   * Update persona's web service addresses.
+   *
+   * This simulates a backing-store-side update of the persona's
+   * { link WebServiceDetails.web_service_addresses} property. It is intended to
+   * be used for testing code which consumes this property. If the property
+   * value changes, this results in a property change notification on the
+   * persona.
+   *
+   * @param web_service_addresses persona's new web service addresses
+   * @since UNRELEASED
+   */
+  public void update_web_service_addresses (
+      MultiMap<string, WebServiceFieldDetails> web_service_addresses)
+    {
+      if (!Utils.multi_map_str_afd_equal (web_service_addresses,
+              this._web_service_addresses))
+        {
+          this._web_service_addresses =
+              this._dup_to_hash_multi_map<string, WebServiceFieldDetails> (
+                  web_service_addresses);
+          this.notify_property ("web-service-addresses");
+        }
+    }
+
+  /**
+   * Update persona's e-mail addresses.
+   *
+   * This simulates a backing-store-side update of the persona's
+   * { link EmailDetails.email_addresses} property. It is intended to be used
+   * for testing code which consumes this property. If the property value
+   * changes, this results in a property change notification on the persona.
+   *
+   * @param email_addresses persona's new e-mail addresses
+   * @since UNRELEASED
+   */
+  public void update_email_addresses (Set<EmailFieldDetails> email_addresses)
+    {
+      if (!Folks.Internal.equal_sets<EmailFieldDetails> (email_addresses,
+               this._email_addresses))
+        {
+          this._email_addresses =
+              this._dup_to_hash_set<EmailFieldDetails> (email_addresses);
+          this._email_addresses_ro = this._email_addresses.read_only_view;
+          this.notify_property ("email-addresses");
+       }
+    }
+
+  /**
+   * Update persona's notes.
+   *
+   * This simulates a backing-store-side update of the persona's
+   * { link NoteDetails.notes} property. It is intended to be used for
+   * testing code which consumes this property. If the property value changes,
+   * this results in a property change notification on the persona.
+   *
+   * @param notes persona's new notes
+   * @since UNRELEASED
+   */
+  public void update_notes (Set<NoteFieldDetails> notes)
+    {
+      if (!Folks.Internal.equal_sets<NoteFieldDetails> (notes, this._notes))
+        {
+          this._notes = this._dup_to_hash_set<NoteFieldDetails> (notes);
+          this._notes_ro = this._notes.read_only_view;
+          this.notify_property ("notes");
+        }
+    }
+
+  /**
+   * Update persona's full name.
+   *
+   * This simulates a backing-store-side update of the persona's
+   * { link NameDetails.full_name} property. It is intended to be used for
+   * testing code which consumes this property. If the property value changes,
+   * this results in a property change notification on the persona.
+   *
+   * @param full_name persona's new full name
+   * @since UNRELEASED
+   */
+  public void update_full_name (string full_name)
+    {
+      if (this._full_name != full_name)
+        {
+          this._full_name = full_name;
+          this.notify_property ("full-name");
+        }
+    }
+
+  /**
+   * Update persona's nickname.
+   *
+   * This simulates a backing-store-side update of the persona's
+   * { link NameDetails.nickname} property. It is intended to be used for
+   * testing code which consumes this property. If the property value changes,
+   * this results in a property change notification on the persona.
+   *
+   * @param nickname persona's new nickname
+   * @since UNRELEASED
+   */
+  public void update_nickname (string nickname)
+    {
+      if (this._nickname != nickname)
+        {
+          this._nickname = nickname;
+          this.notify_property ("nickname");
+        }
+    }
+
+  /**
+   * Update persona's structured name.
+   *
+   * This simulates a backing-store-side update of the persona's
+   * { link NameDetails.structured_name} property. It is intended to be used for
+   * testing code which consumes this property. If the property value changes,
+   * this results in a property change notification on the persona.
+   *
+   * @param structured_name persona's new structured name
+   * @since UNRELEASED
+   */
+  public void update_structured_name (StructuredName? structured_name)
+    {
+      if (structured_name != null && !((!) structured_name).is_empty ())
+        {
+          this._structured_name = (!) structured_name;
+          this.notify_property ("structured-name");
+        }
+      else if (this._structured_name != null)
+        {
+          this._structured_name = null;
+          this.notify_property ("structured-name");
+        }
+    }
+
+  /**
+   * Update persona's avatar.
+   *
+   * This simulates a backing-store-side update of the persona's
+   * { link AvatarDetails.avatar} property. It is intended to be used for
+   * testing code which consumes this property. If the property value changes,
+   * this results in a property change notification on the persona.
+   *
+   * @param avatar persona's new avatar
+   * @since UNRELEASED
+   */
+  public void update_avatar (LoadableIcon? avatar)
+    {
+      if ((this._avatar == null) != (avatar == null) ||
+          (this._avatar != null && avatar != null &&
+              !((!) this._avatar).equal ((!) avatar)))
+        {
+          this._avatar = avatar;
+          this.notify_property ("avatar");
+        }
+    }
+
+  /**
+   * Update persona's URIs.
+   *
+   * This simulates a backing-store-side update of the persona's
+   * { link UrlDetails.urls} property. It is intended to be used for
+   * testing code which consumes this property. If the property value changes,
+   * this results in a property change notification on the persona.
+   *
+   * @param urls persona's new URIs
+   * @since UNRELEASED
+   */
+  public void update_urls (Set<UrlFieldDetails> urls)
+    {
+      if (!Utils.set_afd_equal (urls, this._urls))
+        {
+          this._urls = this._dup_to_hash_set<UrlFieldDetails> (urls);
+          this._urls_ro = this._urls.read_only_view;
+          this.notify_property ("urls");
+        }
+    }
+
+  /**
+   * Update persona's IM addresses.
+   *
+   * This simulates a backing-store-side update of the persona's
+   * { link ImDetails.im_addresses} property. It is intended to be used for
+   * testing code which consumes this property. If the property value changes,
+   * this results in a property change notification on the persona.
+   *
+   * @param im_addresses persona's new IM addresses
+   * @since UNRELEASED
+   */
+  public void update_im_addresses (
+      MultiMap<string, ImFieldDetails> im_addresses)
+    {
+      if (!Utils.multi_map_str_afd_equal (im_addresses,
+              this._im_addresses))
+        {
+          this._im_addresses =
+              this._dup_to_hash_multi_map<string, ImFieldDetails> (
+                  im_addresses);
+          this.notify_property ("im-addresses");
+        }
+    }
+
+  /**
+   * Update persona's phone numbers.
+   *
+   * This simulates a backing-store-side update of the persona's
+   * { link PhoneDetails.phone_numbers} property. It is intended to be used for
+   * testing code which consumes this property. If the property value changes,
+   * this results in a property change notification on the persona.
+   *
+   * @param phone_numbers persona's new phone numbers
+   * @since UNRELEASED
+   */
+  public void update_phone_numbers (Set<PhoneFieldDetails> phone_numbers)
+    {
+      if (!Folks.Internal.equal_sets<PhoneFieldDetails> (phone_numbers,
+              this._phone_numbers))
+        {
+          this._phone_numbers =
+              this._dup_to_hash_set<PhoneFieldDetails> (phone_numbers);
+          this._phone_numbers_ro = this._phone_numbers.read_only_view;
+          this.notify_property ("phone-numbers");
+        }
+   }
+
+  /**
+   * Update persona's postal addresses.
+   *
+   * This simulates a backing-store-side update of the persona's
+   * { link PostalAddressDetails.postal_addresses} property. It is intended to
+   * be used for testing code which consumes this property. If the property
+   * value changes, this results in a property change notification on the
+   * persona.
+   *
+   * @param postal_addresses persona's new postal addresses
+   * @since UNRELEASED
+   */
+  public void update_postal_addresses (
+      Set<PostalAddressFieldDetails> postal_addresses)
+    {
+      if (!Folks.Internal.equal_sets<PostalAddressFieldDetails> (
+              postal_addresses, this._postal_addresses))
+        {
+          this._postal_addresses =
+              this._dup_to_hash_set<PostalAddressFieldDetails> (
+                  postal_addresses);
+          this._postal_addresses_ro = this._postal_addresses.read_only_view;
+          this.notify_property ("postal-addresses");
+        }
+    }
+
+  /**
+   * Update persona's local IDs.
+   *
+   * This simulates a backing-store-side update of the persona's
+   * { link LocalIdDetails.local_ids} property. It is intended to be used for
+   * testing code which consumes this property. If the property value changes,
+   * this results in a property change notification on the persona.
+   *
+   * @param local_ids persona's new local IDs
+   * @since UNRELEASED
+   */
+  public void update_local_ids (Set<string> local_ids)
+    {
+      /* Make sure it includes our local ID. */
+      local_ids.add (this.iid);
+
+      if (!Folks.Internal.equal_sets<string> (local_ids, this.local_ids))
+        {
+          this._local_ids = this._dup_to_hash_set<string> (local_ids);
+          this._local_ids_ro = this._local_ids.read_only_view;
+          this.notify_property ("local-ids");
+        }
+    }
+
+  /**
+   * Update persona's status as a favourite.
+   *
+   * This simulates a backing-store-side update of the persona's
+   * { link FavouriteDetails.is_favourite} property. It is intended to be used
+   * for testing code which consumes this property. If the property value
+   * changes, this results in a property change notification on the persona.
+   *
+   * @param is_favourite persona's new status as a favourite
+   * @since UNRELEASED
+   */
+  public void update_is_favourite (bool is_favourite)
+    {
+      if (is_favourite != this._is_favourite)
+        {
+          this._is_favourite = is_favourite;
+          this.notify_property ("is-favourite");
+        }
+    }
+
+  /**
+   * Update persona's anti-links.
+   *
+   * This simulates a backing-store-side update of the persona's
+   * { link AntiLinkable.anti_links} property. It is intended to be used for
+   * testing code which consumes this property. If the property value changes,
+   * this results in a property change notification on the persona.
+   *
+   * @param anti_links persona's new anti-links
+   * @since UNRELEASED
+   */
+  public void update_anti_links (Set<string> anti_links)
+    {
+      if (!Folks.Internal.equal_sets<string> (anti_links, this._anti_links))
+        {
+          this._anti_links = this._dup_to_hash_set<string> (anti_links);
+          this._anti_links_ro = this._anti_links.read_only_view;
+          this.notify_property ("anti-links");
+        }
+    }
+}
diff --git a/backends/dummy/lib/dummy-persona-store.vala b/backends/dummy/lib/dummy-persona-store.vala
new file mode 100644
index 0000000..0dfc797
--- /dev/null
+++ b/backends/dummy/lib/dummy-persona-store.vala
@@ -0,0 +1,925 @@
+/*
+ * Copyright (C) 2013 Philip Withnall
+ *               2013 Canonical Ltd
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ *
+ * Authors:
+ *       Philip Withnall <philip tecnocode co uk>
+  *      Renato Araujo Oliveira Filho <renato canonical com>
+ */
+
+using Folks;
+using Gee;
+using GLib;
+
+/**
+ * A persona store which allows { link Dummyf.Persona}s to be programmatically
+ * created and manipulated, for the purposes of testing the core of libfolks
+ * itself.
+ *
+ * TODO: Mock functions
+ * TODO: Unstable API
+ *
+ * TODO
+ *
+ * TODO: trust_level and is_user_set_default can be set as normal properties
+ *
+ * @since UNRELEASED
+ */
+public class Dummyf.PersonaStore : Folks.PersonaStore
+{
+  private bool _is_prepared = false;
+  private bool _prepare_pending = false;
+  private bool _is_quiescent = false;
+  private bool _quiescent_on_prepare = false;
+  private int  _contact_id = 0;
+
+  /**
+   * The type of persona store this is.
+   *
+   * See { link Folks.PersonaStore.type_id}.
+   *
+   * @since UNRELEASED
+   */
+  public override string type_id { get { return BACKEND_NAME; } }
+
+  private MaybeBool _can_add_personas = MaybeBool.FALSE;
+
+  /**
+   * Whether this PersonaStore can add { link Folks.Persona}s.
+   *
+   * See { link Folks.PersonaStore.can_add_personas}.
+   *
+   * @since UNRELEASED
+   */
+  public override MaybeBool can_add_personas
+    {
+      get
+        {
+          if (!this._is_prepared)
+            {
+              return MaybeBool.FALSE;
+            }
+
+          return this._can_add_personas;
+        }
+    }
+
+  private MaybeBool _can_alias_personas = MaybeBool.FALSE;
+
+  /**
+   * Whether this PersonaStore can set the alias of { link Folks.Persona}s.
+   *
+   * See { link Folks.PersonaStore.can_alias_personas}.
+   *
+   * @since UNRELEASED
+   */
+  public override MaybeBool can_alias_personas
+    {
+      get
+        {
+          if (!this._is_prepared)
+            {
+              return MaybeBool.FALSE;
+            }
+
+          return this._can_alias_personas;
+        }
+    }
+
+  /**
+   * Whether this PersonaStore can set the groups of { link Folks.Persona}s.
+   *
+   * See { link Folks.PersonaStore.can_group_personas}.
+   *
+   * @since UNRELEASED
+   */
+  public override MaybeBool can_group_personas
+    {
+      get
+        {
+          return ("groups" in this._always_writeable_properties)
+              ? MaybeBool.TRUE : MaybeBool.FALSE;
+        }
+    }
+
+  private MaybeBool _can_remove_personas = MaybeBool.FALSE;
+
+  /**
+   * Whether this PersonaStore can remove { link Folks.Persona}s.
+   *
+   * See { link Folks.PersonaStore.can_remove_personas}.
+   *
+   * @since UNRELEASED
+   */
+  public override MaybeBool can_remove_personas
+    {
+      get
+        {
+          if (!this._is_prepared)
+            {
+              return MaybeBool.FALSE;
+            }
+
+          return this._can_remove_personas;
+        }
+    }
+
+  /**
+   * Whether this PersonaStore has been prepared.
+   *
+   * See { link Folks.PersonaStore.is_prepared}.
+   *
+   * @since UNRELEASED
+   */
+  public override bool is_prepared
+    {
+      get { return this._is_prepared; }
+    }
+
+  private string[] _always_writeable_properties = {};
+  private static string[] _always_writeable_properties_empty = {}; /* oh Vala */
+
+  /**
+   * { inheritDoc}
+   *
+   * @since UNRELEASED
+   */
+  public override string[] always_writeable_properties
+    {
+      get
+        {
+          if (!this._is_prepared)
+            {
+              return PersonaStore._always_writeable_properties_empty;
+            }
+
+          return this._always_writeable_properties;
+        }
+    }
+
+  /*
+   * Whether this PersonaStore has reached a quiescent state.
+   *
+   * See { link Folks.PersonaStore.is_quiescent}.
+   *
+   * @since UNRELEASED
+   */
+  public override bool is_quiescent
+    {
+      get { return this._is_quiescent; }
+    }
+
+  private HashMap<string, Persona> _personas;
+  private Map<string, Persona> _personas_ro;
+  private HashSet<Persona> _pending_personas = null;
+
+  /**
+   * The { link Persona}s exposed by this PersonaStore.
+   *
+   * See { link Folks.PersonaStore.personas}.
+   *
+   * @since UNRELEASED
+   */
+  public override Map<string, Persona> personas
+    {
+      get { return this._personas_ro; }
+    }
+
+  /**
+   * Create a new persona store.
+   *
+   * This store will have no personas to begin with; use
+   * { link Dummyf.PersonaStore.register_personas} to add some, then call
+   * { link Dummyf.PersonaStore.reach_quiescence} to signal the store reaching
+   * quiescence.
+   *
+   * @param id The new store's ID.
+   * @param display_name The new store's display name.
+   * @param always_writeable_properties The set of always writeable properties.
+   *
+   * @since UNRELEASED
+   */
+  public PersonaStore (string id, string display_name,
+      string[] always_writeable_properties)
+    {
+      Object (
+          id: id,
+          display_name: display_name);
+
+      this._always_writeable_properties = always_writeable_properties;
+    }
+
+  construct
+    {
+      this._personas = new HashMap<string, Persona> ();
+      this._personas_ro = this._personas.read_only_view;
+    }
+
+  /**
+   * Type of a mock function for { link PersonaStore.add_persona_from_details}.
+   *
+   * See { link Dummyf.PersonaStore.add_persona_from_details_mock}.
+   *
+   * @param persona the persona being added to the store, as constructed from
+   * the details passed to { link PersonaStore.add_persona_from_details}.
+   * @throws PersonaStoreError to be thrown from
+   * { link PersonaStore.add_persona_from_details}
+   * @since UNRELEASED
+   */
+  public delegate void AddPersonaFromDetailsMock (Persona persona)
+      throws PersonaStoreError;
+
+  /**
+   * Mock function for { link PersonaStore.add_persona_from_details}.
+   *
+   * This function is called whenever this store's
+   * { link PersonaStore.add_persona_from_details} method is called. It allows
+   * the caller to determine whether adding the given persona should fail, by
+   * throwing an error from this mock function. If no error is thrown from this
+   * function, adding the given persona will succeed. This is useful for testing
+   * error handling of calls to { link PersoneStore.add_persona_from_details}.
+   *
+   * If this is ``null``, all calls to
+   * { link PersonaStore.add_persona_from_details} will succeed.
+   *
+   * This mock function may be changed at any time; changes will take effect for
+   * the next call to { link PersonaStore.add_persona_from_details}.
+   *
+   * @since UNRELEASED
+   */
+  public unowned AddPersonaFromDetailsMock? add_persona_from_details_mock
+    {
+      get; set; default = null;
+    }
+
+  /**
+   * Add a new { link Persona} to the PersonaStore.
+   *
+   * Accepted keys for ``details`` are:
+   * - PersonaStore.detail_key (PersonaDetail.AVATAR)
+   * - PersonaStore.detail_key (PersonaDetail.BIRTHDAY)
+   * - PersonaStore.detail_key (PersonaDetail.EMAIL_ADDRESSES)
+   * - PersonaStore.detail_key (PersonaDetail.FULL_NAME)
+   * - PersonaStore.detail_key (PersonaDetail.GENDER)
+   * - PersonaStore.detail_key (PersonaDetail.IM_ADDRESSES)
+   * - PersonaStore.detail_key (PersonaDetail.IS_FAVOURITE)
+   * - PersonaStore.detail_key (PersonaDetail.PHONE_NUMBERS)
+   * - PersonaStore.detail_key (PersonaDetail.POSTAL_ADDRESSES)
+   * - PersonaStore.detail_key (PersonaDetail.ROLES)
+   * - PersonaStore.detail_key (PersonaDetail.STRUCTURED_NAME)
+   * - PersonaStore.detail_key (PersonaDetail.LOCAL_IDS)
+   * - PersonaStore.detail_key (PersonaDetail.WEB_SERVICE_ADDRESSES)
+   * - PersonaStore.detail_key (PersonaDetail.NOTES)
+   * - PersonaStore.detail_key (PersonaDetail.URLS)
+   *
+   * See { link Folks.PersonaStore.add_persona_from_details}.
+   *
+   * @throws Folks.PersonaStoreError.STORE_OFFLINE if the store hasn’t been
+   * prepared
+   * @throws Folks.PersonaStoreError.CREATE_FAILED if creating the persona in
+   * the EDS store failed
+   *
+   * @since UNRELEASED
+   */
+  public override async Folks.Persona? add_persona_from_details (
+      HashTable<string, Value?> details) throws PersonaStoreError
+    {
+      // We have to have called prepare() beforehand.
+      if (!this._is_prepared)
+        {
+          throw new PersonaStoreError.STORE_OFFLINE (
+              "Persona store has not yet been prepared.");
+        }
+
+      /* Allow overriding the class used. */
+      var contact_id = this._contact_id.to_string();
+      this._contact_id++;
+      var uid = Folks.Persona.build_uid (BACKEND_NAME, this.id, contact_id);
+      var iid = this.id + ":" + contact_id;
+
+      var persona = Object.new (this._persona_type,
+          "display-id", contact_id,
+          "uid", uid,
+          "iid", iid,
+          "store", this,
+          "is-user", false,
+          null) as Dummyf.Persona;
+      assert (persona != null);
+      persona.update_writeable_properties (this.always_writeable_properties);
+
+      var iter = HashTableIter<string, Value?> (details);
+      unowned string k;
+      unowned Value? _v;
+
+      while (iter.next (out k, out _v) == true)
+        {
+          if (_v == null)
+            {
+              continue;
+            }
+          unowned Value v = (!) _v;
+
+          if (k == Folks.PersonaStore.detail_key (
+                PersonaDetail.FULL_NAME))
+            {
+              var _persona = persona as NameDetails;
+              if (_persona != null) 
+                {
+                  string? full_name = v.get_string ();
+                  string _full_name = "";
+                  if (full_name != null)
+                    {
+                      _full_name = (!) full_name;
+                    }
+                  _persona.full_name = _full_name;
+                }
+            }
+          else if (k == Folks.PersonaStore.detail_key (
+                PersonaDetail.EMAIL_ADDRESSES))
+            {
+              var _persona = persona as EmailDetails;
+              if (_persona != null)
+                {
+                  Set<EmailFieldDetails> email_addresses =
+                      (Set<EmailFieldDetails>) v.get_object ();
+                  if (email_addresses != null)
+                    {
+                      _persona.email_addresses =  email_addresses;
+                    }             
+                }
+            }
+          else if (k == Folks.PersonaStore.detail_key (PersonaDetail.AVATAR))
+            {
+              var _persona = persona as AvatarDetails;
+              if (_persona != null)
+                {
+                  var avatar = (LoadableIcon?) v.get_object ();
+                  if (avatar != null)
+                    {
+                      _persona.avatar = avatar;
+                    }
+                }
+            }
+          else if (k == Folks.PersonaStore.detail_key (
+                PersonaDetail.IM_ADDRESSES))
+            {
+              var _persona = persona as ImDetails;
+              if (_persona != null)
+                {
+                  MultiMap<string,ImFieldDetails> im_addresses =
+                    (MultiMap<string,ImFieldDetails>) v.get_object ();
+                  if (im_addresses != null)
+                    {
+                      _persona.im_addresses = im_addresses;
+                    }
+                }
+            }
+          else if (k == Folks.PersonaStore.detail_key (
+                PersonaDetail.PHONE_NUMBERS))
+            {
+              var _persona = persona as PhoneDetails;
+              if (_persona != null)
+                {
+                  Set<PhoneFieldDetails> phone_numbers =
+                    (Set<PhoneFieldDetails>) v.get_object ();
+                  if (phone_numbers != null)
+                    {
+                      _persona.phone_numbers = phone_numbers;
+                    }
+                }
+            }
+          else if (k == Folks.PersonaStore.detail_key (
+                PersonaDetail.POSTAL_ADDRESSES))
+            {
+              var _persona = persona as PostalAddressDetails;
+              if (_persona != null)
+                {
+                  Set<PostalAddressFieldDetails> postal_fds =
+                    (Set<PostalAddressFieldDetails>) v.get_object ();
+                  if (postal_fds != null)
+                    {
+                      _persona.postal_addresses = postal_fds;
+                    }
+                }
+            }
+          else if (k == Folks.PersonaStore.detail_key (
+                PersonaDetail.STRUCTURED_NAME))
+            {
+              var _persona = persona as NameDetails;
+              if (_persona != null) 
+                {
+                  StructuredName sname = (StructuredName) v.get_object ();
+                  if (sname != null)
+                    {
+                      _persona.structured_name = sname;
+                    }
+                }
+            }
+          else if (k == Folks.PersonaStore.detail_key (PersonaDetail.LOCAL_IDS))
+            {
+              var _persona = persona as LocalIdDetails;
+              if (_persona != null)
+                {
+                  Set<string> local_ids = (Set<string>) v.get_object ();
+                  if (local_ids != null)
+                    {
+                      _persona.local_ids = local_ids;
+                    }
+                }
+            }
+          else if (k == Folks.PersonaStore.detail_key
+              (PersonaDetail.WEB_SERVICE_ADDRESSES))
+            {
+              var _persona = persona as WebServiceDetails;
+              if (_persona != null)
+                {
+                  HashMultiMap<string, WebServiceFieldDetails>
+                    web_service_addresses = 
+                      (HashMultiMap<string, WebServiceFieldDetails>) v.get_object ();
+                  if (web_service_addresses != null)
+                    {
+                      _persona.web_service_addresses = web_service_addresses;
+                    }
+                }
+            }
+          else if (k == Folks.PersonaStore.detail_key (PersonaDetail.NOTES))
+            {
+              var _persona = persona as NoteDetails;
+              if (_persona != null)
+                {
+                  var notes = (Gee.HashSet<NoteFieldDetails>) v.get_object ();
+                  if (notes != null)
+                    {
+                      _persona.notes = notes;
+                    }
+                }
+            }
+          else if (k == Folks.PersonaStore.detail_key (PersonaDetail.GENDER))
+            {
+              var _persona = persona as GenderDetails;
+              if (_persona != null)
+                {
+                  var gender = (Gender) v.get_enum ();
+                  _persona.gender = gender;
+                }
+            }
+          else if (k == Folks.PersonaStore.detail_key (PersonaDetail.URLS))
+            {
+              var _persona = persona as UrlDetails;
+              if (_persona != null)
+                {
+                  Set<UrlFieldDetails> urls = (Set<UrlFieldDetails>) v.get_object ();
+                  if (urls != null)
+                    {
+                      _persona.urls = urls;
+                    }
+                }
+            }
+          else if (k == Folks.PersonaStore.detail_key (PersonaDetail.BIRTHDAY))
+            {
+              var _persona = persona as BirthdayDetails;
+              if (_persona != null)
+                {
+                  var birthday = (DateTime?) v.get_boxed ();
+                  if (birthday != null)
+                    {
+                      _persona.birthday = birthday;
+                    }
+                }
+            }
+          else if (k == Folks.PersonaStore.detail_key (PersonaDetail.ROLES))
+            {
+              var _persona = persona as RoleDetails;
+              if (_persona != null)
+                {
+                  Set<RoleFieldDetails> roles =
+                      (Set<RoleFieldDetails>) v.get_object ();
+                  if (roles != null)
+                    {
+                      _persona.roles = roles;
+                    }
+                }
+            }
+          else if (k == Folks.PersonaStore.detail_key (
+                  PersonaDetail.IS_FAVOURITE))
+            {
+              var _persona = persona as FavouriteDetails;
+              if (_persona != null)
+                {
+                  bool is_fav = v.get_boolean ();
+                  _persona.is_favourite = is_fav;
+                }
+            }
+          else if (k == Folks.PersonaStore.detail_key (
+                   PersonaDetail.NICKNAME))
+            {
+              var _persona = persona as NameDetails;
+              if (_persona != null)
+                {
+                  string? nickname = v.get_string ();
+                  string _nickname = "";
+                  if (nickname != null)
+                    {
+                      _nickname = (!) nickname;
+                    }
+                  _persona.nickname = _nickname;
+                }
+            }
+        }
+      /* Allow the caller to inject failures into add_persona_from_details()
+       * by providing a mock function which can throw errors as appropriate. */
+      if (this.add_persona_from_details_mock != null)
+        {
+          this.add_persona_from_details_mock (persona);
+        }
+
+      /* No simulated failure: continue adding the persona. */
+      this._personas.set (persona.iid, persona);
+
+      /* Notify of the new persona. */
+      var added_personas = new HashSet<Persona> ();
+      added_personas.add (persona);
+      this._emit_personas_changed (added_personas, null);
+
+      return persona;
+    }
+
+  /**
+   * Type of a mock function for { link PersonaStore.remove_persona}.
+   *
+   * See { link Dummyf.PersonaStore.remove_persona_mock}.
+   *
+   * @param persona the persona being removed from the store
+   * @throws PersonaStoreError to be thrown from
+   * { link PersonaStore.remove_persona}
+   * @since UNRELEASED
+   */
+  public delegate void RemovePersonaMock (Persona persona)
+      throws PersonaStoreError;
+
+  /**
+   * Mock function for { link PersonaStore.remove_persona}.
+   *
+   * This function is called whenever this store's
+   * { link PersonaStore.remove_persona} method is called. It allows
+   * the caller to determine whether removing the given persona should fail, by
+   * throwing an error from this mock function. If no error is thrown from this
+   * function, removing the given persona will succeed. This is useful for
+   * testing error handling of calls to { link PersoneStore.remove_persona}.
+   *
+   * If this is ``null``, all calls to { link PersonaStore.remove_persona} will
+   * succeed.
+   *
+   * This mock function may be changed at any time; changes will take effect for
+   * the next call to { link PersonaStore.remove_persona}.
+   *
+   * @since UNRELEASED
+   */
+  public unowned RemovePersonaMock? remove_persona_mock
+    {
+      get; set; default = null;
+    }
+
+  /**
+   * Remove a { link Persona} from the PersonaStore.
+   *
+   * See { link Folks.PersonaStore.remove_persona}.
+   *
+   * @param persona the persona that should be removed
+   * @throws Folks.PersonaStoreError.STORE_OFFLINE if the store hasn’t been
+   * prepared or has gone offline
+   * @throws Folks.PersonaStoreError.PERMISSION_DENIED if the store denied
+   * permission to delete the contact
+   * @throws Folks.PersonaStoreError.READ_ONLY if the store is read only
+   * @throws Folks.PersonaStoreError.REMOVE_FAILED if any other errors happened
+   * in the store
+   *
+   * @since UNRELEASED
+   */
+  public override async void remove_persona (Folks.Persona persona)
+      throws PersonaStoreError
+      requires (persona is Dummyf.Persona)
+    {
+      // We have to have called prepare() beforehand.
+      if (!this._is_prepared)
+        {
+          throw new PersonaStoreError.STORE_OFFLINE (
+              "Persona store has not yet been prepared.");
+        }
+
+      /* Allow the caller to inject failures into remove_persona()
+       * by providing a mock function which can throw errors as appropriate. */
+      if (this.remove_persona_mock != null)
+        {
+          this.remove_persona_mock ((Dummyf.Persona) persona);
+        }
+
+      Persona? _persona = this._personas.get (persona.iid);
+      if (_persona != null)
+        {
+          this._personas.unset (persona.iid);
+
+          /* Handle the case where a contact is removed before the persona
+           * store has reached quiescence. */
+          if (this._pending_personas != null)
+            {
+              this._pending_personas.remove ((!) _persona);
+            }
+
+          /* Notify of the removal. */
+          var removed_personas = new HashSet<Folks.Persona> ();
+          removed_personas.add ((!) persona);
+          this._emit_personas_changed (null, removed_personas);
+        }
+    }
+
+  /**
+   * Type of a mock function for { link PersonaStore.prepare}.
+   *
+   * See { link Dummyf.PersonaStore.prepare_mock}.
+   *
+   * @throws PersonaStoreError to be thrown from { link PersonaStore.prepare}
+   * @since UNRELEASED
+   */
+  public delegate void PrepareMock () throws PersonaStoreError;
+
+  /**
+   * Mock function for { link PersonaStore.prepare}.
+   *
+   * This function is called whenever this store's
+   * { link PersonaStore.prepare} method is called on an unprepared store. It
+   * allows the caller to determine whether preparing the store should fail, by
+   * throwing an error from this mock function. If no error is thrown from this
+   * function, preparing the store will succeed (and all future calls to
+   * { link PersonaStore.prepare} will return immediately without calling this
+   * mock function). This is useful for testing error handling of calls to
+   * { link PersoneStore.prepare}.
+   *
+   * If this is ``null``, all calls to { link PersonaStore.prepare} will
+   * succeed.
+   *
+   * This mock function may be changed at any time; changes will take effect for
+   * the next call to { link PersonaStore.prepare}.
+   *
+   * @since UNRELEASED
+   */
+  public unowned PrepareMock? prepare_mock
+    {
+      get; set; default = null;
+    }
+
+  /**
+   * Prepare the PersonaStore for use.
+   *
+   * See { link Folks.PersonaStore.prepare}.
+   *
+   * @throws Folks.PersonaStoreError.STORE_OFFLINE if the store is offline
+   * @throws Folks.PersonaStoreError.PERMISSION_DENIED if permission was denied
+   * to open the store
+   * @throws Folks.PersonaStoreError.INVALID_ARGUMENT if any other error
+   * occurred in the store
+   *
+   * @since UNRELEASED
+   */
+  public override async void prepare () throws PersonaStoreError
+    {
+      Internal.profiling_start ("preparing Dummy.PersonaStore (ID: %s)",
+          this.id);
+
+      if (this._is_prepared == true || this._prepare_pending == true)
+        {
+          return;
+        }
+
+      try
+        {
+          this._prepare_pending = true;
+
+          /* Allow the caller to inject failures into prepare() by providing a
+           * mock function which can throw errors as appropriate. */
+          if (this.prepare_mock != null)
+            {
+              this.prepare_mock ();
+            }
+
+          this._is_prepared = true;
+          this.notify_property ("is-prepared");
+
+          /* If reach_quiescence() has been called already, signal
+           * quiescence. */
+          if (this._quiescent_on_prepare == true)
+            {
+              this.reach_quiescence ();
+            }
+        }
+      finally
+        {
+          this._prepare_pending = false;
+        }
+
+      Internal.profiling_end ("preparing Dummy.PersonaStore");
+    }
+
+
+  /*
+   * All the functions below here are to be used by testing code rather than by
+   * libfolks clients. They form the interface which would normally be between
+   * the PersonaStore and a web service or backing store of some kind.
+   */
+
+
+  private Type _persona_type = typeof (Dummyf.Persona);
+
+  /**
+   * Type of programmatically created personas.
+   *
+   * This is the type used to create new personas when
+   * { link PersonaStore.add_persona_from_details} is called. It must be a
+   * subtype of { link Dummyf.Persona}.
+   *
+   * This may be modified at any time, with modifications taking effect for the
+   * next call to { link PersonaStore.add_persona_from_details}.
+   *
+   * @since UNRELEASED
+   */
+  public Type persona_type
+    {
+      get { return this._persona_type; }
+      set
+        {
+          assert (value.is_a (typeof (Dummyf.Persona)));
+          if (this._persona_type != value)
+            {
+              this._persona_type = value;
+              this.notify_property ("persona-type");
+            }
+        }
+    }
+
+  /**
+   * Set capabilities of the persona store.
+   *
+   * This sets the capabilities of the store, as if they were changed on a
+   * backing store somewhere. This is intended to be used for testing code which
+   * depends on the values of { link PersonaStore.can_add_personas},
+   * { link PersonaStore.can_alias_personas} and
+   * { link PersonaStore.can_remove_personas}.
+   *
+   * @param can_add_personas whether the store can handle adding personas
+   * @param can_alias_personas whether the store can handle and update
+   * user-specified persona aliases
+   * @param can_remove_personas whether the store can handle removing personas
+   * @since UNRELEASED
+   */
+  public void set_capabilities (MaybeBool can_add_personas,
+      MaybeBool can_alias_personas, MaybeBool can_remove_personas)
+    {
+      this.freeze_notify ();
+
+      if (can_add_personas != this._can_add_personas)
+        {
+          this._can_add_personas = can_add_personas;
+          this.notify_property ("can-add-personas");
+        }
+
+      if (can_alias_personas != this._can_alias_personas)
+        {
+          this._can_alias_personas = can_alias_personas;
+          this.notify_property ("can-alias-personas");
+        }
+
+      if (can_remove_personas != this._can_remove_personas)
+        {
+          this._can_remove_personas = can_remove_personas;
+          this.notify_property ("can-remove-personas");
+        }
+
+      this.thaw_notify ();
+    }
+
+  /**
+   * TODO
+   *
+   * @param personas TODO
+   * @since UNRELEASED
+   */
+  public void register_personas (Set<Persona> personas)
+    {
+      HashSet<Persona> added_personas;
+
+      /* If the persona store hasn't yet reached quiescence, queue up the
+       * personas and emit a notification about them later.. */
+      if (this._is_quiescent == false)
+        {
+          /* Lazily create pending_personas. */
+          if (this._pending_personas == null)
+            {
+              this._pending_personas = new HashSet<Persona> ();
+            }
+
+          added_personas = this._pending_personas;
+        }
+      else
+        {
+          added_personas = new HashSet<Persona> ();
+        }
+
+      foreach (var persona in personas)
+        {
+          if (!this._personas.has_key (persona.iid))
+            {
+              this._personas.set (persona.iid, persona);
+              added_personas.add (persona);
+            }
+        }
+
+      if (added_personas.size > 0 && this._is_quiescent == true)
+        {
+          this._emit_personas_changed (added_personas, null);
+        }
+    }
+
+  /**
+   * TODO
+   *
+   * @param personas TODO
+   * @since UNRELEASED
+   */
+  public void unregister_personas (Set<Persona> personas)
+    {
+      var removed_personas = new HashSet<Persona> ();
+
+      foreach (var _persona in personas)
+        {
+          Persona? persona = this._personas.get (_persona.iid);
+          if (persona != null)
+            {
+              removed_personas.add ((!) persona);
+              this._personas.unset (((!) persona).iid);
+
+              /* Handle the case where a contact is removed before the persona
+               * store has reached quiescence. */
+              if (this._pending_personas != null)
+                {
+                  this._pending_personas.remove ((!) persona);
+                }
+            }
+        }
+
+       if (removed_personas.size > 0)
+         {
+           this._emit_personas_changed (null, removed_personas);
+         }
+    }
+
+/* TODO: Some method of emitting _emit_personas_changed with no null values */
+
+  /**
+   * TODO
+   *
+   * @since UNRELEASED
+   */
+  public void reach_quiescence ()
+    {
+      /* Can't reach quiescence until prepare() has been called. */
+      if (this._is_prepared == false)
+        {
+          this._quiescent_on_prepare = true;
+          return;
+        }
+
+      /* The initial query is complete, so signal that we've reached
+       * quiescence (even if there was an error). */
+      if (this._is_quiescent == false)
+        {
+          /* Emit a notification about all the personas which were found in the
+           * initial query. They're queued up in _contacts_added_cb() and only
+           * emitted here as _contacts_added_cb() may be called many times
+           * before _contacts_complete_cb() is called. For example, EDS seems to
+           * like emitting contacts in batches of 16 at the moment.
+           * Queueing the personas up and emitting a single notification is a
+           * lot more efficient for the individual aggregator to handle. */
+          if (this._pending_personas != null)
+            {
+              this._emit_personas_changed (this._pending_personas, null);
+              this._pending_personas = null;
+            }
+
+          this._is_quiescent = true;
+          this.notify_property ("is-quiescent");
+        }
+    }
+}
diff --git a/backends/dummy/lib/dummy-persona.vala b/backends/dummy/lib/dummy-persona.vala
new file mode 100644
index 0000000..7d15288
--- /dev/null
+++ b/backends/dummy/lib/dummy-persona.vala
@@ -0,0 +1,239 @@
+/*
+ * Copyright (C) 2013 Philip Withnall
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ *
+ * Authors:
+ *       Philip Withnall <philip tecnocode co uk>
+ */
+
+using Folks;
+using Gee;
+using GLib;
+
+/**
+ * A persona subclass which represents a single contact. TODO
+ *
+ * Each { link Dummy.Persona} instance represents a single EDS { link E.Contact}.
+ * When the contact is modified (either by this folks client, or a different
+ * client), the { link Dummy.Persona} remains the same, but is assigned a new
+ * { link E.Contact}. It then updates its properties from this new contact.
+ *
+ * @since UNRELEASED
+ */
+public class Dummyf.Persona : Folks.Persona
+{
+  private string[] _linkable_properties = new string[0];
+
+  /**
+   * { inheritDoc}
+   *
+   * @since UNRELEASED
+   */
+  public override string[] linkable_properties
+    {
+      get { return this._linkable_properties; }
+    }
+
+  private string[] _writeable_properties = new string[0];
+
+  /**
+   * { inheritDoc}
+   *
+   * @since UNRELEASED
+   */
+  public override string[] writeable_properties
+    {
+      get { return this._writeable_properties; }
+    }
+
+  /**
+   * Create a new persona.
+   *
+   * Create a new persona for the { link PersonaStore} ``store``, representing
+   * the EDS contact given by ``contact``. TODO
+   *
+   * @param store the store which will contain the persona
+   * @param contact_id TODO
+   * @param is_user TODO
+   *
+   * @since UNRELEASED
+   */
+  public Persona (PersonaStore store, string contact_id, bool is_user = false,
+      string[] linkable_properties = {})
+    {
+      var uid = Folks.Persona.build_uid (BACKEND_NAME, store.id, contact_id);
+      var iid = store.id + ":" + contact_id;
+
+      Object (display_id: contact_id,
+              uid: uid,
+              iid: iid,
+              store: store,
+              is_user: is_user);
+
+      this._linkable_properties = linkable_properties;
+      this._writeable_properties = this.store.always_writeable_properties;
+    }
+
+  /**
+   * { inheritDoc}
+   *
+   * @since UNRELEASED
+   */
+  public override void linkable_property_to_links (string prop_name,
+      Folks.Persona.LinkablePropertyCallback callback)
+    {
+      /* TODO */
+      if (prop_name == "im-addresses")
+        {
+          var persona = this as ImDetails;
+          assert (persona != null);
+
+          foreach (var protocol in persona.im_addresses.get_keys ())
+            {
+              var im_fds = persona.im_addresses.get (protocol);
+
+              foreach (var im_fd in im_fds)
+                {
+                  callback (protocol + ":" + im_fd.value);
+                }
+            }
+        }
+      else if (prop_name == "local-ids")
+        {
+          var persona = this as LocalIdDetails;
+          assert (persona != null);
+
+          foreach (var id in persona.local_ids)
+            {
+              callback (id);
+            }
+        }
+      else if (prop_name == "web-service-addresses")
+        {
+          var persona = this as WebServiceDetails;
+          assert (persona != null);
+
+          foreach (var web_service in persona.web_service_addresses.get_keys ())
+            {
+              var web_service_addresses =
+                  persona.web_service_addresses.get (web_service);
+
+              foreach (var ws_fd in web_service_addresses)
+                {
+                  callback (web_service + ":" + ws_fd.value);
+                }
+            }
+        }
+      else if (prop_name == "email-addresses")
+        {
+          var persona = this as EmailDetails;
+          assert (persona != null);
+
+          foreach (var email in persona.email_addresses)
+            {
+              callback (email.value);
+            }
+        }
+      else
+        {
+          /* Chain up */
+          base.linkable_property_to_links (prop_name, callback);
+        }
+    }
+
+
+  /*
+   * All the functions below here are to be used by testing code rather than by
+   * libfolks clients. They form the interface which would normally be between
+   * the Persona and a web service or backing store of some kind.
+   */
+
+
+  /**
+   * TODO
+   *
+   * @since UNRELEASED
+   */
+  public void update_writeable_properties (string[] writeable_properties)
+    {
+      /* TODO: Don't update if there's no change. */
+      var new_length = this.store.always_writeable_properties.length +
+          writeable_properties.length;
+      this._writeable_properties = new string[new_length];
+      int i = 0;
+
+      foreach (var p in this.store.always_writeable_properties)
+        {
+          this._writeable_properties[i++] = p;
+        }
+      foreach (var p in writeable_properties)
+        {
+          this._writeable_properties[i++] = p;
+        }
+
+      this.notify_property ("writeable-properties");
+    }
+
+  /**
+   * TODO (in milliseconds)
+   *
+   * @since UNRELEASED
+   */
+  protected int property_change_delay { get; set; }
+
+  /* TODO */
+  protected delegate void ChangePropertyCallback ();
+
+  /**
+   * TODO
+   *
+   * @param property_name TODO
+   * @param callback TODO
+   * @since UNRELEASED
+   */
+  protected async void change_property (string property_name,
+      ChangePropertyCallback callback)
+    {
+      if (this.property_change_delay < 0)
+        {
+          /* No delay. */
+          callback ();
+        }
+      else if (this.property_change_delay == 0)
+        {
+          /* Idle delay. */
+          Idle.add (() =>
+            {
+              callback ();
+              this.change_property.callback ();
+              return false;
+            });
+
+          yield;
+        }
+      else
+        {
+          /* Timed delay. */
+          Timeout.add (this.property_change_delay, () =>
+            {
+              callback ();
+              this.change_property.callback ();
+              return false;
+            });
+
+          yield;
+        }
+    }
+}
diff --git a/backends/dummy/lib/folks-dummy-uninstalled.pc.in 
b/backends/dummy/lib/folks-dummy-uninstalled.pc.in
new file mode 100644
index 0000000..8438ce5
--- /dev/null
+++ b/backends/dummy/lib/folks-dummy-uninstalled.pc.in
@@ -0,0 +1,12 @@
+prefix=
+exec_prefix=
+abs_top_srcdir= abs_top_srcdir@
+abs_top_builddir= abs_top_builddir@
+vapidir= abs_top_srcdir@/folks
+
+Name: Folks dummy support library (uninstalled copy)
+Description: Dummy support library for the Folks meta-contacts library
+Version: @VERSION@
+Requires: folks glib-2.0 gobject-2.0 gee-0.8
+Libs: ${abs_top_builddir}/backends/dummy/libfolks-dummy.la
+Cflags: -I${abs_top_srcdir} -I${abs_top_srcdir}/backends/eds -I${abs_top_builddir}
diff --git a/backends/dummy/lib/folks-dummy.deps b/backends/dummy/lib/folks-dummy.deps
new file mode 100644
index 0000000..45c26b8
--- /dev/null
+++ b/backends/dummy/lib/folks-dummy.deps
@@ -0,0 +1,4 @@
+glib-2.0
+gobject-2.0
+folks
+gee-0.8
diff --git a/backends/dummy/lib/folks-dummy.pc.in b/backends/dummy/lib/folks-dummy.pc.in
new file mode 100644
index 0000000..40ab2a1
--- /dev/null
+++ b/backends/dummy/lib/folks-dummy.pc.in
@@ -0,0 +1,15 @@
+prefix= prefix@
+exec_prefix= exec_prefix@
+libdir= libdir@
+bindir= bindir@
+includedir= includedir@
+datarootdir= datarootdir@
+datadir= datadir@
+vapidir= datadir@/vala/vapi
+
+Name: Folks dummy support library
+Description: Dummy support library for the Folks meta-contacts library
+Version: @VERSION@
+Requires: folks glib-2.0 gobject-2.0 gee-0.8
+Libs: -L${libdir} -lfolks-dummy
+Cflags: -I${includedir}
diff --git a/configure.ac b/configure.ac
index 991a94e..c70af3f 100644
--- a/configure.ac
+++ b/configure.ac
@@ -292,6 +292,8 @@ AS_IF([test \
         ! -e ${sd}tests/folks/backend_loading_vala.stamp -o \
         ! -e ${sd}tests/key-file/individual_retrieval_vala.stamp -o \
         ! -e ${sd}tests/lib/folks-test.vapi -o \
+        ! -e ${sd}tests/lib/dummy/dummy-test.vapi -o \
+        ! -e ${sd}tests/lib/dummy/libdummy_test_la_vala.stamp -o \
         ! -e ${sd}tests/lib/key-file/kf-test.vapi -o \
         ! -e ${sd}tests/lib/key-file/libkf_test_la_vala.stamp -o \
         ! -e ${sd}tests/lib/telepathy/contactlist/tp-test-contactlist.gir -o \
@@ -301,6 +303,7 @@ AS_IF([test \
         ! -e ${sd}tests/telepathy/individual_retrieval_vala.stamp -o \
         ! -e ${sd}tests/telepathy/individual_properties_vala.stamp -o \
         ! -e ${sd}tests/folks/backend_loading_vala.stamp -o \
+        ! -e ${sd}backends/dummy/dummy_la_vala.stamp -o \
         ! -e ${sd}backends/key-file/key_file_la_vala.stamp -o \
         ! -e ${sd}backends/telepathy/telepathy_la_vala.stamp \
                 -o \
@@ -645,11 +648,15 @@ AC_CONFIG_FILES([
     backends/tracker/lib/folks-tracker-uninstalled.pc
     backends/eds/lib/folks-eds.pc
     backends/eds/lib/folks-eds-uninstalled.pc
+    backends/dummy/lib/folks-dummy.pc
+    backends/dummy/lib/folks-dummy-uninstalled.pc
     folks/folks.pc
     folks/folks-uninstalled.pc
     folks/org.freedesktop.folks.gschema.xml
     Makefile
     backends/Makefile
+    backends/dummy/Makefile
+    backends/dummy/lib/Makefile
     backends/key-file/Makefile
     backends/libsocialweb/Makefile
     backends/libsocialweb/lib/Makefile
@@ -669,6 +676,7 @@ AC_CONFIG_FILES([
     tests/data/Makefile
     tests/eds/Makefile
     tests/folks/Makefile
+    tests/dummy/Makefile
     tests/key-file/Makefile
     tests/libsocialweb/Makefile
     tests/telepathy/Makefile
@@ -676,6 +684,7 @@ AC_CONFIG_FILES([
     tests/lib/Makefile
     tests/lib/folks-test-uninstalled.pc
     tests/lib/eds/Makefile
+    tests/lib/dummy/Makefile
     tests/lib/key-file/Makefile
     tests/lib/libsocialweb/Makefile
     tests/lib/telepathy/Makefile
diff --git a/tests/Makefile.am b/tests/Makefile.am
index 10c0f56..325533f 100644
--- a/tests/Makefile.am
+++ b/tests/Makefile.am
@@ -3,6 +3,7 @@
 SUBDIRS = \
        data \
        lib \
+       dummy \
        key-file \
        tools \
        $(NULL)
@@ -30,6 +31,7 @@ DIST_SUBDIRS = \
        lib \
        tools \
        folks \
+       dummy \
        eds \
        key-file \
        telepathy \
diff --git a/tests/dummy/Makefile.am b/tests/dummy/Makefile.am
new file mode 100644
index 0000000..67c964f
--- /dev/null
+++ b/tests/dummy/Makefile.am
@@ -0,0 +1,76 @@
+AM_CFLAGS = \
+       $(ERROR_CFLAGS) \
+       $(NULL)
+
+AM_CPPFLAGS = \
+       $(GLIB_CFLAGS) \
+       $(GEE_CFLAGS) \
+       -I$(top_srcdir) \
+       -I$(top_srcdir)/folks \
+       -I$(top_srcdir)/backends/dummy/lib \
+       -I$(top_srcdir)/tests/lib \
+       -I$(top_srcdir)/tests/lib/dummy \
+       -include $(CONFIG_HEADER) \
+       $(NULL)
+
+LDADD = \
+       $(top_builddir)/tests/lib/dummy/libdummy-test.la \
+       $(top_builddir)/tests/lib/libfolks-test.la \
+       $(top_builddir)/backends/dummy/lib/libfolks-dummy.la \
+       $(top_builddir)/folks/libfolks.la \
+       $(GLIB_LIBS) \
+       $(GEE_LIBS) \
+       -L$(top_srcdir)/backends/dummy/lib \
+       $(NULL)
+
+AM_VALAFLAGS += \
+       $(ERROR_VALAFLAGS) \
+       --vapidir=. \
+       --vapidir=$(top_srcdir)/folks \
+       --vapidir=$(top_srcdir)/backends/dummy/lib \
+       --vapidir=$(top_srcdir)/tests/lib \
+       --vapidir=$(top_srcdir)/tests/lib/dummy \
+       --pkg gobject-2.0 \
+       --pkg gio-2.0 \
+       --pkg gee-0.8 \
+       --pkg folks \
+       --pkg folks-dummy \
+       --pkg folks-test \
+       --pkg dummy-test \
+       -g \
+       $(NULL)
+
+# in order from least to most complex
+noinst_PROGRAMS = \
+       individual-retrieval \
+       add-persona \
+       $(NULL)
+
+TESTS = $(noinst_PROGRAMS)
+
+individual_retrieval_SOURCES = \
+       individual-retrieval.vala \
+       $(NULL)
+
+add_persona_SOURCES = \
+       add-persona.vala \
+       $(NULL)
+
+CLEANFILES = \
+        *.pid \
+        *.address \
+        $(TESTS) \
+        $(NULL)
+
+MAINTAINERCLEANFILES = \
+        $(addsuffix .c,$(noinst_PROGRAMS)) \
+        individual_retrieval_vala.stamp \
+        $(NULL)
+
+EXTRA_DIST = \
+       $(MAINTAINERCLEANFILES) \
+       $(NULL)
+
+-include $(top_srcdir)/git.mk
+-include $(top_srcdir)/valgrind.mk
+-include $(top_srcdir)/check.mk
diff --git a/tests/dummy/add-persona.vala b/tests/dummy/add-persona.vala
new file mode 100644
index 0000000..c204f6d
--- /dev/null
+++ b/tests/dummy/add-persona.vala
@@ -0,0 +1,481 @@
+/*
+ * Copyright (C) 2011 Collabora Ltd.
+                 2013 Canonical Ltd.
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ *
+ * Authors: Raul Gutierrez Segales <raul gutierrez segales collabora co uk>
+ *          Renato Araujo Oliveira Filho <renato canonical com>
+ *
+ */
+
+using Folks;
+using Gee;
+//using DummyTest;
+//using Dummy;
+
+public class AddPersonaTests : DummyTest.TestCase
+{
+  private GLib.MainLoop _main_loop;
+  private IndividualAggregator _aggregator;
+  private string _persona_fullname;
+  private string _persona_nickname;
+  private string _email_1;
+  private bool _added_persona = false;
+  private HashTable<string, bool> _properties_found;
+  private string _avatar_path;
+  private string _im_addr_1;
+  private string _im_addr_2;
+  private string _phone_1;
+  private string _phone_1_type;
+  private string _phone_2;
+  private string _phone_2_type;
+  private PostalAddressFieldDetails _address;
+  private string _po_box = "12345";
+  private string _locality = "locality";
+  private string _postal_code = "code";
+  private string _street = "some street";
+  private string _extension = "some extension";
+  private string _country = "some country";
+  private string _region = "some region";
+  private string _family_name;
+  private string _given_name;
+  private string _note = "This is a note.";
+  private Individual _individual_received;
+
+  public AddPersonaTests ()
+    {
+      base ("AddPersonaTests");
+
+      this.add_test ("test adding a persona to dummy ", this.test_add_persona);
+    }
+
+  public void test_add_persona ()
+    {
+      this._main_loop = new GLib.MainLoop (null, false);
+      this._persona_fullname = "persona #1";
+      this._persona_nickname = "Jo";
+      this._email_1 = "someone-1 example org";
+      this._avatar_path = Folks.TestUtils.get_source_test_data (
+          "data/avatar-01.jpg");
+      this._im_addr_1 = "someone-1 jabber example org";
+      this._im_addr_2 = "someone-2 jabber example org";
+      this._phone_1 = "12345";
+      this._phone_1_type = AbstractFieldDetails.PARAM_TYPE_HOME;
+      this._phone_2 = "54321";
+      this._phone_2_type = AbstractFieldDetails.PARAM_TYPE_OTHER;
+      this._family_name = "family";
+      this._given_name = "given";
+
+      var pa = new PostalAddress (this._po_box,
+          this._extension, this._street, this._locality, this._region,
+          this._postal_code, this._country, null, null);
+      this._address = new PostalAddressFieldDetails (pa);
+      this._address.add_parameter (AbstractFieldDetails.PARAM_TYPE,
+          AbstractFieldDetails.PARAM_TYPE_HOME);
+
+      this._properties_found = new HashTable<string, bool>
+          (str_hash, str_equal);
+      this._properties_found.insert ("full_name", false);
+      this._properties_found.insert ("nickname", false);
+      this._properties_found.insert ("email-1", false);
+      this._properties_found.insert ("avatar", false);
+      this._properties_found.insert ("im-addr-1", false);
+      this._properties_found.insert ("im-addr-2", false);
+      this._properties_found.insert ("phone-1", false);
+      this._properties_found.insert ("phone-2", false);
+      this._properties_found.insert ("postal-address-1", false);
+      this._properties_found.insert ("structured_name", false);
+      this._properties_found.insert ("note", false);
+      //FIXME: for some reason Birthday is not getting notified
+      //this._properties_found.insert ("birthday", false);
+      this._properties_found.insert ("role-1", false);
+      this._properties_found.insert ("is-favourite", false);
+
+      this._test_add_persona_async.begin ();
+
+      TestUtils.loop_run_with_timeout (this._main_loop);
+
+      foreach (var k in this._properties_found.get_values ())
+        {
+          assert (k);
+        }
+    }
+
+  private async void _test_add_persona_async ()
+    {
+      this._aggregator = new IndividualAggregator ();
+      this._aggregator.individuals_changed_detailed.connect (this._individuals_changed_cb);
+      try
+        {
+          yield this._aggregator.prepare ();
+          this._try_to_add ();
+        }
+      catch (GLib.Error e)
+        {
+          GLib.warning ("Error when calling prepare: %s\n", e.message);
+        }
+    }
+
+  private async void _add_persona ()
+    {
+      HashTable<string, Value?> details = new HashTable<string, Value?>
+          (str_hash, str_equal);
+
+      Value? v1 = Value (typeof (string));
+      v1.set_string (this._persona_fullname);
+      details.insert (Folks.PersonaStore.detail_key (PersonaDetail.FULL_NAME),
+          (owned) v1);
+
+      Value? v2 = Value (typeof (Set));
+      var emails = new HashSet<EmailFieldDetails> (
+          AbstractFieldDetails<string>.hash_static,
+          AbstractFieldDetails<string>.equal_static);
+      var email_1 = new EmailFieldDetails (this._email_1);
+      email_1.set_parameter (AbstractFieldDetails.PARAM_TYPE,
+          AbstractFieldDetails.PARAM_TYPE_HOME);
+      emails.add (email_1);
+      v2.set_object (emails);
+      details.insert (
+          Folks.PersonaStore.detail_key (PersonaDetail.EMAIL_ADDRESSES),
+          (owned) v2);
+
+      Value? v3 = Value (typeof (LoadableIcon));
+      var avatar = new FileIcon (File.new_for_path (this._avatar_path));
+      v3.set_object (avatar);
+      details.insert (Folks.PersonaStore.detail_key (PersonaDetail.AVATAR),
+          (owned) v3);
+
+      Value? v4 = Value (typeof (MultiMap));
+      var im_fds = new HashMultiMap<string, ImFieldDetails> ();
+      im_fds.set ("jabber", new ImFieldDetails (this._im_addr_1));
+      im_fds.set ("yahoo", new ImFieldDetails (this._im_addr_2));
+      v4.set_object (im_fds);
+      details.insert (
+         Folks.PersonaStore.detail_key (PersonaDetail.IM_ADDRESSES), v4);
+
+      Value? v5 = Value (typeof (Set));
+      var phones = new HashSet<PhoneFieldDetails> (
+          AbstractFieldDetails<string>.hash_static,
+          AbstractFieldDetails<string>.equal_static);
+
+      var phone_1 = new PhoneFieldDetails (this._phone_1);
+      phone_1.set_parameter (AbstractFieldDetails.PARAM_TYPE,
+          this._phone_1_type);
+      phones.add (phone_1);
+      var phone_2 = new PhoneFieldDetails (this._phone_2);
+      phone_2.set_parameter (AbstractFieldDetails.PARAM_TYPE,
+          this._phone_2_type);
+      phones.add (phone_2);
+      v5.set_object (phones);
+      details.insert (
+          Folks.PersonaStore.detail_key (PersonaDetail.PHONE_NUMBERS),
+          (owned) v5);
+
+      Value? v6 = Value (typeof (Set));
+      var pa_fds = new HashSet<PostalAddressFieldDetails> (
+          AbstractFieldDetails<PostalAddress>.hash_static,
+          AbstractFieldDetails<PostalAddress>.equal_static);
+
+      PostalAddress pa_a = new PostalAddress (this._po_box,
+          this._extension, this._street, this._locality, this._region,
+          this._postal_code, this._country, null, null);
+      var pa_fd_a = new PostalAddressFieldDetails (pa_a);
+      pa_fd_a.add_parameter (AbstractFieldDetails.PARAM_TYPE,
+          AbstractFieldDetails.PARAM_TYPE_HOME);
+      pa_fds.add (pa_fd_a);
+      v6.set_object (pa_fds);
+      details.insert (
+          Folks.PersonaStore.detail_key (PersonaDetail.POSTAL_ADDRESSES),
+          (owned) v6);
+
+      Value? v7 = Value (typeof (StructuredName));
+      StructuredName sname = new StructuredName (this._family_name,
+          this._given_name, null, null, null);
+      v7.set_object (sname);
+      details.insert (
+          Folks.PersonaStore.detail_key (PersonaDetail.STRUCTURED_NAME),
+          (owned) v7);
+
+      Value? v8 = Value (typeof (Set));
+      var notes = new HashSet<NoteFieldDetails> (
+          AbstractFieldDetails<string>.hash_static,
+          AbstractFieldDetails<string>.equal_static);
+      var note = new NoteFieldDetails (this._note);
+      notes.add (note);
+      v8.set_object (notes);
+      details.insert (
+          Folks.PersonaStore.detail_key (PersonaDetail.NOTES),
+          (owned) v8);
+
+      Value? v9 = Value (typeof (DateTime));
+      DateTime dobj = new DateTime.local (1980, 1, 1, 0, 0, 0.0).to_utc ();
+      v9.set_boxed (dobj);
+      details.insert (Folks.PersonaStore.detail_key (PersonaDetail.BIRTHDAY),
+          (owned) v9);
+
+      Value? v10 = Value (typeof (Set));
+      var role_fds = new HashSet<RoleFieldDetails> (
+          AbstractFieldDetails<Role>.hash_static,
+          AbstractFieldDetails<Role>.equal_static);
+      var r1 = new Role ("Dr.", "The Nut House Ltd");
+      r1.role = "The Manager";
+      var role_fd1 = new RoleFieldDetails (r1);
+      role_fds.add (role_fd1);
+      v10.set_object (role_fds);
+      details.insert (Folks.PersonaStore.detail_key (PersonaDetail.ROLES),
+          (owned) v10);
+
+      Value? v11 = Value (typeof (bool));
+      v11.set_boolean (true);
+      details.insert (
+          Folks.PersonaStore.detail_key (PersonaDetail.IS_FAVOURITE),
+          (owned) v11);
+
+      Value? v12 = Value (typeof (string));
+      v12.set_string (this._persona_nickname);
+      details.insert (Folks.PersonaStore.detail_key (PersonaDetail.NICKNAME),
+          (owned) v12);
+
+      try
+        {
+          yield this._aggregator.add_persona_from_details (null,
+              this.dummy_persona_store, details);
+        }
+      catch (Folks.IndividualAggregatorError e)
+        {
+          GLib.warning ("[AddPersonaError] add_persona_from_details: %s\n",
+              e.message);
+        }
+    }
+
+  private void _individuals_changed_cb (
+       MultiMap<Individual?, Individual?> changes)
+    {
+      var added = changes.get_values ();
+      var removed = changes.get_keys ();
+
+      uint num_replaces = 0;
+
+      foreach (var i in added)
+        {
+          if (i == null)
+            {
+              continue;
+            }
+
+          num_replaces = this._track_individual (i);
+        }
+
+      assert (removed.size <= num_replaces + 1);
+    }
+
+  private uint _track_individual (Individual i)
+    {
+      uint retval = 0;
+
+      if (i.is_user == false)
+        {
+          /* we assume that there will be exactly one (unique) individual
+           * received */
+          assert (this._individual_received == null ||
+              this._individual_received.id == i.id);
+
+          /* handle replacement */
+          if (this._individual_received != null)
+            {
+              i.notify["full-name"].disconnect (this._notify_cb);
+              i.notify["nickname"].disconnect (this._notify_cb);
+              i.notify["email-addresses"].disconnect (this._notify_cb);
+              i.notify["avatar"].disconnect (this._notify_cb);
+              i.notify["im-addresses"].disconnect (this._notify_cb);
+              i.notify["phone-numbers"].disconnect (this._notify_cb);
+              i.notify["postal-addresses"].disconnect (this._notify_cb);
+              i.notify["structured-name"].disconnect (this._notify_cb);
+              i.notify["notes"].disconnect (this._notify_cb);
+              i.notify["birthday"].disconnect (this._notify_cb);
+              i.notify["roles"].disconnect (this._notify_cb);
+              i.notify["is-favourite"].disconnect (this._notify_cb);
+
+              this._properties_found.remove_all ();
+            }
+
+          this._individual_received = i;
+          retval++;
+
+          i.notify["full-name"].connect (this._notify_cb);
+          i.notify["nickname"].connect (this._notify_cb);
+          i.notify["email-addresses"].connect (this._notify_cb);
+          i.notify["avatar"].connect (this._notify_cb);
+          i.notify["im-addresses"].connect (this._notify_cb);
+          i.notify["phone-numbers"].connect (this._notify_cb);
+          i.notify["postal-addresses"].connect (this._notify_cb);
+          i.notify["structured-name"].connect (this._notify_cb);
+          i.notify["notes"].connect (this._notify_cb);
+          i.notify["birthday"].connect (this._notify_cb);
+          i.notify["roles"].connect (this._notify_cb);
+          i.notify["is-favourite"].connect (this._notify_cb);
+
+          this._check_properties.begin (i);
+        }
+
+      return retval;
+    }
+
+  private void _notify_cb (Object individual_obj, ParamSpec ps)
+    {
+      Folks.Individual i = (Folks.Individual) individual_obj;
+      this._check_properties.begin (i);
+    }
+
+  private void _try_to_add ()
+    {
+      lock (this._added_persona)
+        {
+          this._add_persona.begin ();
+        }
+    }
+
+  private async void _check_properties (Individual i)
+    {
+      if (i.full_name == this._persona_fullname)
+        this._properties_found.replace ("full_name", true);
+
+      if (i.nickname == this._persona_nickname)
+        {
+          this._properties_found.replace ("nickname", true);
+        }
+
+      foreach (var e in i.email_addresses)
+        {
+          if (e.value == this._email_1)
+            {
+              this._properties_found.replace ("email-1", true);
+            }
+        }
+
+      foreach (var proto in i.im_addresses.get_keys ())
+        {
+          var im_fds = i.im_addresses.get (proto);
+          foreach (var im_fd in im_fds)
+            {
+              if (im_fd.value == this._im_addr_1)
+                this._properties_found.replace ("im-addr-1", true);
+              else if (im_fd.value == this._im_addr_2)
+                this._properties_found.replace ("im-addr-2", true);
+            }
+        }
+
+      foreach (var phone_fd in i.phone_numbers)
+        {
+          var phone_1 = new PhoneFieldDetails (this._phone_1);
+          phone_1.set_parameter (AbstractFieldDetails.PARAM_TYPE,
+              this._phone_1_type);
+          var phone_2 = new PhoneFieldDetails (this._phone_2);
+          phone_2.set_parameter (AbstractFieldDetails.PARAM_TYPE,
+              this._phone_2_type);
+
+          if (phone_fd.equal (phone_1))
+            {
+              this._properties_found.replace ("phone-1", true);
+            }
+          else if (phone_fd.equal (phone_2))
+            {
+              this._properties_found.replace ("phone-2", true);
+            }
+        }
+
+      foreach (var pa_fd in i.postal_addresses)
+        {
+          this._address.id = pa_fd.id;
+          if (pa_fd.equal (this._address))
+            this._properties_found.replace ("postal-address-1", true);
+        }
+
+      if (i.structured_name != null &&
+          i.structured_name.family_name == this._family_name &&
+          i.structured_name.given_name == this._given_name)
+        this._properties_found.replace ("structured_name", true);
+
+      foreach (var note in i.notes)
+        {
+          if (note.equal (new NoteFieldDetails (this._note)))
+            {
+              this._properties_found.replace ("note", true);
+              break;
+            }
+        }
+
+      if (i.avatar != null)
+        {
+          var b = new FileIcon (File.new_for_path (this._avatar_path));
+
+          var same = yield TestUtils.loadable_icons_content_equal (b, i.avatar,
+              -1);
+          if (same)
+            this._properties_found.replace ("avatar", true);
+        }
+
+      if (i.birthday != null)
+        {
+          DateTime dobj = new DateTime.local (1980, 1, 1, 0, 0, 0.0).to_utc ();
+          if (i.birthday.equal (dobj)) {
+            this._properties_found.replace ("birthday", true);
+          }
+        }
+
+      foreach (var role_fd in i.roles)
+        {
+          var r1 = new Role ("Dr.", "The Nut House Ltd");
+          r1.role = "The Manager";
+          var role_fd_expected = new RoleFieldDetails (r1);
+          if (role_fd.equal (role_fd_expected))
+            this._properties_found.replace ("role-1", true);
+        }
+
+      if (i.is_favourite)
+        {
+          this._properties_found.replace ("is-favourite", true);
+        }
+
+      this._exit_if_all_properties_found ();
+    }
+
+  private void _exit_if_all_properties_found ()
+    {
+      GLib.debug("_exit_if_all_properties_found>>>>>>>>>>>>>>>>>>>>>>>>>>>: BEGIN\n");                
+      foreach (var k in this._properties_found.get_keys ())
+        {
+          var v = this._properties_found.lookup (k);
+          if (v == false) {
+            GLib.debug("_exit_if_all_properties_found>>>>>>>>>>>>>>>>>>>>>>>>>>>: [%s:%d] CONTINUE\n", k, 
(int) v);                
+            return;
+          }
+        }
+      GLib.debug("_exit_if_all_properties_found>>>>>>>>>>>>>>>>>>>>>>>>>>>: END\n");                
+      this._main_loop.quit ();
+    }
+}
+
+public int main (string[] args)
+{
+  Test.init (ref args);
+
+  var tests = new AddPersonaTests ();
+  tests.register ();
+  Test.run ();
+  tests.final_tear_down ();
+
+  return 0;
+}
diff --git a/tests/dummy/individual-retrieval.vala b/tests/dummy/individual-retrieval.vala
new file mode 100644
index 0000000..3a3c088
--- /dev/null
+++ b/tests/dummy/individual-retrieval.vala
@@ -0,0 +1,265 @@
+/*
+ * Copyright (C) 2011 Collabora Ltd.
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ *
+ * Authors: Renato Araujo Oliveira Filho <renato canonical com>
+ */
+
+using Gee;
+using Folks;
+using DummyTest;
+using Dummyf;
+
+public class IndividualRetrievalTests : DummyTest.TestCase
+{
+  public IndividualRetrievalTests ()
+    {
+      base ("IndividualRetrieval");
+
+      this.add_test ("dummy individuals", this.test_aggregator);
+    }
+
+  private Folks.Persona create_persona_rodrigo()
+    {
+      var rodrigo = new FatPersona(this.dummy_persona_store, "dummy 2");
+      var main_loop = new GLib.MainLoop (null, false);
+
+      rodrigo.change_full_name.begin("Rodrigo Almeida", (s, r) =>
+            {
+              try
+                {
+                  rodrigo.change_full_name.end(r);
+                }
+              catch (Folks.PropertyError e)
+                {
+                }
+                main_loop.quit();
+            });
+      TestUtils.loop_run_with_timeout (main_loop);
+
+      rodrigo.nickname = "kiko";
+      rodrigo.change_nickname.begin("kiko", (s, r) =>
+            {
+              try
+                {
+                  rodrigo.change_nickname.end(r);
+                }
+              catch (Folks.PropertyError e)
+                {
+                }
+                main_loop.quit();
+            });
+      TestUtils.loop_run_with_timeout (main_loop);
+
+      // Emails
+      var emails = new HashSet<EmailFieldDetails> (
+          AbstractFieldDetails<string>.hash_static,
+          AbstractFieldDetails<string>.equal_static);
+
+      var email_1 = new EmailFieldDetails ("rodrigo gmail com");
+      email_1.set_parameter (AbstractFieldDetails.PARAM_TYPE,
+          AbstractFieldDetails.PARAM_TYPE_HOME);
+      emails.add (email_1);
+      rodrigo.change_email_addresses.begin(emails, (s, r) =>
+            {
+              try
+                {
+                  rodrigo.change_email_addresses.end(r);
+                }
+              catch (Folks.PropertyError e)
+                {
+                }
+                main_loop.quit();
+            });
+      TestUtils.loop_run_with_timeout (main_loop);
+
+      //Ims
+      var im_fds = new HashMultiMap<string, ImFieldDetails> ();
+      im_fds.set ("jabber", new ImFieldDetails ("rodrigo jabber com"));
+      im_fds.set ("yahoo", new ImFieldDetails ("rodrigo yahoo com"));
+      rodrigo.change_im_addresses.begin(im_fds, (s, r) =>
+            {
+              try
+                {
+                  rodrigo.change_im_addresses.end(r);
+                }
+              catch (Folks.PropertyError e)
+                {
+                }
+                main_loop.quit();
+            });
+      TestUtils.loop_run_with_timeout (main_loop);
+      return rodrigo;
+    }
+
+
+  private Folks.Persona create_persona_renato()
+    {
+      var renato = new FatPersona(this.dummy_persona_store, "dummy 1");
+      var main_loop = new GLib.MainLoop (null, false);
+
+      renato.change_full_name.begin("Renato Araujo Oliveira Filho", (s, r) =>
+            {
+              try
+                {
+                  renato.change_full_name.end(r);
+                }
+              catch (Folks.PropertyError e)
+                {                 
+                  assert_not_reached ();
+                }
+                main_loop.quit();
+            });
+      TestUtils.loop_run_with_timeout (main_loop);
+
+      renato.change_nickname.begin("renatofilho", (s, r) =>
+            {
+              try
+                {
+                  renato.change_nickname.end(r);
+                }
+              catch (Folks.PropertyError e)
+                {
+                }
+                main_loop.quit();
+            });
+      TestUtils.loop_run_with_timeout (main_loop);
+
+      // Emails
+      var emails = new HashSet<EmailFieldDetails> (
+          AbstractFieldDetails<string>.hash_static,
+          AbstractFieldDetails<string>.equal_static);
+
+      var email_1 = new EmailFieldDetails ("renato canonical com");
+      email_1.set_parameter (AbstractFieldDetails.PARAM_TYPE,
+          AbstractFieldDetails.PARAM_TYPE_HOME);
+      emails.add (email_1);
+      renato.change_email_addresses.begin(emails, (s, r) =>
+            {
+              try
+                {
+                  renato.change_email_addresses.end(r);
+                }
+              catch (Folks.PropertyError e)
+                {
+                }
+                main_loop.quit();
+            });
+      TestUtils.loop_run_with_timeout (main_loop);
+
+      //Ims
+      var im_fds = new HashMultiMap<string, ImFieldDetails> ();
+      im_fds.set ("jabber", new ImFieldDetails ("renato jabber com"));
+      im_fds.set ("yahoo", new ImFieldDetails ("renato yahoo com"));
+      renato.change_im_addresses.begin(im_fds, (s, r) =>
+            {
+              try
+                {
+                  renato.change_im_addresses.end(r);
+                }
+              catch (Folks.PropertyError e)
+                {
+                }
+                main_loop.quit();
+            });
+      TestUtils.loop_run_with_timeout (main_loop);
+
+      return renato;
+    }
+    
+  private void register_personas()
+    {
+      var personas = new HashSet<Folks.Persona>();
+      var kiko = create_persona_rodrigo();
+      GLib.debug("CREATED: %s\n", (kiko as NameDetails).nickname);
+      personas.add(create_persona_renato());
+      personas.add(create_persona_rodrigo());
+      this.dummy_persona_store.register_personas (personas);
+    }
+
+  public void test_aggregator ()
+    {
+      var main_loop = new GLib.MainLoop (null, false);
+
+      HashSet<string> expected_individuals = new HashSet<string> ();
+      expected_individuals.add("Renato Araujo Oliveira Filho");
+      expected_individuals.add("Rodrigo Almeida");
+
+      /* Set up the aggregator */
+      var aggregator = new IndividualAggregator ();
+      aggregator.individuals_changed_detailed.connect ((changes) =>
+        {
+          var added = changes.get_values ();
+          var removed = changes.get_keys ();
+
+          assert (added.size == 2);
+
+          foreach (Individual i in added)
+            {
+              assert (i != null);
+              expected_individuals.remove (i.full_name);
+            }
+
+          assert (removed.size == 1);
+
+          main_loop.quit();
+        });
+
+      /* 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). */
+
+      Idle.add (() =>
+        {
+          aggregator.prepare.begin ((s,r) =>
+            {
+              try
+                {
+                  aggregator.prepare.end (r);
+                  this.dummy_persona_store.reach_quiescence ();
+                  register_personas();
+                }
+              catch (GLib.Error e1)
+                {
+                  GLib.critical ("failed to prepare aggregator: %s",
+                    e1.message);
+                  assert_not_reached ();
+                }
+            });
+
+          return false;
+        });
+
+      TestUtils.loop_run_with_timeout (main_loop);
+
+      /* We should have enumerated exactly the individuals in the set */
+      assert (expected_individuals.size == 0);
+
+      /* necessary to reset the aggregator for the next test */
+      aggregator = null;
+    }
+}
+
+public int main (string[] args)
+{
+  Test.init (ref args);
+
+  var tests = new IndividualRetrievalTests ();
+  tests.register ();
+  Test.run ();
+  tests.final_tear_down ();
+
+  return 0;
+}
diff --git a/tests/folks/Makefile.am b/tests/folks/Makefile.am
index 0856529..a605ec3 100644
--- a/tests/folks/Makefile.am
+++ b/tests/folks/Makefile.am
@@ -8,7 +8,9 @@ AM_CPPFLAGS = \
        $(TP_GLIB_CFLAGS) \
        -I$(top_srcdir) \
        -I$(top_srcdir)/folks \
+       -I$(top_srcdir)/backends/dummy/lib \
        -I$(top_srcdir)/tests/lib \
+       -I$(top_srcdir)/tests/lib/dummy \
        -I$(top_srcdir)/tests/lib/key-file \
        -I$(top_srcdir)/tests/lib/telepathy \
        -I$(top_srcdir)/tests/lib/telepathy/contactlist \
@@ -18,9 +20,11 @@ AM_CPPFLAGS = \
 
 LDADD = \
        $(top_builddir)/tests/lib/libfolks-test.la \
+       $(top_builddir)/tests/lib/dummy/libdummy-test.la \
        $(top_builddir)/tests/lib/key-file/libkf-test.la \
        $(top_builddir)/tests/lib/telepathy/libtpf-test.la \
        $(top_builddir)/tests/lib/telepathy/contactlist/libtp-test-contactlist.la \
+       $(top_builddir)/backends/dummy/lib/libfolks-dummy.la \
        $(top_builddir)/folks/libfolks.la \
        $(GLIB_LIBS) \
        $(GEE_LIBS) \
@@ -37,8 +41,12 @@ AM_VALAFLAGS += \
        --vapidir=$(abs_builddir) \
        --vapidir=$(abs_top_srcdir)/folks \
        --vapidir=$(abs_top_builddir)/folks \
+       --vapidir=$(abs_top_srcdir)/backends/dummy/lib \
+       --vapidir=$(abs_top_builddir)/backends/dummy/lib \
        --vapidir=$(abs_top_srcdir)/tests/lib \
        --vapidir=$(abs_top_builddir)/tests/lib \
+       --vapidir=$(abs_top_srcdir)/tests/lib/dummy \
+       --vapidir=$(abs_top_builddir)/tests/lib/dummy \
        --vapidir=$(abs_top_srcdir)/tests/lib/key-file \
        --vapidir=$(abs_top_builddir)/tests/lib/key-file \
        --vapidir=$(abs_top_srcdir)/tests/lib/telepathy \
@@ -51,9 +59,11 @@ AM_VALAFLAGS += \
        --pkg folks \
        --pkg folks-generics \
        --pkg folks-test \
+       --pkg dummy-test \
        --pkg kf-test \
        --pkg tpf-test \
        --pkg tp-test-contactlist \
+       --pkg folks-dummy \
        -g \
        $(NULL)
 
diff --git a/tests/lib/Makefile.am b/tests/lib/Makefile.am
index e080949..aa005ac 100644
--- a/tests/lib/Makefile.am
+++ b/tests/lib/Makefile.am
@@ -1,6 +1,7 @@
 # Build . first so that backends' test libraries can link to libfolks-test.la
 SUBDIRS = \
        . \
+       dummy \
        key-file \
        $(NULL)
 
@@ -27,6 +28,7 @@ SUBDIRS += eds
 endif
 
 DIST_SUBDIRS = \
+       dummy \
        key-file \
        telepathy \
        eds \
diff --git a/tests/lib/dummy/Makefile.am b/tests/lib/dummy/Makefile.am
new file mode 100644
index 0000000..a90d068
--- /dev/null
+++ b/tests/lib/dummy/Makefile.am
@@ -0,0 +1,59 @@
+AM_VALAFLAGS += \
+       $(ERROR_VALAFLAGS) \
+       --library=dummy-test \
+       --header=dummy-test.h \
+       --vapidir=$(top_srcdir)/folks \
+       --vapidir=$(top_srcdir)/backends/dummy/lib \
+       --vapidir=$(top_srcdir)/backends/dummy/vapi \
+       --vapidir=$(abs_top_srcdir)/tests/lib \
+       --vapidir=$(abs_top_builddir)/tests/lib \
+       --vapidir=. \
+       --pkg gee-0.8 \
+       --pkg folks-dummy \
+       --pkg folks-test \
+       -g \
+       $(NULL)
+
+noinst_LTLIBRARIES = libdummy-test.la
+
+libdummy_test_la_SOURCES = \
+       test-case.vala \
+       $(NULL)
+
+libdummy_test_la_CFLAGS = \
+       -I$(top_srcdir) \
+       -I$(top_srcdir)/folks \
+       -I$(top_srcdir)/tests/lib \
+       -I$(top_srcdir)/backends/dummy/lib \
+       -I$(top_srcdir)/backends/dummy/lib/folks \
+       $(AM_CFLAGS) \
+       $(ERROR_CFLAGS) \
+       $(GLIB_CFLAGS) \
+       $(GEE_CFLAGS) \
+       $(NULL)
+
+libdummy_test_la_LIBADD = \
+       $(top_builddir)/folks/libfolks.la \
+       $(top_builddir)/tests/lib/libfolks-test.la \
+       $(top_builddir)/backends/dummy/lib/libfolks-dummy.la \
+       $(GLIB_LIBS) \
+       $(GEE_LIBS) \
+       $(NULL)
+
+MAINTAINERCLEANFILES = \
+       dummy-test.vapi \
+       dummy-test.h \
+       $(NULL)
+
+EXTRA_DIST = \
+       dummy-test.deps \
+       dummy-test.vapi \
+       dummy-test.h \
+       $(NULL)
+
+GITIGNOREFILES = \
+       $(patsubst %.vala,%.c,$(filter %.vala,$(libdummy_test_la_SOURCES))) \
+       libdummy_test_la_vala.stamp \
+       $(NULL)
+
+-include $(top_srcdir)/git.mk
diff --git a/tests/lib/dummy/test-case.vala b/tests/lib/dummy/test-case.vala
new file mode 100644
index 0000000..45e58e2
--- /dev/null
+++ b/tests/lib/dummy/test-case.vala
@@ -0,0 +1,97 @@
+/* test-case.vala
+ *
+ *
+ * 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:
+ *      Renato Araujo Oliveira Filho <renato canonical com>
+ */
+
+using Folks;
+using Gee;
+
+/**
+ * A test case for the dummy backend, which is configured as the
+ * primary store and as the only backend allowed.
+ */
+public class DummyTest.TestCase : Folks.TestCase
+{
+  /**
+   * The dummy test backend.
+   */
+  public Dummyf.Backend dummy_backend;
+
+  /**
+   * The default persona store
+   */
+  public Dummyf.PersonaStore dummy_persona_store;
+
+  private BackendStore backend_store;
+
+  public TestCase (string name)
+    {
+      base (name);
+
+      Environment.set_variable ("FOLKS_BACKENDS_ALLOWED", "dummy", true);
+      Environment.set_variable ("FOLKS_PRIMARY_STORE", "dummy", true);
+    }
+
+  public override void set_up ()
+    {
+      base.set_up ();
+
+      var main_loop = new GLib.MainLoop (null, false);
+      this.backend_store = BackendStore.dup();
+      this.backend_store.load_backends.begin((obj, res) => 
+        {
+            try 
+              {
+                this.backend_store.load_backends.end(res);
+                main_loop.quit ();
+              }
+            catch (GLib.Error error)
+              {
+                GLib.critical("Fail to initialized backend store.\n");
+                assert_not_reached ();
+              } 
+        });
+      TestUtils.loop_run_with_timeout (main_loop);
+
+      this.dummy_backend = this.backend_store.dup_backend_by_name ("dummy") as Dummyf.Backend;
+      this.configure_primary_store ();
+    }
+
+  public virtual void configure_primary_store ()
+    {
+      var persona_stores = new HashSet<PersonaStore>();
+      string[] writable_properties = { Folks.PersonaStore.detail_key (PersonaDetail.BIRTHDAY),
+          Folks.PersonaStore.detail_key (PersonaDetail.EMAIL_ADDRESSES),
+          Folks.PersonaStore.detail_key (PersonaDetail.PHONE_NUMBERS),
+          null };
+
+      this.dummy_persona_store = new Dummyf.PersonaStore ("dummy-store", "Dummy personas", 
writable_properties);
+      this.dummy_persona_store.persona_type = typeof (Dummyf.FatPersona);
+
+      persona_stores.add (this.dummy_persona_store);
+      this.dummy_backend.register_persona_stores (persona_stores);
+    }
+
+  public override void tear_down ()
+    {
+      this.dummy_persona_store = null;
+      this.dummy_backend = null;
+      base.tear_down ();
+    }
+}
diff --git a/tests/lib/test-case.vala b/tests/lib/test-case.vala
index 82b0700..a174f64 100644
--- a/tests/lib/test-case.vala
+++ b/tests/lib/test-case.vala
@@ -61,6 +61,7 @@ public abstract class Folks.TestCase : Object
         {
           string[] locations = {
               Folks.BuildConf.ABS_TOP_BUILDDIR + "/backends/key-file/.libs/key-file.so",
+              Folks.BuildConf.ABS_TOP_BUILDDIR + "/backends/dummy/.libs/dummy.so",
           };
 
           if (Folks.BuildConf.HAVE_EDS)
diff --git a/tests/lib/test-utils.vala b/tests/lib/test-utils.vala
index a6b855c..129bc0e 100644
--- a/tests/lib/test-utils.vala
+++ b/tests/lib/test-utils.vala
@@ -22,6 +22,7 @@
 
 using Folks;
 using GLib;
+using Gee;
 
 public class Folks.TestUtils
 {
@@ -335,4 +336,73 @@ public class Folks.TestUtils
           return BuildConf.ABS_TOP_BUILDDIR + "/tests/" + filename;
         }
     }
+
+  /**
+   * TODO
+   *
+   * @since UNRELEASED
+   */
+  public static bool personas_set_equals (Set<Persona> actual_personas,
+      string[] expected_personas)
+    {
+      if (actual_personas.size != expected_personas.length)
+        {
+          return false;
+        }
+
+      foreach (var p in actual_personas)
+        {
+          if (!(p.uid in expected_personas))
+            {
+              return false;
+            }
+        }
+
+      return true;
+    }
+
+  /**
+   * TODO
+   *
+   * @since UNRELEASED
+   */
+  public static bool individuals_map_equals (
+      Map<string, Individual> actual_individuals, string[] expected_individuals)
+    {
+      if (actual_individuals.size != expected_individuals.length)
+        {
+          return false;
+        }
+
+      var _actual_individuals = new HashSet<Individual> ();
+      _actual_individuals.add_all (actual_individuals.values);
+      assert (_actual_individuals.size == actual_individuals.size);
+
+      var _expected_individuals = new HashSet<string> ();
+      foreach (var i in expected_individuals)
+        {
+          _expected_individuals.add (i);
+        }
+
+      foreach (var i in _actual_individuals)
+        {
+          var actual_personas = i.personas;
+
+          foreach (var _expected_personas in _expected_individuals)
+            {
+              var expected_personas = _expected_personas.split (",");
+
+              if (TestUtils.personas_set_equals (actual_personas,
+                  expected_personas) == true)
+                {
+                  _expected_individuals.remove (_expected_personas);
+                  break;
+                }
+            }
+
+          return false;
+        }
+
+      return true;
+    }
 }


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