[folks] dummy: Add a dummy backend



commit 3a63de91648c822306df37e3b88285da8e28f057
Author: Philip Withnall <philip withnall collabora co uk>
Date:   Thu Nov 7 10:26:02 2013 +0000

    dummy: Add a dummy backend
    
    This is a new backend targeted at making libfolks more testable. It is
    designed to be used in internal libfolks unit tests, allowing the test
    driver code to manipulate the backing store state to test how that
    affects front-end state in libfolks. For example, it will be useful in
    more thoroughly testing the IndividualAggregator.
    
    It includes work by Renato Araujo Oliveira Filho <renatox gmail com>.
    
    The backend may also be useful for testing libfolks clients,
    such as contacts UIs, by providing an easy way to create well-known
    personas to appear in the the UI. Hence, it is an installed backend, and
    is loaded by default (although creates no Backend or PersonaStore instances
    unless API calls are made).
    
    It is designed to allow delay and error injection into all major
    PersonaStore operations, and to allow any kind of Persona implementation
    to be returned from the store.
    
    It includes a few small test cases to sanity-check that the backend
    works, but no thorough self-test-coverage.
    
    Full API documentation is included and installed by default. The API is
    not currently stable, so while external projects may start to consume
    it, they should be prepared for breakage in the future as the API
    stabilises. No timescale is given for such stabilisation.
    
    This commit does not include any changes to the core libfolks unit
    tests, but future changes could be made to port them to use the dummy
    backend.
    
    New API:
     • libfolks-dummy.la mocking library and all its symbols
    
    https://bugzilla.gnome.org/show_bug.cgi?id=648811

 NEWS                                             |    3 +
 backends/Makefile.am                             |    2 +
 backends/dummy/Makefile.am                       |   40 +
 backends/dummy/dummy-backend-factory.vala        |   49 +
 backends/dummy/lib/Makefile.am                   |   47 +
 backends/dummy/lib/dummy-backend.vala            |  394 ++++++++
 backends/dummy/lib/dummy-full-persona.vala       | 1139 ++++++++++++++++++++++
 backends/dummy/lib/dummy-persona-store.vala      | 1079 ++++++++++++++++++++
 backends/dummy/lib/dummy-persona.vala            |  312 ++++++
 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                                     |   17 +
 docs/Makefile.am                                 |   61 ++
 tests/Makefile.am                                |    2 +
 tests/dummy/Makefile.am                          |   41 +
 tests/dummy/add-persona.vala                     |  449 +++++++++
 tests/dummy/individual-retrieval.vala            |  176 ++++
 tests/lib/Makefile.am                            |    2 +
 tests/lib/dummy/Makefile.am                      |   59 ++
 tests/lib/dummy/test-case.vala                   |  141 +++
 tests/lib/test-case.vala                         |    1 +
 22 files changed, 4045 insertions(+), 0 deletions(-)
---
diff --git a/NEWS b/NEWS
index 836ba89..1b8e57d 100644
--- a/NEWS
+++ b/NEWS
@@ -4,13 +4,16 @@ Overview of changes from libfolks 0.9.6 to libfolks 0.9.7
 Dependencies:
 
 Major changes:
+ • Add a dummy backend
 
 Bugs fixed:
  • Bug 651672 — Individual should have a display-name property
+ • Bug 648811 — Add a dummy backend
 
 API changes:
  • Add Individual.display_name
  • Add StructuredName.to_string_with_format()
+ • Add libfolks-dummy.la and all its symbols
 
 Overview of changes from libfolks 0.9.5 to libfolks 0.9.6
 =========================================================
diff --git a/backends/Makefile.am b/backends/Makefile.am
index 148a944..774f02c 100644
--- a/backends/Makefile.am
+++ b/backends/Makefile.am
@@ -1,4 +1,5 @@
 SUBDIRS = \
+       dummy \
        key-file \
        $(NULL)
 
@@ -34,6 +35,7 @@ DIST_SUBDIRS = \
        ofono \
        telepathy \
        tracker \
+       dummy \
        $(NULL)
 
 EXTRA_DIST = \
diff --git a/backends/dummy/Makefile.am b/backends/dummy/Makefile.am
new file mode 100644
index 0000000..2a282ef
--- /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/dummy-backend-factory.vala b/backends/dummy/dummy-backend-factory.vala
new file mode 100644
index 0000000..8228ef4
--- /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, 2013 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 FolksDummy.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..a5acd40
--- /dev/null
+++ b/backends/dummy/lib/Makefile.am
@@ -0,0 +1,47 @@
+CLEANFILES =
+MAINTAINERCLEANFILES =
+EXTRA_DIST =
+
+include $(top_srcdir)/backends/backend-library.mk
+
+BACKEND_NAME = dummy
+BACKEND_NAME_C = Dummy
+BACKEND_LT_VERSION = $(FOLKS_DUMMY_LT_VERSION)
+BACKEND_API_VERSION = $(FOLKS_DUMMY_API_VERSION)
+BACKEND_SYMBOLS_REGEX = "^(FOLKS_DUMMY|folks_dummy)_.*|"
+BACKEND_NAMESPACE = Dummy
+
+lib_LTLIBRARIES = libfolks-dummy.la
+
+# Deliberately don't include $(backend_library_sources) to avoid namespace.vala
+libfolks_dummy_la_SOURCES = \
+       dummy-backend.vala \
+       dummy-persona.vala \
+       dummy-persona-store.vala \
+       dummy-full-persona.vala \
+       $(NULL)
+
+libfolks_dummy_la_VALAFLAGS = \
+       $(backend_library_valaflags) \
+       $(NULL)
+
+libfolks_dummy_la_CPPFLAGS = \
+       $(backend_library_cppflags) \
+       -include folks/redeclare-internal-api.h \
+       $(NULL)
+
+libfolks_dummy_la_CFLAGS = \
+       $(backend_library_cflags) \
+       $(NULL)
+
+libfolks_dummy_la_LIBADD = \
+       $(backend_library_libadd) \
+       $(top_builddir)/folks/libfolks-internal.la \
+       $(NULL)
+
+libfolks_dummy_la_LDFLAGS = \
+       $(backend_library_ldflags) \
+       $(NULL)
+
+
+-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..7b6bc14
--- /dev/null
+++ b/backends/dummy/lib/dummy-backend.vala
@@ -0,0 +1,394 @@
+/*
+ * Copyright (C) 2013 Philip Withnall
+ * Copyright (C) 2013 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:
+ *       Philip Withnall <philip tecnocode co uk>
+ */
+
+using Gee;
+using GLib;
+using Folks;
+
+extern const string BACKEND_NAME;
+
+/**
+ * A backend which allows { link FolksDummy.PersonaStore}s and
+ * { link FolksDummy.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 FolksDummy.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 FolksDummy.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 Folks.Backend.persona_stores} is the set of enabled stores.
+ * libfolks may internally enable or disable stores using
+ * { link Folks.Backend.enable_persona_store},
+ * { link Folks.Backend.disable_persona_store}
+ * and { link Folks.Backend.set_persona_stores}.  The ``register_`` and
+ * ``unregister_`` prefixes are commonly used for backend methods.
+ *
+ * The API in { link FolksDummy} is unstable and may change wildly. It is
+ * designed mostly for use by libfolks unit tests.
+ *
+ * @since UNRELEASED
+ */
+public class FolksDummy.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 ((FolksDummy.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 FolksDummy.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 Folks.Backend.persona_store_added} for each
+   * newly-enabled store. After all addition signals are emitted, a change
+   * notification for { link Folks.Backend.persona_stores} will be emitted (but
+   * only if at least one addition signal is emitted).
+   *
+   * Persona stores are identified by their { link Folks.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 Folks.Backend.persona_store_added} won't be emitted for that store).
+   *
+   * Persona stores must be instances of { link FolksDummy.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)
+        {
+          assert (store is FolksDummy.PersonaStore);
+
+          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 FolksDummy.PersonaStore}s.
+   *
+   * 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 Folks.Backend.enable_persona_store} or
+   * { link Folks.Backend.set_persona_stores}.
+   *
+   * { link Folks.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 Folks.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)
+        {
+          assert (store is FolksDummy.PersonaStore);
+
+          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-full-persona.vala b/backends/dummy/lib/dummy-full-persona.vala
new file mode 100644
index 0000000..8330abb
--- /dev/null
+++ b/backends/dummy/lib/dummy-full-persona.vala
@@ -0,0 +1,1139 @@
+/*
+ * Copyright (C) 2013 Philip Withnall
+ * Copyright (C) 2013 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:
+ *       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 representing a single ‘full’ contact.
+ *
+ * This mocks up a ‘full’ persona which implements all the available property
+ * interfaces provided by libfolks. This is in contrast with
+ * { link FolksDummy.Persona}, which provides a base class implementing none of
+ * libfolks’ interfaces.
+ *
+ * The full dummy persona can be used to simulate a persona from most libfolks
+ * backends, if writing a custom { link FolksDummy.Persona} subclass is not an
+ * option.
+ *
+ * There are two sides to this class’ interface: the normal methods required by
+ * the libfolks ‘details’ interfaces, such as
+ * { link Folks.GenderDetails.change_gender},
+ * and the backend methods which should be called by test driver code to
+ * simulate changes in the backing store providing this persona, such as
+ * { link FullPersona.update_gender}. For example, test driver code should call
+ * { link FullPersona.update_nickname} to simulate the user editing a contact’s
+ * nickname in an online address book which is being exposed to libfolks. The
+ * ``update_``, ``register_`` and ``unregister_`` prefixes are commonly used for
+ * backend methods.
+ *
+ * The API in { link FolksDummy} is unstable and may change wildly. It is
+ * designed mostly for use by libfolks unit tests.
+ *
+ * @since UNRELEASED
+ */
+public class FolksDummy.FullPersona : FolksDummy.Persona,
+    AntiLinkable,
+    AvatarDetails,
+    BirthdayDetails,
+    EmailDetails,
+    FavouriteDetails,
+    GenderDetails,
+    GroupDetails,
+    ImDetails,
+    LocalIdDetails,
+    NameDetails,
+    NoteDetails,
+    PhoneDetails,
+    RoleDetails,
+    UrlDetails,
+    PostalAddressDetails,
+    WebServiceDetails
+{
+  /**
+   * Create a new ‘full’ persona.
+   *
+   * Create a new persona for the { link FolksDummy.PersonaStore} ``store``,
+   * with the given construct-only properties.
+   *
+   * @param store the store which will contain the persona
+   * @param contact_id a unique free-form string identifier for the persona
+   * @param is_user ``true`` if the persona represents the user, ``false``
+   * otherwise
+   * @param linkable_properties an array of names of the properties which should
+   * be used for linking this persona to others
+   *
+   * @since UNRELEASED
+   */
+  public FullPersona (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 (bday);
+        });
+    }
+
+  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> ();
+
+      var iter = input_multi_map.map_iterator ();
+      while (iter.next () == true)
+          output_multi_map.set (iter.get_key (), iter.get_value ());
+
+      return output_multi_map;
+    }
+
+  /**
+   * Update persona's gender.
+   *
+   * This simulates a backing-store-side update of the persona's
+   * { link Folks.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 Folks.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 Folks.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 Folks.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 Folks.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 Folks.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 Folks.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 Folks.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 Folks.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 Folks.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 Folks.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 Folks.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 Folks.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 Folks.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 Folks.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 Folks.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 Folks.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)
+    {
+      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 Folks.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 Folks.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..8dcdf77
--- /dev/null
+++ b/backends/dummy/lib/dummy-persona-store.vala
@@ -0,0 +1,1079 @@
+/*
+ * Copyright (C) 2013 Philip Withnall
+ * Copyright (C) 2013 Canonical Ltd
+ * Copyright (C) 2013 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:
+ *       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 FolksDummy.Persona}s to be
+ * programmatically created and manipulated, for the purposes of testing the
+ * core of libfolks itself. This should not be used in user-visible
+ * applications.
+ *
+ * There are two sides to this class’ interface: the methods and properties
+ * declared by { link Folks.PersonaStore}, which form the normal libfolks
+ * persona store API; and the mock methods and properties (such as
+ * { link FolksDummy.PersonaStore.add_persona_from_details_mock}) which are
+ * intended to be used by test driver code to simulate the behaviour of a real
+ * backing store. Calls to these mock methods effect state changes in the store
+ * which are visible in the normal libfolks API. The ``update_``, ``register_``
+ * and ``unregister_`` prefixes and the ``mock`` suffix are commonly used for
+ * backing store methods.
+ *
+ * The main action performed with a dummy persona store is to change its set of
+ * personas, adding and removing them dynamically to test client-side behaviour.
+ * The client-side APIs ({ link Folks.PersonaStore.add_persona_from_details} and
+ * { link Folks.PersonaStore.remove_persona}) should //not// be used for this.
+ * Instead, the mock APIs should be used:
+ * { link FolksDummy.PersonaStore.freeze_personas_changed},
+ * { link FolksDummy.PersonaStore.register_personas},
+ * { link FolksDummy.PersonaStore.unregister_personas} and
+ * { link FolksDummy.PersonaStore.thaw_personas_changed}. These can be used to
+ * build up complex { link Folks.PersonaStore.personas_changed} signal
+ * emissions, which are only emitted after the final call to
+ * { link FolksDummy.PersonaStore.thaw_personas_changed}.
+ *
+ * The API in { link FolksDummy} is unstable and may change wildly. It is
+ * designed mostly for use by libfolks unit tests.
+ *
+ * @since UNRELEASED
+ */
+public class FolksDummy.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;
+
+  /* Personas which have been registered but not yet emitted in a
+   * personas-changed signal. */
+  private HashSet<Persona> _pending_persona_registrations;
+
+  /* Personas which have been unregistered but not yet emitted in a
+   * personas-changed signal. */
+  private HashSet<Persona> _pending_persona_unregistrations;
+
+  /* Freeze counter for persona changes: personas-changed is only emitted when
+   * this is 0. */
+  private uint _personas_changed_frozen = 0;
+
+  /**
+   * 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 FolksDummy.PersonaStore.register_personas} to add some, then call
+   * { link FolksDummy.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;
+      this._pending_persona_registrations = new HashSet<Persona> ();
+      this._pending_persona_unregistrations = new HashSet<Persona> ();
+    }
+
+  /**
+   * 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}.
+   *
+   * @param details key–value pairs giving the new persona’s details
+   * @throws Folks.PersonaStoreError.STORE_OFFLINE if the store hasn’t been
+   * prepared
+   * @throws Folks.PersonaStoreError.CREATE_FAILED if creating the persona in
+   * the dummy 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 FolksDummy.Persona;
+      assert (persona != null);
+      persona.update_writeable_properties (this.always_writeable_properties);
+
+      unowned Value? v;
+
+      try
+        {
+          v = details.lookup (
+              Folks.PersonaStore.detail_key (PersonaDetail.FULL_NAME));
+          var p_name = persona as NameDetails;
+          if (p_name != null && v != null)
+            {
+              string full_name = ((!) v).get_string () ?? "";
+              yield p_name.change_full_name (full_name);
+            }
+
+          v = details.lookup (
+              Folks.PersonaStore.detail_key (PersonaDetail.STRUCTURED_NAME));
+          if (p_name != null && v != null)
+            {
+              var sname = (StructuredName) ((!) v).get_object ();
+              if (sname != null)
+                  yield p_name.change_structured_name (sname);
+            }
+
+          v = details.lookup (
+              Folks.PersonaStore.detail_key (PersonaDetail.NICKNAME));
+          if (p_name != null && v != null)
+            {
+              string nickname = ((!) v).get_string () ?? "";
+              yield p_name.change_nickname (nickname);
+            }
+
+          v = details.lookup (
+              Folks.PersonaStore.detail_key (PersonaDetail.EMAIL_ADDRESSES));
+          var p_email = persona as EmailDetails;
+          if (p_email != null && v != null)
+            {
+              var email_addresses = (Set<EmailFieldDetails>) ((!) v).get_object ();
+              if (email_addresses != null)
+                  yield p_email.change_email_addresses (email_addresses);
+            }
+
+          v = details.lookup (
+              Folks.PersonaStore.detail_key (PersonaDetail.AVATAR));
+          var p_avatar = persona as AvatarDetails;
+          if (p_avatar != null && v != null)
+            {
+              var avatar = (LoadableIcon?) ((!) v).get_object ();
+              if (avatar != null)
+                  yield p_avatar.change_avatar (avatar);
+            }
+
+          v = details.lookup (
+              Folks.PersonaStore.detail_key (PersonaDetail.IM_ADDRESSES));
+          var p_im = persona as ImDetails;
+          if (p_im != null && v != null)
+            {
+              var im_addresses =
+                  (MultiMap<string,ImFieldDetails>) ((!) v).get_object ();
+              if (im_addresses != null)
+                  yield p_im.change_im_addresses (im_addresses);
+            }
+
+          v = details.lookup (
+              Folks.PersonaStore.detail_key (PersonaDetail.PHONE_NUMBERS));
+          var p_phone = persona as PhoneDetails;
+          if (p_phone != null && v != null)
+            {
+              var phone_numbers = (Set<PhoneFieldDetails>) ((!) v).get_object ();
+              if (phone_numbers != null)
+                  yield p_phone.change_phone_numbers (phone_numbers);
+            }
+
+          v = details.lookup (
+              Folks.PersonaStore.detail_key (PersonaDetail.POSTAL_ADDRESSES));
+          var p_postal = persona as PostalAddressDetails;
+          if (p_postal != null && v != null)
+            {
+              var postal_fds =
+                  (Set<PostalAddressFieldDetails>) ((!) v).get_object ();
+              if (postal_fds != null)
+                  yield p_postal.change_postal_addresses (postal_fds);
+            }
+
+          v = details.lookup (
+              Folks.PersonaStore.detail_key (PersonaDetail.LOCAL_IDS));
+          var p_local = persona as LocalIdDetails;
+          if (p_local != null && v != null)
+            {
+              var local_ids = (Set<string>) ((!) v).get_object ();
+              if (local_ids != null)
+                  yield p_local.change_local_ids (local_ids);
+            }
+
+          v = details.lookup (
+              Folks.PersonaStore.detail_key (
+                  PersonaDetail.WEB_SERVICE_ADDRESSES));
+          var p_web = persona as WebServiceDetails;
+          if (p_web != null && v != null)
+            {
+              var addrs =
+                  (HashMultiMap<string, WebServiceFieldDetails>)
+                      ((!) v).get_object ();
+              if (addrs != null)
+                  yield p_web.change_web_service_addresses (addrs);
+            }
+
+          v = details.lookup (
+              Folks.PersonaStore.detail_key (PersonaDetail.NOTES));
+          var p_note = persona as NoteDetails;
+          if (p_note != null && v != null)
+            {
+              var notes = (Gee.HashSet<NoteFieldDetails>) ((!) v).get_object ();
+              if (notes != null)
+                  yield p_note.change_notes (notes);
+            }
+
+          v = details.lookup (
+              Folks.PersonaStore.detail_key (PersonaDetail.GENDER));
+          var p_gender = persona as GenderDetails;
+          if (p_gender != null && v != null)
+            {
+              var gender = (Gender) ((!) v).get_enum ();
+              yield p_gender.change_gender (gender);
+            }
+
+          v = details.lookup (
+              Folks.PersonaStore.detail_key (PersonaDetail.URLS));
+          var p_url = persona as UrlDetails;
+          if (p_url != null && v != null)
+            {
+              var urls = (Set<UrlFieldDetails>) ((!) v).get_object ();
+              if (urls != null)
+                  yield p_url.change_urls (urls);
+            }
+
+          v = details.lookup (
+              Folks.PersonaStore.detail_key (PersonaDetail.BIRTHDAY));
+          var p_birthday = persona as BirthdayDetails;
+          if (p_birthday != null && v != null)
+            {
+              var birthday = (DateTime?) ((!) v).get_boxed ();
+              if (birthday != null)
+                  yield p_birthday.change_birthday (birthday);
+            }
+
+          v = details.lookup (
+              Folks.PersonaStore.detail_key (PersonaDetail.ROLES));
+          var p_role = persona as RoleDetails;
+          if (p_role != null && v != null)
+            {
+              var roles = (Set<RoleFieldDetails>) ((!) v).get_object ();
+              if (roles != null)
+                  yield p_role.change_roles (roles);
+            }
+
+          v = details.lookup (
+              Folks.PersonaStore.detail_key (PersonaDetail.IS_FAVOURITE));
+          var p_favourite = persona as FavouriteDetails;
+          if (p_favourite != null && v != null)
+            {
+              bool is_fav = ((!) v).get_boolean ();
+              yield p_favourite.change_is_favourite (is_fav);
+            }
+        }
+      catch (PropertyError e1)
+        {
+          throw new PersonaStoreError.CREATE_FAILED (
+              "Setting a property on the new persona failed: %s", e1.message);
+        }
+
+      /* Allow the caller to inject failures and delays into
+       * add_persona_from_details() by providing a mock function. */
+      if (this.add_persona_from_details_mock != null)
+        {
+          var delay = this.add_persona_from_details_mock (persona);
+          yield this._implement_mock_delay (delay);
+        }
+
+      /* 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;
+    }
+
+  /**
+   * 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 FolksDummy.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 and delays. */
+      if (this.remove_persona_mock != null)
+        {
+          var delay = this.remove_persona_mock ((FolksDummy.Persona) persona);
+          yield this._implement_mock_delay (delay);
+        }
+
+      Persona? _persona = this._personas.get (persona.iid);
+      if (_persona != null)
+        {
+          this._personas.unset (persona.iid);
+
+          /* Handle the case where a contact is removed while persona changes
+           * are frozen. */
+          this._pending_persona_registrations.remove ((!) _persona);
+          this._pending_persona_unregistrations.remove ((!) _persona);
+
+          /* Notify of the removal. */
+          var removed_personas = new HashSet<Folks.Persona> ();
+          removed_personas.add ((!) persona);
+          this._emit_personas_changed (null, removed_personas);
+        }
+    }
+
+  /**
+   * 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 and delays. */
+          if (this.prepare_mock != null)
+            {
+              var delay = this.prepare_mock ();
+              yield this._implement_mock_delay (delay);
+            }
+
+          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.
+   */
+
+
+  /**
+   * Delay for the given number of milliseconds.
+   *
+   * This implements an asynchronous delay (which should be yielded on) until
+   * the given number of milliseconds has elapsed.
+   *
+   * If ``delay`` is negative, this function returns immediately. If it is
+   * zero, this function returns in an idle callback.
+   *
+   * @param delay number of milliseconds to delay for
+   *
+   * @since UNRELEASED
+   */
+  private async void _implement_mock_delay (int delay)
+    {
+      if (delay < 0)
+        {
+          /* No delay. */
+          return;
+        }
+      else if (delay == 0)
+        {
+          /* Idle delay. */
+          Idle.add (() =>
+            {
+              this._implement_mock_delay.callback ();
+              return false;
+            });
+
+          yield;
+        }
+      else
+        {
+          /* Timed delay. */
+          Timeout.add (delay, () =>
+            {
+              this._implement_mock_delay.callback ();
+              return false;
+            });
+
+          yield;
+        }
+    }
+
+  /**
+   * Type of a mock function for
+   * { link Folks.PersonaStore.add_persona_from_details}.
+   *
+   * See { link FolksDummy.PersonaStore.add_persona_from_details_mock}.
+   *
+   * @param persona the persona being added to the store, as constructed from
+   * the details passed to { link Folks.PersonaStore.add_persona_from_details}.
+   * @throws PersonaStoreError to be thrown from
+   * { link Folks.PersonaStore.add_persona_from_details}
+   * @return delay to apply to the add persona operation (negative delays
+   * complete synchronously; zero delays complete in an idle callback; positive
+   * delays complete after that many milliseconds)
+   *
+   * @since UNRELEASED
+   */
+  public delegate int AddPersonaFromDetailsMock (Persona persona)
+      throws PersonaStoreError;
+
+  /**
+   * Mock function for { link Folks.PersonaStore.add_persona_from_details}.
+   *
+   * This function is called whenever this store's
+   * { link Folks.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 Folks.PersonaStore.add_persona_from_details}.
+   *
+   * The value returned by this function gives a delay which is imposed for
+   * completion of the { link Folks.PersonaStore.add_persona_from_details} call.
+   * Negative or zero delays
+   * result in completion in an idle callback, and positive delays result in
+   * completion after that many milliseconds.
+   *
+   * If this is ``null``, all calls to
+   * { link Folks.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 Folks.PersonaStore.add_persona_from_details}.
+   *
+   * @since UNRELEASED
+   */
+  public unowned AddPersonaFromDetailsMock? add_persona_from_details_mock
+    {
+      get; set; default = null;
+    }
+
+  /**
+   * Type of a mock function for { link Folks.PersonaStore.remove_persona}.
+   *
+   * See { link FolksDummy.PersonaStore.remove_persona_mock}.
+   *
+   * @param persona the persona being removed from the store
+   * @throws PersonaStoreError to be thrown from
+   * { link Folks.PersonaStore.remove_persona}
+   * @return delay to apply to the remove persona operation (negative and zero
+   * delays complete in an idle callback; positive
+   * delays complete after that many milliseconds)
+   *
+   * @since UNRELEASED
+   */
+  public delegate int RemovePersonaMock (Persona persona)
+      throws PersonaStoreError;
+
+  /**
+   * Mock function for { link Folks.PersonaStore.remove_persona}.
+   *
+   * This function is called whenever this store's
+   * { link Folks.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 Folks.PersonaStore.remove_persona}.
+   *
+   * See { link FolksDummy.PersonaStore.add_persona_from_details_mock}.
+   *
+   * This mock function may be changed at any time; changes will take effect for
+   * the next call to { link Folks.PersonaStore.remove_persona}.
+   *
+   * @since UNRELEASED
+   */
+  public unowned RemovePersonaMock? remove_persona_mock
+    {
+      get; set; default = null;
+    }
+
+  /**
+   * Type of a mock function for { link Folks.PersonaStore.prepare}.
+   *
+   * See { link FolksDummy.PersonaStore.prepare_mock}.
+   *
+   * @throws PersonaStoreError to be thrown from
+   * { link Folks.PersonaStore.prepare}
+   * @return delay to apply to the prepare operation (negative and zero delays
+   * complete in an idle callback; positive
+   * delays complete after that many milliseconds)
+   *
+   * @since UNRELEASED
+   */
+  public delegate int PrepareMock () throws PersonaStoreError;
+
+  /**
+   * Mock function for { link Folks.PersonaStore.prepare}.
+   *
+   * This function is called whenever this store's
+   * { link Folks.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 Folks.PersonaStore.prepare} will return immediately without calling
+   * this mock function). This is useful for testing error handling of calls to
+   * { link Folks.PersonaStore.prepare}.
+   *
+   * See { link FolksDummy.PersonaStore.add_persona_from_details_mock}.
+   *
+   * This mock function may be changed at any time; changes will take effect for
+   * the next call to { link Folks.PersonaStore.prepare}.
+   *
+   * @since UNRELEASED
+   */
+  public unowned PrepareMock? prepare_mock
+    {
+      get; set; default = null;
+    }
+
+  private Type _persona_type = typeof (FolksDummy.Persona);
+
+  /**
+   * Type of programmatically created personas.
+   *
+   * This is the type used to create new personas when
+   * { link Folks.PersonaStore.add_persona_from_details} is called. It must be a
+   * subtype of { link FolksDummy.Persona}.
+   *
+   * This may be modified at any time, with modifications taking effect for the
+   * next call to { link Folks.PersonaStore.add_persona_from_details} or
+   * { link FolksDummy.PersonaStore.register_personas}.
+   *
+   * @since UNRELEASED
+   */
+  public Type persona_type
+    {
+      get { return this._persona_type; }
+      set
+        {
+          assert (value.is_a (typeof (FolksDummy.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 Folks.PersonaStore.can_add_personas},
+   * { link Folks.PersonaStore.can_alias_personas} and
+   * { link Folks.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 update_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 ();
+    }
+
+  /**
+   * Freeze persona changes in the store.
+   *
+   * This freezes externally-visible changes to the set of personas in the store
+   * until { link FolksDummy.PersonaStore.thaw_personas_changed} is called, at
+   * which point all pending changes are made visible in the
+   * { link Folks.PersonaStore.personas} property and by emitting
+   * { link Folks.PersonaStore.personas_changed}.
+   *
+   * Calls to { link FolksDummy.PersonaStore.freeze_personas_changed} and
+   * { link FolksDummy.PersonaStore.thaw_personas_changed} must be well-nested.
+   * Pending changes will only be committed after the final call to
+   * { link FolksDummy.PersonaStore.thaw_personas_changed}.
+   *
+   * @see PersonaStore.thaw_personas_changed
+   * @since UNRELEASED
+   */
+  public void freeze_personas_changed ()
+    {
+      this._personas_changed_frozen++;
+    }
+
+  /**
+   * Thaw persona changes in the store.
+   *
+   * This thaws externally-visible changes to the set of personas in the store.
+   * If the number of calls to
+   * { link FolksDummy.PersonaStore.thaw_personas_changed} matches the number of
+   * calls to { link FolksDummy.PersonaStore.freeze_personas_changed}, all
+   * pending changes are committed and made externally-visible.
+   *
+   * @see PersonaStore.freeze_personas_changed
+   * @since UNRELEASED
+   */
+  public void thaw_personas_changed ()
+    {
+      assert (this._personas_changed_frozen > 0);
+      this._personas_changed_frozen--;
+
+      if (this._personas_changed_frozen == 0)
+        {
+          /* Emit the queued changes. */
+          this._emit_personas_changed (this._pending_persona_registrations,
+              this._pending_persona_unregistrations);
+
+          this._pending_persona_registrations.clear ();
+          this._pending_persona_unregistrations.clear ();
+        }
+    }
+
+  /**
+   * Register new personas with the persona store.
+   *
+   * This registers a set of personas as if they had just appeared in the
+   * backing store. If the persona store is not frozen (see
+   * { link FolksDummy.PersonaStore.freeze_personas_changed}) the changes are
+   * made externally visible on the store immediately (e.g. in the
+   * { link Folks.PersonaStore.personas} property and through a
+   * { link Folks.PersonaStore.personas_changed} signal). If the store is
+   * frozen, the changes will be pending until the store is next unfrozen.
+   *
+   * All elements in the @personas set be of type
+   * { link FolksDummy.PersonaStore.persona_type}.
+   *
+   * @param personas set of personas to register
+   *
+   * @since UNRELEASED
+   */
+  public void register_personas (Set<Persona> personas)
+    {
+      Set<Persona> added_personas;
+      var emit_notifications = (this._personas_changed_frozen == 0);
+
+      /* If the persona store has persona changes frozen, queue up the
+       * personas and emit a notification about them later. */
+      if (emit_notifications == false)
+          added_personas = this._pending_persona_registrations;
+      else
+          added_personas = new HashSet<Persona> ();
+
+      foreach (var persona in personas)
+        {
+          assert (persona.get_type ().is_a (this._persona_type));
+
+          /* Handle the case where a persona is unregistered while the store is
+           * frozen, then registered again before it's unfrozen. */
+          if (this._pending_persona_unregistrations.remove (persona))
+              this._personas.unset (persona.iid);
+
+          if (this._personas.has_key (persona.iid))
+              continue;
+
+          added_personas.add (persona);
+          if (emit_notifications == true)
+              this._personas.set (persona.iid, persona);
+        }
+
+      if (added_personas.size > 0 && emit_notifications == true)
+          this._emit_personas_changed (added_personas, null);
+    }
+
+  /**
+   * Unregister existing personas with the persona store.
+   *
+   * This unregisters a set of personas as if they had just disappeared from the
+   * backing store. If the persona store is not frozen (see
+   * { link FolksDummy.PersonaStore.freeze_personas_changed}) the changes are
+   * made externally visible on the store immediately (e.g. in the
+   * { link Folks.PersonaStore.personas} property and through a
+   * { link Folks.PersonaStore.personas_changed} signal). If the store is
+   * frozen, the changes will be pending until the store is next unfrozen.
+   *
+   * @param personas set of personas to unregister
+   *
+   * @since UNRELEASED
+   */
+  public void unregister_personas (Set<Persona> personas)
+    {
+      Set<Persona> removed_personas;
+      var emit_notifications = (this._personas_changed_frozen == 0);
+
+      /* If the persona store has persona changes frozen, queue up the
+       * personas and emit a notification about them later. */
+      if (emit_notifications == false)
+          removed_personas = this._pending_persona_unregistrations;
+      else
+          removed_personas = new HashSet<Persona> ();
+
+      foreach (var _persona in personas)
+        {
+          /* Handle the case where a persona is registered while the store is
+           * frozen, then unregistered before it's unfrozen. */
+          this._pending_persona_registrations.remove (_persona);
+
+          Persona? persona = this._personas.get (_persona.iid);
+          if (persona == null)
+              continue;
+
+          removed_personas.add ((!) persona);
+        }
+
+      /* Modify this._personas afterwards, just in case
+       * personas == this._personas. */
+       if (removed_personas.size > 0 && emit_notifications == true)
+         {
+           foreach (var _persona in removed_personas)
+                this._personas.unset (_persona.iid);
+
+           this._emit_personas_changed (null, removed_personas);
+         }
+    }
+
+  /**
+   * Reach quiescence on the store.
+   *
+   * If the { link Folks.PersonaStore.prepare} method has already been called on
+   * the store, this causes the store to signal that it has reached quiescence
+   * immediately. If the store has not yet been prepared, this will set a flag
+   * to ensure that quiescence is reached as soon as
+   * { link Folks.PersonaStore.prepare} is called.
+   *
+   * This must be called before the store will reach quiescence.
+   *
+   * @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)
+        {
+          this._is_quiescent = true;
+          this.notify_property ("is-quiescent");
+        }
+    }
+
+  /**
+   * Update the { link Folks.PersonaStore.is_user_set_default} property.
+   *
+   * Backend method for use by test code to simulate a backing-store-driven
+   * change in the { link Folks.PersonaStore.is_user_set_default} property.
+   *
+   * @param is_user_set_default new value for the property
+   *
+   * @since UNRELEASED
+   */
+  public void update_is_user_set_default (bool is_user_set_default)
+    {
+      /* Implemented as an ‘update_*()’ method to make it more explicit that
+       * this is for test driver use only. */
+      this.is_user_set_default = is_user_set_default;
+    }
+
+  /**
+   * Update the { link Folks.PersonaStore.trust_level} property.
+   *
+   * Backend method for use by test code to simulate a backing-store-driven
+   * change in the { link Folks.PersonaStore.trust_level} property.
+   *
+   * @param trust_level new value for the property
+   *
+   * @since UNRELEASED
+   */
+  public void update_trust_level (PersonaStoreTrust trust_level)
+    {
+      /* Implemented as an ‘update_*()’ method to make it more explicit that
+       * this is for test driver use only. */
+      this.trust_level = trust_level;
+    }
+}
diff --git a/backends/dummy/lib/dummy-persona.vala b/backends/dummy/lib/dummy-persona.vala
new file mode 100644
index 0000000..a0d8cbd
--- /dev/null
+++ b/backends/dummy/lib/dummy-persona.vala
@@ -0,0 +1,312 @@
+/*
+ * Copyright (C) 2013 Philip Withnall
+ * Copyright (C) 2013 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:
+ *       Philip Withnall <philip tecnocode co uk>
+ */
+
+using Folks;
+using Gee;
+using GLib;
+
+/**
+ * A persona subclass representing a single contact.
+ *
+ * This mocks up a ‘thin’ persona which implements none of the available
+ * property interfaces provided by libfolks, and is designed as a base class to
+ * be subclassed by personas which will implement one or more of these
+ * interfaces. For example, { link FolksDummy.FullPersona} is one such subclass
+ * which implements all available interfaces.
+ *
+ * There are two sides to this class’ interface: the normal methods required by
+ * { link Folks.Persona}, such as
+ * { link Folks.Persona.linkable_property_to_links},
+ * and the backend methods which should be called by test driver code to
+ * simulate changes in the backing store providing this persona, such as
+ * { link FolksDummy.Persona.update_writeable_properties}. The ``update_``,
+ * ``register_`` and ``unregister_`` prefixes are commonly used for backend
+ * methods.
+ *
+ * All property changes for contact details of subclasses of
+ * { link FolksDummy.Persona} have a configurable delay before taking effect,
+ * which can be controlled by { link FolksDummy.Persona.property_change_delay}.
+ *
+ * The API in { link FolksDummy} is unstable and may change wildly. It is
+ * designed mostly for use by libfolks unit tests.
+ *
+ * @since UNRELEASED
+ */
+public class FolksDummy.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 FolksDummy.PersonaStore} ``store``,
+   * with the given construct-only properties.
+   *
+   * The persona’s { link Folks.Persona.writeable_properties} are initialised to
+   * the given ``store``’s
+   * { link Folks.PersonaStore.always_writeable_properties}. They may be updated
+   * afterwards using { link FolksDummy.Persona.update_writeable_properties}.
+   *
+   * @param store the store which will contain the persona
+   * @param contact_id a unique free-form string identifier for the persona
+   * @param is_user ``true`` if the persona represents the user, ``false``
+   * otherwise
+   * @param linkable_properties an array of names of the properties which should
+   * be used for linking this persona to others
+   *
+   * @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)
+    {
+      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.
+   */
+
+
+  /**
+   * Update the persona’s set of writeable properties.
+   *
+   * Update the { link Folks.Persona.writeable_properties} property to contain
+   * the union of { link Folks.PersonaStore.always_writeable_properties} from
+   * the persona’s store, and the given ``writeable_properties``.
+   *
+   * This should be used to simulate a change in the backing store for the
+   * persona which affects the writeability of one or more of its properties.
+   *
+   * @since UNRELEASED
+   */
+  public void update_writeable_properties (string[] writeable_properties)
+    {
+      var new_writeable_properties = new HashSet<string> ();
+
+      foreach (var p in this.store.always_writeable_properties)
+          new_writeable_properties.add (p);
+      foreach (var p in writeable_properties)
+          new_writeable_properties.add (p);
+
+      /* Check for changes. */
+      var changed = false;
+
+      if (this._writeable_properties.length != new_writeable_properties.size)
+        {
+          changed = true;
+        }
+      else
+        {
+          foreach (var p in this._writeable_properties)
+            {
+              if (new_writeable_properties.contains (p) == false)
+                {
+                  changed = true;
+                  break;
+                }
+            }
+        }
+
+      if (changed == true)
+        {
+          this._writeable_properties = new_writeable_properties.to_array ();
+          this.notify_property ("writeable-properties");
+        }
+    }
+
+  /**
+   * Delay between property changes and notifications.
+   *
+   * This sets an optional delay between client code requesting a property
+   * change (e.g. by calling { link Folks.NameDetails.change_nickname}) and the
+   * property change taking place and a { link Object.notify} signal being
+   * emitted for it.
+   *
+   * Delays are in milliseconds. Negative delays mean that property change
+   * notifications happen synchronously in the change method. A delay of 0
+   * means that property change notifications happen in an idle callback
+   * immediately after the change method. A positive delay means that property
+   * change notifications happen that many milliseconds after the change method
+   * is called.
+   *
+   * @since UNRELEASED
+   */
+  protected int property_change_delay { get; set; }
+
+  /**
+   * Callback to effect a property change in a backing store.
+   *
+   * This is called by { link FolksDummy.Persona.change_property} after the
+   * { link FolksDummy.Persona.property_change_delay} has expired. It must
+   * effect the property change in the simulated backing store, for example by
+   * calling an ‘update’ method such as
+   * { link FolksDummy.FullPersona.update_nickname}.
+   *
+   * @since UNRELEASED
+   */
+  protected delegate void ChangePropertyCallback ();
+
+  /**
+   * Change a property in the simulated backing store.
+   *
+   * This triggers a property change in the simulated backing store, applying
+   * the current { link FolksDummy.Persona.property_change_delay} before calling
+   * the given ``callback`` which should actually effect the property change.
+   *
+   * @param property_name name of the property being changed
+   * @param callback callback to call once the change delay has passed
+   * @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..03cd154
--- /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/dummy -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 79914c0..4ea9298 100644
--- a/configure.ac
+++ b/configure.ac
@@ -49,6 +49,10 @@ m4_define([folks_tracker_lt_current], [41])
 m4_define([folks_tracker_lt_revision], [1])
 m4_define([folks_tracker_lt_age], [16])
 
+m4_define([folks_dummy_lt_current], [41])
+m4_define([folks_dummy_lt_revision], [1])
+m4_define([folks_dummy_lt_age], [16])
+
 # Display the nano_version only if it's not '0'
 m4_define([folks_base_version],
           folks_major_version.folks_minor_version.folks_micro_version)
@@ -111,6 +115,9 @@ AC_SUBST([FOLKS_TELEPATHY_LT_VERSION])
 FOLKS_TRACKER_LT_VERSION=folks_tracker_lt_current:folks_tracker_lt_revision:folks_tracker_lt_age
 AC_SUBST([FOLKS_TRACKER_LT_VERSION])
 
+FOLKS_DUMMY_LT_VERSION=folks_dummy_lt_current:folks_dummy_lt_revision:folks_dummy_lt_age
+AC_SUBST([FOLKS_DUMMY_LT_VERSION])
+
 # Core library API version (potentially used for parallel installation)
 # When updating this, don’t forget to update the backend library API versions
 # below.
@@ -126,6 +133,7 @@ AC_SUBST([FOLKS_EDS_API_VERSION], [0.6])
 AC_SUBST([FOLKS_LIBSOCIALWEB_API_VERSION], [0.6])
 AC_SUBST([FOLKS_TELEPATHY_API_VERSION], [0.6])
 AC_SUBST([FOLKS_TRACKER_API_VERSION], [0.6])
+AC_SUBST([FOLKS_DUMMY_API_VERSION], [0.6])
 
 # Backend configuration
 AC_ARG_ENABLE(tracker-backend,
@@ -358,6 +366,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 \
@@ -367,6 +377,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 \
@@ -714,11 +725,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
@@ -739,6 +754,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
@@ -746,6 +762,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/docs/Makefile.am b/docs/Makefile.am
index d94b21a..efe547e 100644
--- a/docs/Makefile.am
+++ b/docs/Makefile.am
@@ -101,6 +101,67 @@ folks-gtk-doc.stamp: $(folks_doc_files) $(folks_wiki_files)
        @touch $@
 
 #####################
+# Dummy docs #
+#####################
+
+folksdummydocdir=$(datadir)/devhelp/references/folks-dummy
+folksdummygtkdocdir=$(datadir)/gtk-doc/html/folks-dummy
+
+folksdummydoc_DATA = \
+       folks-dummy/folks-dummy/folks-dummy.devhelp2 \
+       $(addprefix folks-dummy/folks-dummy/,$(common_doc_files))\
+       $(NULL)
+
+folksdummygtkdoc_DATA = gtk-doc/folks-dummy/html/*
+
+$(folksdummydoc_DATA): \
+       folks-dummy-doc.stamp \
+       $(NULL)
+$(folksdummygtkdoc_DATA): folks-dummy-gtk-doc.stamp
+
+folks_dummy_doc_files = $(top_srcdir)/backends/dummy/lib/*.vala
+folks_dummy_doc_deps = \
+       gobject-2.0 \
+       gio-2.0 \
+       gee-0.8 \
+       folks \
+       folks-internal \
+       $(NULL)
+valadoc_flags_folks_dummy = \
+       $(valadoc_flags) \
+       -X $(top_srcdir)/backends/dummy/lib/folks/folks-dummy.h \
+       $(addprefix --pkg=,$(folks_dummy_doc_deps)) \
+       --vapidir=$(top_srcdir)/folks \
+       --vapidir=$(top_srcdir)/backends/dummy/lib \
+       $(folks_dummy_doc_files) \
+       $(NULL)
+
+folks-dummy-doc.stamp: $(folks_dummy_doc_files)
+       $(AM_V_GEN)$(VALADOC) \
+               -o folks-dummy/ \
+               --doclet=devhelp \
+               $(valadoc_flags_folks_dummy) \
+               $(NULL)
+       @touch $@
+
+folks-dummy-gtk-doc.stamp: $(folks_dummy_doc_files)
+       $(AM_V_GEN)$(VALADOC) \
+               -X -l -X $(top_builddir)/folks/.libs/libfolks.so \
+               -X -l -X $(top_builddir)/backends/dummy/lib/.libs/libfolks-dummy.so \
+               -o gtk-doc/folks-dummy \
+               --doclet=gtkdoc \
+               $(valadoc_flags_folks_dummy) \
+               $(NULL)
+       @touch $@
+
+distclean_dirs += folks-dummy
+docs += \
+       folks-dummy-doc \
+       folks-dummy-gtk-doc \
+       $(NULL)
+upload_docs += upload-docs-c-folks-dummy upload-docs-vala-folks-dummy
+
+#####################
 # Telepathy docs #
 #####################
 folkstelepathydocdir=$(datadir)/devhelp/references/folks-telepathy
diff --git a/tests/Makefile.am b/tests/Makefile.am
index fc3011e..2fad8ce 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..90fd2d5
--- /dev/null
+++ b/tests/dummy/Makefile.am
@@ -0,0 +1,41 @@
+include $(top_srcdir)/tests/test.mk
+
+AM_VALAFLAGS = \
+       $(test_valaflags) \
+       --vapidir=$(top_srcdir)/tests/lib/dummy \
+       --pkg dummy-test \
+       $(NULL)
+
+AM_CPPFLAGS = \
+       $(test_cppflags) \
+       -I$(top_srcdir)/tests/lib/dummy \
+       $(NULL)
+
+AM_CFLAGS = \
+       $(test_cflags) \
+       $(NULL)
+
+LDADD = \
+       $(test_ldadd) \
+       $(top_builddir)/tests/lib/dummy/libdummy-test.la \
+       $(NULL)
+
+# in order from least to most complex
+TESTS = \
+       individual-retrieval \
+       add-persona \
+       $(NULL)
+
+noinst_PROGRAMS = $(TESTS)
+
+individual_retrieval_SOURCES = \
+       individual-retrieval.vala \
+       $(NULL)
+
+add_persona_SOURCES = \
+       add-persona.vala \
+       $(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..c354ad7
--- /dev/null
+++ b/tests/dummy/add-persona.vala
@@ -0,0 +1,449 @@
+/*
+ * Copyright (C) 2011 Collabora Ltd.
+   Copyright (C) 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>
+ *          Philip Withnall <philip withnall collabora co uk>
+ *
+ */
+
+using Folks;
+using Gee;
+
+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 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 ("adding a persona", 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);
+      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 = IndividualAggregator.dup ();
+      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.disconnect (this._notify_cb);
+
+              this._properties_found.remove_all ();
+            }
+
+          this._individual_received = i;
+          retval++;
+
+          i.notify.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 ()
+    {
+      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 ()
+    {
+      foreach (var k in this._properties_found.get_keys ())
+        {
+          var v = this._properties_found.lookup (k);
+          if (v == false)
+              return;
+        }
+      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..cb6cd50
--- /dev/null
+++ b/tests/dummy/individual-retrieval.vala
@@ -0,0 +1,176 @@
+/*
+ * Copyright (C) 2011, 2013 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>
+ *          Philip Withnall <philip withnall collabora co uk>
+ */
+
+using Gee;
+using Folks;
+using DummyTest;
+using FolksDummy;
+
+public class IndividualRetrievalTests : DummyTest.TestCase
+{
+  public IndividualRetrievalTests ()
+    {
+      base ("IndividualRetrieval");
+
+      this.add_test ("dummy individuals", this.test_aggregator);
+    }
+
+  private struct PersonaInfo
+    {
+      unowned string contact_id;
+      unowned string full_name;
+      unowned string nickname;
+      unowned string home_email_address;
+      unowned string jabber_im_address;
+      unowned string yahoo_im_address;
+    }
+
+  private const PersonaInfo[] _persona_info =
+    {
+      { "dummy 2", "Rodrigo A", "kiko", "rodrigo gmail com",
+          "rodrigo jabber com", "rodrigo yahoo com" },
+      { "dummy 1", "Renato F", "renatof", "renato gmail com",
+          "renato jabber com", "renato yahoo com" },
+    };
+
+  private static async Folks.Persona _create_persona_from_info (
+      FolksDummy.PersonaStore store, PersonaInfo info)
+    {
+      var p = new FullPersona (store, info.contact_id);
+
+      try
+        {
+          /* Names. */
+          yield p.change_full_name (info.full_name);
+          yield p.change_nickname (info.nickname);
+
+          /* E-mail addresses. */
+          var emails = new HashSet<EmailFieldDetails> (
+              AbstractFieldDetails<string>.hash_static,
+              AbstractFieldDetails<string>.equal_static);
+
+          var email_1 = new EmailFieldDetails (info.home_email_address);
+          email_1.set_parameter (AbstractFieldDetails.PARAM_TYPE,
+              AbstractFieldDetails.PARAM_TYPE_HOME);
+          emails.add (email_1);
+
+          yield p.change_email_addresses (emails);
+
+          /* IM addresses. */
+          var im_fds = new HashMultiMap<string, ImFieldDetails> ();
+          im_fds.set ("jabber", new ImFieldDetails (info.jabber_im_address));
+          im_fds.set ("yahoo", new ImFieldDetails (info.yahoo_im_address));
+
+          yield p.change_im_addresses (im_fds);
+        }
+      catch (Folks.PropertyError e)
+        {
+          error ("Error setting property: %s", e.message);
+        }
+
+      return p;
+    }
+
+  private async void _register_personas ()
+    {
+      var personas = new HashSet<Folks.Persona> ();
+
+      /* Create a set of personas. */
+      foreach (var info in IndividualRetrievalTests._persona_info)
+        {
+          var p =
+              yield IndividualRetrievalTests._create_persona_from_info (
+                  this.dummy_persona_store, info);
+          personas.add (p);
+        }
+
+      /* Register them with the dummy store. */
+      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 F");
+      expected_individuals.add ("Rodrigo A");
+
+      /* Set up the aggregator */
+      var aggregator = IndividualAggregator.dup ();
+      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 ();
+        });
+
+      /* Prepare the aggregator, then instruct the store to reach quiescence,
+       * then register the personas with the store. This should result in an
+       * individuals-changed signal. */
+      aggregator.prepare.begin ((s, r) =>
+        {
+          try
+            {
+              aggregator.prepare.end (r);
+
+              this.dummy_persona_store.reach_quiescence ();
+
+              this._register_personas.begin ((s, r) =>
+                {
+                  this._register_personas.end (r);
+                });
+            }
+          catch (GLib.Error e1)
+            {
+              error ("Failed to prepare aggregator: %s", e1.message);
+            }
+        });
+
+      /* Run the test for a few seconds and fail if the timeout is exceeded. */
+      TestUtils.loop_run_with_timeout (main_loop);
+
+      /* We should have enumerated exactly the individuals in the set */
+      assert (expected_individuals.size == 0);
+    }
+}
+
+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/lib/Makefile.am b/tests/lib/Makefile.am
index 5c9df41..49dde70 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..1161004
--- /dev/null
+++ b/tests/lib/dummy/Makefile.am
@@ -0,0 +1,59 @@
+noinst_LTLIBRARIES = libdummy-test.la
+
+libdummy_test_la_VALAFLAGS = \
+       $(AM_VALAFLAGS) \
+       $(ERROR_VALAFLAGS) \
+       --library dummy-test \
+       --vapi dummy-test.vapi \
+       --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 gobject-2.0 \
+       --pkg gio-2.0 \
+       --pkg gee-0.8 \
+       --pkg folks \
+       --pkg folks-dummy \
+       --pkg folks-test \
+       -g \
+       $(NULL)
+
+libdummy_test_la_SOURCES = \
+       test-case.vala \
+       $(NULL)
+
+libdummy_test_la_CPPFLAGS = \
+       $(AM_CPPFLAGS) \
+       -include $(top_srcdir)/folks/warnings.h \
+       $(NULL)
+
+libdummy_test_la_CFLAGS = \
+       $(AM_CFLAGS) \
+       $(ERROR_CFLAGS) \
+       $(GLIB_CFLAGS) \
+       $(GEE_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 \
+       -I$(top_srcdir)/tests/lib \
+       $(NULL)
+
+libdummy_test_la_LIBADD = \
+       $(top_builddir)/folks/libfolks.la \
+       $(top_builddir)/backends/dummy/lib/libfolks-dummy.la \
+       $(top_builddir)/tests/lib/libfolks-test.la \
+       $(GLIB_LIBS) \
+       $(GEE_LIBS) \
+       $(NULL)
+
+EXTRA_DIST = \
+       dummy-test.vapi \
+       dummy-test.h \
+       $(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..43ea8b2
--- /dev/null
+++ b/tests/lib/dummy/test-case.vala
@@ -0,0 +1,141 @@
+/* test-case.vala
+ *
+ * Copyright © 2013 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, 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>
+ *      Philip Withnall <philip withnall collabora co uk>
+ */
+
+using Folks;
+using Gee;
+
+/**
+ * A test case for the dummy backend, which is configured as the
+ * primary store and as the only backend allowed.
+ *
+ * @since UNRELEASED
+ */
+public class DummyTest.TestCase : Folks.TestCase
+{
+  private BackendStore _backend_store;
+
+  /**
+   * The dummy test backend.
+   *
+   * @since UNRELEASED
+   */
+  public FolksDummy.Backend dummy_backend;
+
+  /**
+   * The default dummy persona store.
+   *
+   * @since UNRELEASED
+   */
+  public FolksDummy.PersonaStore dummy_persona_store;
+
+  /**
+   * Create a new dummy test case.
+   *
+   * @param name test case name
+   *
+   * @since UNRELEASED
+   */
+  public TestCase (string name)
+    {
+      base (name);
+
+      Environment.set_variable ("FOLKS_BACKENDS_ALLOWED", "dummy", true);
+      Environment.set_variable ("FOLKS_PRIMARY_STORE", "dummy", true);
+    }
+
+  /**
+   * { inheritDoc}
+   *
+   * @since UNRELEASED
+   */
+  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 e1)
+              {
+                error ("Failed to initialise backend store: %s", e1.message);
+              }
+        });
+
+      TestUtils.loop_run_with_timeout (main_loop);
+
+      /* Grab the dummy backend. */
+      this.dummy_backend =
+          (FolksDummy.Backend)
+              this._backend_store.dup_backend_by_name ("dummy");
+      assert (this.dummy_backend != null);
+
+      this.configure_primary_store ();
+    }
+
+  /**
+   * { inheritDoc}
+   *
+   * @since UNRELEASED
+   */
+  public virtual void configure_primary_store ()
+    {
+      string[] writable_properties =
+        {
+          Folks.PersonaStore.detail_key (PersonaDetail.BIRTHDAY),
+          Folks.PersonaStore.detail_key (PersonaDetail.EMAIL_ADDRESSES),
+          Folks.PersonaStore.detail_key (PersonaDetail.PHONE_NUMBERS),
+          null
+        };
+
+      /* Create a new persona store. */
+      this.dummy_persona_store =
+          new FolksDummy.PersonaStore ("dummy-store", "Dummy personas",
+              writable_properties);
+      this.dummy_persona_store.persona_type = typeof (FolksDummy.FullPersona);
+
+      /* Register it with the backend. */
+      var persona_stores = new HashSet<PersonaStore> ();
+      persona_stores.add (this.dummy_persona_store);
+      this.dummy_backend.register_persona_stores (persona_stores);
+    }
+
+  /**
+   * { inheritDoc}
+   *
+   * @since UNRELEASED
+   */
+  public override void tear_down ()
+    {
+      this.dummy_persona_store = null;
+      this.dummy_backend = null;
+      this._backend_store = 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)



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