[folks] bluez: Add a test suite



commit 6e85b2bec3ee5511d596e71438e228ce343c663a
Author: Philip Withnall <philip withnall collabora co uk>
Date:   Mon Nov 11 13:44:14 2013 +0000

    bluez: Add a test suite
    
    This adds a test suite for the BlueZ backend, using a python-dbusmock
    mock up of the BlueZ and OBEX D-Bus services.
    
    This requires the latest version of python-dbusmock, plus up-to-date
    versions of GLib and Vala for binding updates. The use of the second and
    third arguments to AM_PROG_VALAC in configure.ac also necessitates use
    of automake 1.12 or newer.
    
    Only a few test cases have been added so far, covering vCard parsing and
    general set up of PersonaStores. Using python-dbusmock it should be easy
    to add more tests covering advanced Bluetooth device
    appearance/disappearance situations in future.
    
    https://bugzilla.gnome.org/show_bug.cgi?id=712274

 autogen.sh                            |   13 --
 configure.ac                          |   25 +++-
 tests/Makefile.am                     |    7 +
 tests/bluez/Makefile.am               |   48 +++++
 tests/bluez/device-properties.vala    |  305 ++++++++++++++++++++++++++++++++
 tests/bluez/individual-retrieval.vala |  232 +++++++++++++++++++++++++
 tests/bluez/vcard-parsing.vala        |  255 +++++++++++++++++++++++++++
 tests/lib/Makefile.am                 |    7 +
 tests/lib/bluez/Makefile.am           |   51 ++++++
 tests/lib/bluez/backend.vala          |  306 +++++++++++++++++++++++++++++++++
 tests/lib/bluez/test-case.vala        |  127 ++++++++++++++
 tests/lib/test-case.vala              |    3 +
 12 files changed, 1365 insertions(+), 14 deletions(-)
---
diff --git a/autogen.sh b/autogen.sh
index b3b63b1..a38c8cb 100755
--- a/autogen.sh
+++ b/autogen.sh
@@ -1,19 +1,6 @@
 #!/bin/sh
 set -e
 
-if test -n "$AUTOMAKE"; then
-    : # don't override an explicit user request
-elif automake-1.11 --version >/dev/null 2>/dev/null && \
-     aclocal-1.11 --version >/dev/null 2>/dev/null; then
-    # If we have automake-1.11, use it. This is the oldest version (=> least
-    # likely to introduce undeclared dependencies) that will give us
-    # --enable-silent-rules support.
-    AUTOMAKE=automake-1.11
-    export AUTOMAKE
-    ACLOCAL=aclocal-1.11
-    export ACLOCAL
-fi
-
 autoreconf -i -f
 intltoolize --force --copy --automake
 
diff --git a/configure.ac b/configure.ac
index 4ea9298..729e971 100644
--- a/configure.ac
+++ b/configure.ac
@@ -79,7 +79,7 @@ AC_CONFIG_MACRO_DIR([m4])
 AC_CONFIG_SRCDIR([Makefile.am])
 AC_CONFIG_HEADERS(config.h)
 AC_CONFIG_SRCDIR([configure.ac])
-AM_INIT_AUTOMAKE([1.11 dist-xz no-define
+AM_INIT_AUTOMAKE([1.12 dist-xz no-define
                   no-dist-gzip tar-ustar -Wno-portability color-tests
                   parallel-tests])
 AM_MAINTAINER_MODE([enable])
@@ -336,8 +336,29 @@ AS_IF([test x$enable_ofono_backend = xyes], [
 
 AS_IF([test x$enable_bluez_backend = xyes], [
         PKG_CHECK_MODULES([EBOOK], [libebook-1.2 >= $EBOOK_REQUIRED])
+
+        # Dependencies for the BlueZ tests
+        PKG_CHECK_MODULES([GLIB_2_39_2], [glib-2.0 >= 2.39.2],
+                          [have_glib_2_39_2=yes], [have_glib_2_39_2=no])
+        AM_PATH_PYTHON([3.0], [have_python=yes], [have_python=no])
+
+        AC_MSG_CHECKING([for python-dbusmock])
+        AS_IF([! $PYTHON -c 'import dbusmock' > /dev/null 2>&1],
+              [have_dbusmock=no], [have_dbusmock=yes])
+        AC_MSG_RESULT([$have_dbusmock])
+
+        AM_PROG_VALAC([0.22.0.45-383d-dirty],
+                      [have_valac_0_22_2=yes], [have_valac_0_22_2=no])
 ])
 
+# The BlueZ tests are conditional on several bleeding-edge dependencies.
+# FIXME: Remove this once things have stabilised a bit.
+AM_CONDITIONAL([HAVE_BLUEZ_TESTS],
+               [test "x$have_glib_2_39_2" = "xyes" -a \
+                     "x$have_python" = "xyes" -a \
+                     "x$have_dbusmock" = "xyes" -a \
+                     "x$have_valac_0_22_2" = "xyes"])
+
 #
 # Vala building options -- allows tarball builds without installing Vala
 #
@@ -751,6 +772,7 @@ AC_CONFIG_FILES([
     docs/Makefile
     po/Makefile.in
     tests/Makefile
+    tests/bluez/Makefile
     tests/data/Makefile
     tests/eds/Makefile
     tests/folks/Makefile
@@ -761,6 +783,7 @@ AC_CONFIG_FILES([
     tests/tracker/Makefile
     tests/lib/Makefile
     tests/lib/folks-test-uninstalled.pc
+    tests/lib/bluez/Makefile
     tests/lib/eds/Makefile
     tests/lib/dummy/Makefile
     tests/lib/key-file/Makefile
diff --git a/tests/Makefile.am b/tests/Makefile.am
index 2fad8ce..3856194 100644
--- a/tests/Makefile.am
+++ b/tests/Makefile.am
@@ -8,6 +8,12 @@ SUBDIRS = \
        tools \
        $(NULL)
 
+if ENABLE_BLUEZ
+if HAVE_BLUEZ_TESTS
+SUBDIRS += bluez
+endif
+endif
+
 if ENABLE_TELEPATHY
 SUBDIRS += folks telepathy
 endif
@@ -34,6 +40,7 @@ DIST_SUBDIRS = \
        dummy \
        eds \
        key-file \
+       bluez \
        telepathy \
        libsocialweb \
        tracker \
diff --git a/tests/bluez/Makefile.am b/tests/bluez/Makefile.am
new file mode 100644
index 0000000..46661fb
--- /dev/null
+++ b/tests/bluez/Makefile.am
@@ -0,0 +1,48 @@
+include $(top_srcdir)/tests/test.mk
+
+AM_VALAFLAGS = \
+       $(test_valaflags) \
+       --vapidir=$(top_srcdir)/tests/lib/bluez \
+       --pkg bluez-test \
+       --pkg folks-generics \
+       $(NULL)
+
+AM_CPPFLAGS = \
+       $(test_cppflags) \
+       -I$(top_srcdir)/tests/lib/bluez \
+       $(NULL)
+
+AM_CFLAGS = \
+       $(test_cflags) \
+       $(NULL)
+
+LDADD = \
+       $(AM_LDADD) \
+       $(test_ldadd) \
+       $(top_builddir)/tests/lib/bluez/libbluez-test.la \
+       $(NULL)
+
+# in order from least to most complex
+noinst_PROGRAMS = \
+       device-properties \
+       individual-retrieval \
+       vcard-parsing \
+       $(NULL)
+
+TESTS = $(noinst_PROGRAMS)
+
+device_properties_SOURCES = \
+       device-properties.vala \
+       $(NULL)
+
+individual_retrieval_SOURCES = \
+       individual-retrieval.vala \
+       $(NULL)
+
+vcard_parsing_SOURCES = \
+       vcard-parsing.vala \
+       $(NULL)
+
+-include $(top_srcdir)/git.mk
+-include $(top_srcdir)/valgrind.mk
+-include $(top_srcdir)/check.mk
diff --git a/tests/bluez/device-properties.vala b/tests/bluez/device-properties.vala
new file mode 100644
index 0000000..19d806a
--- /dev/null
+++ b/tests/bluez/device-properties.vala
@@ -0,0 +1,305 @@
+/*
+ * 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 withnall collabora co uk>
+ */
+
+using Gee;
+using Folks;
+using BluezTest;
+
+public class DevicePropertiesTests : BluezTest.TestCase
+{
+  public DevicePropertiesTests ()
+    {
+      base ("DeviceProperties");
+
+      this.add_test ("device pairing", this.test_device_pairing);
+      this.add_test ("blocked device", this.test_blocked_device);
+      this.add_test ("device alias", this.test_device_alias);
+    }
+
+  /* Start with an unpaired Bluetooth device, and check that it’s not turned
+   * into a PersonaStore. Then pair the device, and check that it is added as
+   * a store. */
+  public void test_device_pairing ()
+    {
+      var main_loop = new GLib.MainLoop (null, false);
+
+      /* Set up a simple unpaired device. */
+      try
+        {
+          this.bluez_backend.mock_bluez.add_adapter ("hci0", "Test System");
+          this.bluez_backend.mock_bluez.add_device ("hci0",
+              this.bluez_backend.primary_device_address, "My Phone");
+        }
+      catch (IOError e1)
+        {
+          error ("Error setting up mock BlueZ device: %s", e1.message);
+        }
+
+      /* Set up its vCard in preparation. */
+      this.bluez_backend.set_simple_device_vcard (
+          "BEGIN:VCARD\n" +
+          "VERSION:3.0\n" +
+          "N:Jones;Pam;Mrs.\n" +
+          "FN:Pam Jones\n" +
+          "TEL:0123456789\n" +
+          "END:VCARD\n");
+
+      /* Set up the aggregator and wait until either quiescence, or the test
+       * times out and fails. Unset the primary store to prevent a warning. */
+      Environment.set_variable ("FOLKS_PRIMARY_STORE", "", true);
+
+      var aggregator = IndividualAggregator.dup ();
+      TestUtils.aggregator_prepare_and_wait_for_quiescence.begin (aggregator,
+          (o, r) =>
+        {
+          try
+            {
+              TestUtils.aggregator_prepare_and_wait_for_quiescence.end (r);
+            }
+          catch (GLib.Error e2)
+            {
+              error ("Error preparing aggregator: %s", e2.message);
+            }
+
+          main_loop.quit ();
+        });
+
+      TestUtils.loop_run_with_timeout (main_loop);
+
+      var real_backend =
+          aggregator.backend_store.dup_backend_by_name ("bluez");
+
+      /* Check there are no individuals and no persona stores. */
+      assert (aggregator.individuals.size == 0);
+      assert (real_backend.persona_stores.size == 0);
+
+      /* Wait for a signal about an added persona store. */
+      TestUtils.aggregator_wait_for_individuals.begin (aggregator,
+          {"Pam Jones"}, {}, (o, r) =>
+        {
+          TestUtils.aggregator_wait_for_individuals.end (r);
+          main_loop.quit ();
+        });
+
+      /* Pair the device. */
+      try
+        {
+          this.bluez_backend.mock_bluez.pair_device ("hci0",
+              this.bluez_backend.primary_device_address);
+        }
+      catch (IOError e4)
+        {
+          error ("Error pairing mock BlueZ device: %s", e4.message);
+        }
+
+      TestUtils.loop_run_with_timeout (main_loop);
+    }
+
+  /* Start with a blocked Bluetooth device, and check it’s not made into a
+   * PersonaStore. Then unblock the device and check a PersonaStore is created.
+   * Then block the device again and check the PersonaStore is removed. */
+  public void test_blocked_device ()
+    {
+      var main_loop = new GLib.MainLoop (null, false);
+
+      /* Set up a simple paired but blocked device. */
+      this.bluez_backend.create_simple_device_with_vcard (
+          "BEGIN:VCARD\n" +
+          "VERSION:3.0\n" +
+          "N:Jones;Pam;Mrs.\n" +
+          "FN:Pam Jones\n" +
+          "TEL:0123456789\n" +
+          "END:VCARD\n");
+
+      try
+        {
+          this.bluez_backend.mock_bluez.block_device ("hci0",
+              this.bluez_backend.primary_device_address);
+        }
+      catch (IOError e1)
+        {
+          error ("Error blocking device: %s", e1.message);
+        }
+
+      /* Set up the aggregator and wait until either quiescence, or the test
+       * times out and fails. Unset the primary store to prevent a warning. */
+      Environment.set_variable ("FOLKS_PRIMARY_STORE", "", true);
+
+      var aggregator = IndividualAggregator.dup ();
+      TestUtils.aggregator_prepare_and_wait_for_quiescence.begin (aggregator,
+          (o, r) =>
+        {
+          try
+            {
+              TestUtils.aggregator_prepare_and_wait_for_quiescence.end (r);
+            }
+          catch (GLib.Error e2)
+            {
+              error ("Error preparing aggregator: %s", e2.message);
+            }
+
+          main_loop.quit ();
+        });
+
+      TestUtils.loop_run_with_timeout (main_loop);
+
+      var real_backend =
+          aggregator.backend_store.dup_backend_by_name ("bluez");
+
+      /* Check there are no individuals and no persona stores. */
+      assert (aggregator.individuals.size == 0);
+      assert (real_backend.persona_stores.size == 0);
+
+      /* Wait for a signal about an added persona store. */
+      TestUtils.aggregator_wait_for_individuals.begin (aggregator,
+          {"Pam Jones"}, {}, (o, r) =>
+        {
+          TestUtils.aggregator_wait_for_individuals.end (r);
+          main_loop.quit ();
+        });
+
+      /* Unblock the device. */
+      try
+        {
+          this.bluez_backend.mock_bluez.pair_device ("hci0",
+              this.bluez_backend.primary_device_address);
+        }
+      catch (IOError e4)
+        {
+          error ("Error blocking device: %s", e4.message);
+        }
+
+      TestUtils.loop_run_with_timeout (main_loop);
+
+      /* Wait for a signal about a removed persona store. */
+      TestUtils.aggregator_wait_for_individuals.begin (aggregator,
+          {}, {"Pam Jones"}, (o, r) =>
+        {
+          TestUtils.aggregator_wait_for_individuals.end (r);
+          main_loop.quit ();
+        });
+
+      /* Block the device again. */
+      try
+        {
+          this.bluez_backend.mock_bluez.block_device ("hci0",
+              this.bluez_backend.primary_device_address);
+        }
+      catch (IOError e5)
+        {
+          error ("Error blocking device again: %s", e5.message);
+        }
+
+      TestUtils.loop_run_with_timeout (main_loop);
+
+      /* Check there are no individuals and no persona stores. */
+      assert (aggregator.individuals.size == 0);
+      assert (real_backend.persona_stores.size == 0);
+    }
+
+  /* Test that changes of a device’s Alias property result in the PersonaStore’s
+   * display-name being updated. */
+  public void test_device_alias ()
+    {
+      /* Set up the backend. */
+      string device_path = "";
+      this.bluez_backend.create_simple_device_with_vcard (
+          "BEGIN:VCARD\n" +
+          "VERSION:3.0\n" +
+          "N:Jones;Pam;Mrs.\n" +
+          "FN:Pam Jones\n" +
+          "TEL:0123456789\n" +
+          "END:VCARD\n",
+          null, out device_path);
+
+      var aggregator = IndividualAggregator.dup ();
+      TestUtils.aggregator_prepare_and_wait_for_individuals_sync_with_timeout (
+          aggregator, {"Pam Jones"});
+
+      /* Check the PersonaStore’s alias. */
+      var real_backend =
+          aggregator.backend_store.dup_backend_by_name ("bluez");
+      assert (real_backend.persona_stores.size == 1);
+
+      var real_store =
+          real_backend.persona_stores.get (
+              this.bluez_backend.primary_device_address);
+
+      /* FIXME: Have to get the display-name this way because
+       * Folks.PersonaStore.display_name is not declared as abstract. */
+      string display_name = "";
+      real_store.get ("display-name", out display_name);
+      assert (display_name == "My Phone");
+
+      /* Change the device’s Alias and see if the display-name changes. */
+      var main_loop = new GLib.MainLoop (null, false);
+      real_store.notify["display-name"].connect ((p) =>
+        {
+          real_store.get ("display-name", out display_name);
+          assert (display_name == "New Alias!");
+          main_loop.quit ();
+        });
+
+      try
+        {
+          Device mock_device =
+              Bus.get_proxy_sync (BusType.SYSTEM, "org.bluez", device_path);
+          org.freedesktop.DBus.Mock mock =
+              Bus.get_proxy_sync (BusType.SYSTEM, "org.bluez", device_path);
+
+          var props = new HashTable<string, Variant> (str_hash, str_equal);
+          props.insert ("Alias", "New Alias!");
+
+          mock_device.alias = "New Alias!";
+          mock.emit_signal ("org.freedesktop.DBus.Properties",
+              "PropertiesChanged", "sa{sv}as",
+              {
+                "org.bluez.Device1",
+                props,
+                new Variant.array (VariantType.STRING, {})
+              });
+        }
+      catch (IOError e1)
+        {
+          error ("Error setting device alias: %s", e1.message);
+        }
+
+      TestUtils.loop_run_with_timeout (main_loop);
+    }
+}
+
+/* Mini-copy of the org-bluez.vala file in the BlueZ backend. */
+[DBus (name = "org.bluez.Device1")]
+public interface Device : Object
+  {
+    [DBus (name = "Alias")]
+    public abstract string alias { owned get; set; }
+  }
+
+public int main (string[] args)
+{
+  Test.init (ref args);
+
+  var tests = new DevicePropertiesTests ();
+  tests.register ();
+  Test.run ();
+  tests.final_tear_down ();
+
+  return 0;
+}
diff --git a/tests/bluez/individual-retrieval.vala b/tests/bluez/individual-retrieval.vala
new file mode 100644
index 0000000..033821e
--- /dev/null
+++ b/tests/bluez/individual-retrieval.vala
@@ -0,0 +1,232 @@
+/*
+ * 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 withnall collabora co uk>
+ */
+
+using Gee;
+using Folks;
+using BluezTest;
+
+public class IndividualRetrievalTests : BluezTest.TestCase
+{
+  public IndividualRetrievalTests ()
+    {
+      base ("IndividualRetrieval");
+
+      this.add_test ("singleton individuals", this.test_singleton_individuals);
+      this.add_test ("empty address book", this.test_empty_address_book);
+      this.add_test ("photos downloaded later",
+          this.test_photos_downloaded_later);
+    }
+
+  /* Test that personas on a pre-existing Bluetooth device are successfully
+   * downloaded and presented as singleton individuals by the aggregator. */
+  public void test_singleton_individuals ()
+    {
+      var main_loop = new GLib.MainLoop (null, false);
+
+      /* Set up the backend. */
+      this.bluez_backend.create_simple_device_with_vcard (
+          "BEGIN:VCARD\n" +
+          "VERSION:3.0\n" +
+          "N:Gump;Forrest;Mr.\n" +
+          "FN:Forrest Gump\n" +
+          "NICKNAME:Fir\n" +
+          "TEL;TYPE=WORK,VOICE:(111) 555-1212\n" +
+          "TEL;TYPE=HOME,VOICE:(404) 555-1212\n" +
+          "EMAIL;TYPE=PREF,INTERNET:forrestgump example com\n" +
+          "URL;TYPE=HOME:http://example.com/\n"; +
+          "END:VCARD\n" +
+          "\n" +
+          "BEGIN:VCARD\n" +
+          "VERSION:3.0\n" +
+          "N:Jones;Pam;Mrs.\n" +
+          "FN:Pam Jones\n" +
+          "TEL:0123456789\n" +
+          "END:VCARD\n");
+
+      /* Set up the aggregator and wait until either the expected persona are
+       * seen, or the test times out and fails. */
+      var aggregator = IndividualAggregator.dup ();
+      TestUtils.aggregator_prepare_and_wait_for_individuals.begin (aggregator,
+          {"Forrest Gump", "Pam Jones"}, (o, r) =>
+        {
+          try
+            {
+              TestUtils.aggregator_prepare_and_wait_for_individuals.end (r);
+            }
+          catch (GLib.Error e1)
+            {
+              error ("Error preparing aggregator: %s", e1.message);
+            }
+
+          main_loop.quit ();
+        });
+
+      TestUtils.loop_run_with_timeout (main_loop);
+    }
+
+  /* Test that an empty address book is handled correctly. */
+  public void test_empty_address_book ()
+    {
+      var main_loop = new GLib.MainLoop (null, false);
+
+      /* Set up the backend with *no* contacts. */
+      this.bluez_backend.create_simple_device_with_vcard ("");
+
+      /* Set up the aggregator and wait until either quiescence, or the test
+       * times out and fails. */
+      var aggregator = IndividualAggregator.dup ();
+      TestUtils.aggregator_prepare_and_wait_for_quiescence.begin (aggregator,
+          (o, r) =>
+        {
+          try
+            {
+              TestUtils.aggregator_prepare_and_wait_for_quiescence.end (r);
+            }
+          catch (GLib.Error e1)
+            {
+              error ("Error preparing aggregator: %s", e1.message);
+            }
+
+          main_loop.quit ();
+        });
+
+      TestUtils.loop_run_with_timeout (main_loop);
+
+      /* Check there are no individuals. */
+      assert (aggregator.individuals.size == 0);
+    }
+
+  /* Test that photos are downloaded in a second sweep of the address book. */
+  public void test_photos_downloaded_later ()
+    {
+      var main_loop = new GLib.MainLoop (null, false);
+
+      /* Set up the backend, at first with a vCard without a photo. */
+      var vcard_signal_id = this.bluez_backend.create_simple_device_with_vcard (
+          "BEGIN:VCARD\n" +
+          "VERSION:3.0\n" +
+          "N:Gump;Forrest;Mr.\n" +
+          "FN:Forrest Gump\n" +
+          "NICKNAME:Fir\n" +
+          "TEL;TYPE=WORK,VOICE:(111) 555-1212\n" +
+          "TEL;TYPE=HOME,VOICE:(404) 555-1212\n" +
+          "EMAIL;TYPE=PREF,INTERNET:forrestgump example com\n" +
+          "URL;TYPE=HOME:http://example.com/\n"; +
+          "END:VCARD\n");
+
+      /* Set up the aggregator and wait until either the expected persona are
+       * seen, or the test times out and fails. */
+      var aggregator = IndividualAggregator.dup ();
+      TestUtils.aggregator_prepare_and_wait_for_individuals.begin (aggregator,
+          {"Forrest Gump"}, (o, r) =>
+        {
+          try
+            {
+              TestUtils.aggregator_prepare_and_wait_for_individuals.end (r);
+            }
+          catch (GLib.Error e1)
+            {
+              error ("Error preparing aggregator: %s", e1.message);
+            }
+
+          main_loop.quit ();
+        });
+
+      TestUtils.loop_run_with_timeout (main_loop);
+
+      /* Re-set the backend to now return a vCard with a photo (and nothing
+       * else). */
+      this.bluez_backend.mock_obex.disconnect (vcard_signal_id);
+      this.bluez_backend.set_simple_device_vcard (
+          "BEGIN:VCARD\n" +
+          "VERSION:3.0\n" +
+          "N:Gump;Forrest;Mr.\n" +
+          "FN:Forrest Gump\n" +
+          "NICKNAME:Fir\n" +
+          "TEL;TYPE=WORK,VOICE:(111) 555-1212\n" +
+          "TEL;TYPE=HOME,VOICE:(404) 555-1212\n" +
+          "EMAIL;TYPE=PREF,INTERNET:forrestgump example com\n" +
+          "URL;TYPE=HOME:http://example.com/\n"; +
+          "PHOTO;TYPE=jpeg;ENCODING=b:" +
+          "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\n" +
+          "END:VCARD\n");
+
+      /* The individual should not have a photo to begin with; wait until one
+       * appears. */
+      assert (aggregator.individuals.size == 1);
+      var iter = aggregator.individuals.map_iterator ();
+      while (iter.next () == true)
+        {
+          var individual = iter.get_value ();
+          assert (individual.avatar == null);
+
+          /* Wait for it to change. Assert that only the avatar changes. */
+          individual.notify.connect ((pspec) =>
+            {
+              assert (pspec.name == "avatar");
+              assert (individual.avatar != null);
+              main_loop.quit ();
+            });
+        }
+
+      /* There’s normally a 5s wait between poll attempts in the backend, but
+       * we set the FOLKS_BLUEZ_TIMEOUT_DIVISOR in the TestCase to reduce
+       * this. */
+      TestUtils.loop_run_with_timeout (main_loop);
+    }
+}
+
+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/bluez/vcard-parsing.vala b/tests/bluez/vcard-parsing.vala
new file mode 100644
index 0000000..f9e9100
--- /dev/null
+++ b/tests/bluez/vcard-parsing.vala
@@ -0,0 +1,255 @@
+/*
+ * 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 withnall collabora co uk>
+ */
+
+using Gee;
+using Folks;
+using BluezTest;
+
+public class VcardParsingTests : BluezTest.TestCase
+{
+  public VcardParsingTests ()
+    {
+      base ("VcardParsing");
+
+      this.add_test ("multiple attributes", this.test_multiple_attributes);
+      this.add_test ("name components", this.test_name_components);
+      this.add_test ("encoding", this.test_encoding);
+    }
+
+  /* Test that vCards containing multiple attributes with the same name (e.g.
+   * multiple phone numbers or e-mail addresses) are parsed correctly. */
+  public void test_multiple_attributes ()
+    {
+      /* Set up the backend. */
+      this.bluez_backend.create_simple_device_with_vcard (
+          "BEGIN:VCARD\n" +
+          "VERSION:3.0\n" +
+          "FN:Forrest Gump\n" +
+          "TEL;TYPE=WORK,VOICE:(111) 555-1212\n" +
+          "TEL;TYPE=HOME,VOICE:(404) 555-1212\n" +
+          "EMAIL;TYPE=PREF,INTERNET:forrestgump example com\n" +
+          "EMAIL:test example com\n" +
+          "URL;TYPE=HOME:http://example.com/\n"; +
+          "URL:http://forest.com/\n"; +
+          "URL:https://test.com/\n"; +
+          "END:VCARD\n");
+
+      /* Set up the aggregator and wait until either the expected persona are
+       * seen, or the test times out and fails. */
+      var aggregator = IndividualAggregator.dup ();
+      TestUtils.aggregator_prepare_and_wait_for_individuals_sync_with_timeout (
+          aggregator, {"Forrest Gump"});
+
+      /* Check the properties of our friend Forrest. */
+      var ind = TestUtils.get_individual_by_name (aggregator, "Forrest Gump");
+
+      var expected_phone_numbers = new SmallSet<PhoneFieldDetails> (
+          AbstractFieldDetails<string>.hash_static,
+          AbstractFieldDetails<string>.equal_static);
+
+      var expected_phone_fd = new PhoneFieldDetails ("(111) 555-1212");
+      expected_phone_fd.add_parameter ("type", "work");
+      expected_phone_fd.add_parameter ("type", "voice");
+      expected_phone_numbers.add (expected_phone_fd);
+
+      expected_phone_fd = new PhoneFieldDetails ("(404) 555-1212");
+      expected_phone_fd.add_parameter ("type", "home");
+      expected_phone_fd.add_parameter ("type", "voice");
+      expected_phone_numbers.add (expected_phone_fd);
+
+      var expected_email_addresses = new SmallSet<EmailFieldDetails> (
+          AbstractFieldDetails<string>.hash_static,
+          AbstractFieldDetails<string>.equal_static);
+
+      var expected_email_fd = new EmailFieldDetails ("forrestgump example com");
+      expected_email_fd.add_parameter ("type", "pref");
+      expected_email_fd.add_parameter ("type", "internet");
+      expected_email_addresses.add (expected_email_fd);
+
+      expected_email_fd = new EmailFieldDetails ("test example com");
+      expected_email_addresses.add (expected_email_fd);
+
+      var expected_uris = new SmallSet<UrlFieldDetails> (
+          AbstractFieldDetails<string>.hash_static,
+          AbstractFieldDetails<string>.equal_static);
+
+      var expected_uri_fd = new UrlFieldDetails ("http://example.com/";);
+      expected_uri_fd.add_parameter ("type", "home");
+      expected_uris.add (expected_uri_fd);
+
+      expected_uri_fd = new UrlFieldDetails ("http://forest.com/";);
+      expected_uris.add (expected_uri_fd);
+
+      expected_uri_fd = new UrlFieldDetails ("https://test.com/";);
+      expected_uris.add (expected_uri_fd);
+
+      assert (Utils.set_afd_equal (ind.phone_numbers, expected_phone_numbers));
+      assert (Utils.set_afd_equal (ind.email_addresses,
+                  expected_email_addresses));
+      assert (Utils.set_afd_equal (ind.urls, expected_uris));
+    }
+
+  /* Test that vCards with different numbers of values for their N (structured
+   * name) attribute are parsed correctly. */
+  public void test_name_components ()
+    {
+      /* Set up the backend. */
+      this.bluez_backend.create_simple_device_with_vcard (
+          /* Valid N attributes. */
+          "BEGIN:VCARD\n" +
+          "VERSION:3.0\n" +
+          "FN:John Public\n" +
+          "N:Public;John;Quinlan;Mr.;Esq.\n" +
+          "END:VCARD\n" +
+          "\n" +
+          "BEGIN:VCARD\n" +
+          "VERSION:3.0\n" +
+          "FN:John Stevenson\n" +
+          "N:Stevenson;John;Philip,Paul;Dr.;Jr.,M.D.,A.C.P.\n" +
+          "END:VCARD\n" +
+          "\n" +
+          "BEGIN:VCARD\n" +
+          "VERSION:3.0\n" +
+          "FN:Franco Dianno\n" +
+          "N:Dianno;Franco;;;\n" +
+          "END:VCARD\n" +
+          "\n" +
+          /* Invalid N attributes (but we should handle them anyway). */
+          "BEGIN:VCARD\n" +
+          "VERSION:3.0\n" +
+          "FN:Amelia Smith\n" +
+          "N:Smith;Amelia;David;Dr.\n" +
+          "END:VCARD\n" +
+          "\n" +
+          "BEGIN:VCARD\n" +
+          "VERSION:3.0\n" +
+          "FN:Sadie Jones\n" +
+          "N:Jones;Sadie;M.\n" +
+          "END:VCARD\n" +
+          "\n" +
+          "BEGIN:VCARD\n" +
+          "VERSION:3.0\n" +
+          "FN:Alex Lawson\n" +
+          "N:Lawson;Alex\n" +
+          "END:VCARD\n");
+
+      /* Set up the aggregator and wait until either the expected persona are
+       * seen, or the test times out and fails. */
+      var aggregator = IndividualAggregator.dup ();
+      TestUtils.aggregator_prepare_and_wait_for_individuals_sync_with_timeout (
+          aggregator,
+          {
+            "John Public",
+            "John Stevenson",
+            "Franco Dianno",
+            "Amelia Smith",
+            "Sadie Jones",
+            "Alex Lawson"
+          });
+
+      /* Check the properties of our individuals. */
+      var ind = TestUtils.get_individual_by_name (aggregator, "John Public");
+      var expected_name =
+          new StructuredName ("Public", "John", "Quinlan", "Mr.", "Esq.");
+      assert (ind.structured_name.equal (expected_name));
+
+      ind = TestUtils.get_individual_by_name (aggregator, "John Stevenson");
+      expected_name =
+          new StructuredName ("Stevenson", "John", "Philip,Paul", "Dr.",
+              "Jr.,M.D.,A.C.P.");
+      assert (ind.structured_name.equal (expected_name));
+
+      ind = TestUtils.get_individual_by_name (aggregator, "Franco Dianno");
+      expected_name = new StructuredName ("Dianno", "Franco", null, null, null);
+      assert (ind.structured_name.equal (expected_name));
+
+      ind = TestUtils.get_individual_by_name (aggregator, "Amelia Smith");
+      expected_name =
+          new StructuredName ("Smith", "Amelia", "David", "Dr.", null);
+      assert (ind.structured_name.equal (expected_name));
+
+      ind = TestUtils.get_individual_by_name (aggregator, "Sadie Jones");
+      expected_name = new StructuredName ("Jones", "Sadie", "M.", null, null);
+      assert (ind.structured_name.equal (expected_name));
+
+      ind = TestUtils.get_individual_by_name (aggregator, "Alex Lawson");
+      expected_name = new StructuredName ("Lawson", "Alex", null, null, null);
+      assert (ind.structured_name.equal (expected_name));
+    }
+
+  /* Test that vCards with weird encodings are parsed correctly. */
+  public void test_encoding ()
+    {
+      /* Set up the backend. */
+      this.bluez_backend.create_simple_device_with_vcard (
+          /* From https://bugs.kde.org/show_bug.cgi?id=98790 */
+          "BEGIN:VCARD\n" +
+          "VERSION:2.1\n" +
+          "FN:Test 1\n" +
+          "N;CHARSET=UTF-8:溌剌;元気\n" +
+          "END:VCARD\n" +
+          "\n" +
+          /* From https://git.gnome.org/browse/evolution-data-server/tree/tests/
+           *      libebook-contacts/test-vcard-parsing.c#n360 */
+          "BEGIN:VCARD\n" +
+          "VERSION:2.1\n" +
+          "FN;ENCODING=quoted-printable:ActualValue=20=C4=9B=C5=A1" +
+            "=C4=8D=C5=99=C5=BE=C3=BD=C3=A1=C3=AD=C3=A9=C3=BA=C5=AF=C3" +
+            "=B3=C3=B6=C4=9A=C5=A0=C4=8C=C5=98=C5=BD=C3=9D=C3=81=C3=8D" +
+            "=C3=89=C3=9A=C5=AE=C3=93=C3=96=C2=A7=201234567890=2012345" +
+            "67890=201234567890=201234567890=201234567890\n" +
+          "END:VCARD\n");
+
+      /* Set up the aggregator and wait until either the expected persona are
+       * seen, or the test times out and fails. */
+      var aggregator = IndividualAggregator.dup ();
+      TestUtils.aggregator_prepare_and_wait_for_individuals_sync_with_timeout (
+          aggregator,
+          {
+            "Test 1",
+            "ActualValue ěščřžýáíéúůóöĚŠČŘŽÝÁÍÉÚŮÓÖ§ " +
+              "1234567890 1234567890 1234567890 1234567890 1234567890"
+          });
+
+      /* Check the properties of our individuals. */
+      var ind = TestUtils.get_individual_by_name (aggregator, "Test 1");
+      var expected_name =
+          new StructuredName ("溌剌", "元気", null, null, null);
+      assert (ind.structured_name.equal (expected_name));
+
+      ind =
+          TestUtils.get_individual_by_name (aggregator,
+              "ActualValue ěščřžýáíéúůóöĚŠČŘŽÝÁÍÉÚŮÓÖ§ " +
+                  "1234567890 1234567890 1234567890 1234567890 1234567890");
+      assert (ind.full_name == "ActualValue ěščřžýáíéúůóöĚŠČŘŽÝÁÍÉÚŮÓÖ§ " +
+                  "1234567890 1234567890 1234567890 1234567890 1234567890");
+    }
+}
+
+public int main (string[] args)
+{
+  Test.init (ref args);
+
+  var tests = new VcardParsingTests ();
+  tests.register ();
+  Test.run ();
+  tests.final_tear_down ();
+
+  return 0;
+}
diff --git a/tests/lib/Makefile.am b/tests/lib/Makefile.am
index 4486e43..a6e958f 100644
--- a/tests/lib/Makefile.am
+++ b/tests/lib/Makefile.am
@@ -5,6 +5,12 @@ SUBDIRS = \
        key-file \
        $(NULL)
 
+if ENABLE_BLUEZ
+if HAVE_BLUEZ_TESTS
+SUBDIRS += bluez
+endif
+endif
+
 if ENABLE_TELEPATHY
 # Build the contactlist first because autotools fails to recognize the
 # dependencies implicitly. There may be a better way to fix this, but then I'd
@@ -30,6 +36,7 @@ endif
 DIST_SUBDIRS = \
        dummy \
        key-file \
+       bluez \
        telepathy \
        eds \
        libsocialweb \
diff --git a/tests/lib/bluez/Makefile.am b/tests/lib/bluez/Makefile.am
new file mode 100644
index 0000000..a2a382d
--- /dev/null
+++ b/tests/lib/bluez/Makefile.am
@@ -0,0 +1,51 @@
+noinst_LTLIBRARIES = libbluez-test.la
+
+libbluez_test_la_VALAFLAGS = \
+       $(AM_VALAFLAGS) \
+       $(TARGET_VALAFLAGS) \
+       $(ERROR_VALAFLAGS) \
+       --library bluez-test \
+       --vapi bluez-test.vapi \
+       --header bluez-test.h \
+       --vapidir=$(abs_srcdir) \
+       --vapidir=$(abs_builddir) \
+       --vapidir=$(abs_top_srcdir)/folks \
+       --vapidir=$(abs_top_builddir)/folks \
+       --vapidir=$(abs_top_srcdir)/tests/lib \
+       --vapidir=$(abs_top_builddir)/tests/lib \
+       --pkg folks-test \
+       --pkg folks-test-dbus \
+       -g \
+       $(NULL)
+
+libbluez_test_la_SOURCES = \
+       backend.vala \
+       test-case.vala \
+       $(NULL)
+
+libbluez_test_la_CPPFLAGS = \
+       $(AM_CPPFLAGS) \
+       -include $(top_srcdir)/folks/warnings.h \
+       $(NULL)
+
+libbluez_test_la_CFLAGS = \
+       -I$(top_srcdir) \
+       -I$(top_srcdir)/tests/lib \
+       $(AM_CFLAGS) \
+       $(ERROR_CFLAGS) \
+       $(GLIB_CFLAGS) \
+       $(GEE_CFLAGS) \
+       $(NULL)
+
+libbluez_test_la_LIBADD = \
+       $(top_builddir)/tests/lib/libfolks-test.la \
+       $(GLIB_LIBS) \
+       $(GEE_LIBS) \
+       $(NULL)
+
+EXTRA_DIST = \
+       bluez-test.vapi \
+       bluez-test.h \
+       $(NULL)
+
+-include $(top_srcdir)/git.mk
diff --git a/tests/lib/bluez/backend.vala b/tests/lib/bluez/backend.vala
new file mode 100644
index 0000000..1ba34a2
--- /dev/null
+++ b/tests/lib/bluez/backend.vala
@@ -0,0 +1,306 @@
+/*
+ * 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 withnall collabora co uk>
+ */
+
+using GLib;
+
+/* Specific mock interfaces for the BlueZ and OBEX services. */
+namespace org
+  {
+    namespace bluez
+      {
+        /* Interface for the bluez5 python-dbusmock template. */
+        [DBus (name = "org.bluez.Mock")]
+        public interface Mock : Object
+          {
+            [DBus (name = "AddAdapter")]
+            public abstract string add_adapter (string device_name,
+                string system_name) throws IOError;
+
+            [DBus (name = "AddDevice")]
+            public abstract string add_device (string adapter_device_name,
+                string device_address, string alias) throws IOError;
+
+            [DBus (name = "PairDevice")]
+            public abstract void pair_device (string adapter_device_name,
+                string device_address) throws IOError;
+
+            [DBus (name = "BlockDevice")]
+            public abstract void block_device (string adapter_device_name,
+                string device_address) throws IOError;
+          }
+
+        namespace obex
+          {
+            /* Interface for the bluez5-obex python-dbusmock template. */
+            [DBus (name = "org.bluez.obex.Mock")]
+            public interface Mock : Object
+              {
+                [DBus (name = "TransferCreated")]
+                public abstract signal void transfer_created (string path,
+                    HashTable<string, Variant> filters,
+                    string transfer_filename);
+              }
+
+            namespace transfer1
+              {
+                [DBus (name = "org.bluez.obex.transfer1.Mock")]
+                public interface Mock : Object
+                  {
+                    [DBus (name = "UpdateStatus")]
+                    public abstract void update_status (bool is_complete)
+                        throws IOError;
+                  }
+              }
+          }
+      }
+  }
+
+/**
+ * Controller for a mock BlueZ backend.
+ *
+ * This contains control methods to instantiate and manipulate a mock BlueZ
+ * service over D-Bus, for the purposes of testing the folks BlueZ backend.
+ *
+ * The mock service uses python-dbusmock, with control messages being sent to
+ * ``*.Mock`` interfaces on the D-Bus objects. Those control interfaces are
+ * exposed as { link Backend.mock_bluez}, { link Backend.mock_bluez_base},
+ * { link Backend.mock_obex} and { link Backend.mock_obex_base}.
+ *
+ * @since UNRELEASED
+ */
+public class BluezTest.Backend
+{
+  private org.bluez.Mock? _mock_bluez = null;
+  private org.freedesktop.DBus.Mock? _mock_bluez_base = null;
+  private org.bluez.obex.Mock? _mock_obex = null;
+  private org.freedesktop.DBus.Mock? _mock_obex_base = null;
+
+  /**
+   * D-Bus proxy for the BlueZ-specific mock interface on the org.bluez object.
+   *
+   * @since UNRELEASED
+   */
+  public org.bluez.Mock? mock_bluez
+    {
+      get { return this._mock_bluez; }
+    }
+
+  /**
+   * D-Bus proxy for the dbusmock mock interface on the org.bluez object.
+   *
+   * @since UNRELEASED
+   */
+  public org.freedesktop.DBus.Mock? mock_bluez_base
+    {
+      get { return this._mock_bluez_base; }
+    }
+
+  /**
+   * D-Bus proxy for the BlueZ-specific mock interface on the org.bluez.obex
+   * object.
+   *
+   * @since UNRELEASED
+   */
+  public org.bluez.obex.Mock? mock_obex
+    {
+      get { return this._mock_obex; }
+    }
+
+  /**
+   * D-Bus proxy for the dbusmock mock interface on the org.bluez.obex object.
+   *
+   * @since UNRELEASED
+   */
+  public org.freedesktop.DBus.Mock? mock_obex_base
+    {
+      get { return this._mock_obex_base; }
+    }
+
+  /**
+   * Default Bluetooth address used for the primary adapter.
+   *
+   * This is the address used for the primary Bluetooth adapter (``hci0``)
+   * unless otherwise specified.
+   *
+   * @since UNRELEASED
+   */
+  public string primary_device_address
+    {
+      get { return "00:00:00:00:00:00"; }
+    }
+
+  /**
+   * Set up the mock D-Bus interfaces.
+   *
+   * This must be called before every different unit test. It creates D-Bus
+   * proxies for the dbusmock objects, auto-launching python-dbusmock if
+   * necessary.
+   *
+   * The required D-Bus service files must previously have been set up with the
+   * buses which are in use. This is done in
+   * { link TestCase.create_transient_dir}.
+   *
+   * @since UNRELEASED
+   */
+  public void set_up ()
+    {
+      /* Create proxies for the client code to use. This auto-starts the
+       * services. Their service files are created in TestCase. */
+      try
+        {
+          this._mock_bluez =
+              Bus.get_proxy_sync (BusType.SYSTEM, "org.bluez", "/");
+          this._mock_bluez_base =
+              Bus.get_proxy_sync (BusType.SYSTEM, "org.bluez", "/");
+
+          this._mock_obex =
+              Bus.get_proxy_sync (BusType.SESSION, "org.bluez.obex", "/");
+          this._mock_obex_base =
+              Bus.get_proxy_sync (BusType.SESSION, "org.bluez.obex", "/");
+        }
+      catch (GLib.Error e1)
+        {
+          /* Tidy up. */
+          this.tear_down ();
+
+          error ("Error connecting to mock object: %s", e1.message);
+        }
+    }
+
+  /**
+   * Tear down the mock D-Bus interfaces.
+   *
+   * This must be called after every different unit test. It undoes
+   * { link Backend.set_up}, although the python-dbusmock processes are kept
+   * around and reset, rather than being killed.
+   *
+   * @since UNRELEASED
+   */
+  public void tear_down ()
+    {
+      /* Reset the python-dbusmock state. */
+      try
+        {
+          this._mock_obex_base.reset ();
+          this._mock_bluez_base.reset ();
+        }
+      catch (IOError e1)
+        {
+          error ("Error resetting python-dbusmock state: %s", e1.message);
+        }
+
+      /* Remove the D-Bus proxies. The python-dbusmock instances will close by
+       * themselves when the mock D-Bus buses are destroyed in
+       * final_tear_down(). */
+      this._mock_obex_base = null;
+      this._mock_bluez_base = null;
+      this._mock_obex = null;
+      this._mock_bluez = null;
+    }
+
+  /**
+   * Create a simple Bluetooth device with the given vCard.
+   *
+   * Create a new Bluetooth adapter (``hci0``) and a new Bluetooth device (with
+   * address { link Backend.primary_device_address}). Pair with the Bluetooth
+   * device and simulate it having the given ``vcard`` (potentially containing
+   * multiple whitespace-separated entries) as its address book.
+   *
+   * On error this function will abort the test.
+   *
+   * @param vcard series of vCards for the device’s address book
+   * @param adapter_path optional return location for the adapter’s D-Bus object
+   * path
+   * @param device_path optional return location for the device’s D-Bus object
+   * path
+   * @return ID of the signal returning the vCard, as per
+   * { link Backend.set_simple_device_vcard}
+   *
+   * @since UNRELEASED
+   */
+  public ulong create_simple_device_with_vcard (string vcard,
+      out string? adapter_path = null, out string? device_path = null)
+    {
+      try
+        {
+          /* Set up a Bluetooth adapter and a single persona store. */
+          adapter_path = this.mock_bluez.add_adapter ("hci0", "Test System");
+          device_path =
+              this.mock_bluez.add_device ("hci0", this.primary_device_address,
+                  "My Phone");
+
+          /* Pair with the phone. */
+          this.mock_bluez.pair_device ("hci0", this.primary_device_address);
+
+          /* Set the vCard to be returned for all transfers. */
+          return this.set_simple_device_vcard (vcard);
+        }
+      catch (IOError e1)
+        {
+          error ("Error setting up mock BlueZ device: %s", e1.message);
+        }
+    }
+
+  /**
+   * Set the vCard to be returned by a simple Bluetooth device.
+   *
+   * This sets the vCard which will be returned indefinitely. It returns a
+   * signal ID which may be disconnected with:
+   * {{{
+   * this.mock_obex.disconnect (signal_id);
+   * }}}
+   * to prevent the vCard being returned in future.
+   *
+   * @param vcard series of vCards for the device’s address book
+   * @return ID of the signal returning the vCard
+   *
+   * @since UNRELEASED
+   */
+  public ulong set_simple_device_vcard (string vcard)
+    {
+      /* Wait for a transfer to be created. Skip activating it and go
+       * straight to completion. */
+      return this.mock_obex.transfer_created.connect ((p, f, v) =>
+        {
+          org.bluez.obex.transfer1.Mock proxy;
+
+          try
+            {
+              FileUtils.set_contents (v, vcard);
+            }
+          catch (FileError e1)
+            {
+              error ("Error writing vCard transfer file ‘%s’: %s",
+                  v, e1.message);
+            }
+
+          try
+            {
+              proxy =
+                  Bus.get_proxy_sync (BusType.SESSION, "org.bluez.obex", p);
+              proxy.update_status (true);
+            }
+          catch (GLib.Error e1)
+            {
+              error ("Error activating transfer: %s", e1.message);
+            }
+        });
+    }
+}
diff --git a/tests/lib/bluez/test-case.vala b/tests/lib/bluez/test-case.vala
new file mode 100644
index 0000000..accaf0b
--- /dev/null
+++ b/tests/lib/bluez/test-case.vala
@@ -0,0 +1,127 @@
+/*
+ * 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:
+ *      Philip Withnall <philip withnall collabora co uk>
+ */
+
+/**
+ * A test case for the BlueZ backend, whose private D-Bus session contains the
+ * necessary python-dbusmock instance to mock up BlueZ.
+ *
+ * @since UNRELEASED
+ */
+public class BluezTest.TestCase : Folks.TestCase
+{
+  /**
+   * A BlueZ backend, normally non-null between set_up() and tear_down().
+   *
+   * If this is non-null, the subclass is expected to have called
+   * its set_up() method at some point before tear_down() is reached.
+   * This usually happens in create_backend().
+   */
+  public BluezTest.Backend? bluez_backend = null;
+
+  public TestCase (string name)
+    {
+      base (name);
+
+      this.bluez_backend = new BluezTest.Backend ();
+
+      Environment.set_variable ("FOLKS_BACKENDS_ALLOWED", "bluez", true);
+      Environment.set_variable ("FOLKS_PRIMARY_STORE", "bluez", true);
+      Environment.set_variable ("FOLKS_BLUEZ_TIMEOUT_DIVISOR", "100", true);
+    }
+
+  /**
+   * { inheritDoc}
+   *
+   * @since UNRELEASED
+   */
+  public override void set_up ()
+    {
+      base.set_up ();
+      this.create_backend ();
+      this.configure_primary_store ();
+    }
+
+  /**
+   * { inheritDoc}
+   *
+   * @since UNRELEASED
+   */
+  public override void private_bus_up ()
+    {
+      /* Set up service files for the python-dbusmock services. */
+      this.create_dbusmock_service (BusType.SYSTEM, "org.bluez", "bluez5");
+      this.create_dbusmock_service (BusType.SESSION, "org.bluez.obex",
+          "bluez5-obex");
+
+      base.private_bus_up ();
+    }
+
+  /**
+   * Virtual method to create and set up the BlueZ backend.
+   *
+   * Called from set_up(); may be overridden to not create the backend,
+   * or to create it but not set it up.
+   *
+   * Subclasses may chain up, but are not required to so.
+   *
+   * @since UNRELEASED
+   */
+  public virtual void create_backend ()
+    {
+      this.bluez_backend = new BluezTest.Backend ();
+      ((!) this.bluez_backend).set_up ();
+    }
+
+  /**
+   * Virtual method to configure ``FOLKS_PRIMARY_STORE`` to point to
+   * our //bluez_backend//.
+   *
+   * Subclasses may chain up, but are not required to so.
+   *
+   * @since UNRELEASED
+   */
+  public virtual void configure_primary_store ()
+    {
+      /* By default, configure BlueZ as the primary store. */
+      assert (this.bluez_backend != null);
+      var config_val =
+          "bluez:" + ((!) this.bluez_backend).primary_device_address;
+      Environment.set_variable ("FOLKS_PRIMARY_STORE", config_val, true);
+    }
+
+  /**
+   * { inheritDoc}
+   *
+   * @since UNRELEASED
+   */
+  public override void tear_down ()
+    {
+      if (this.bluez_backend != null)
+        {
+          ((!) this.bluez_backend).tear_down ();
+          this.bluez_backend = null;
+        }
+
+      Environment.unset_variable ("FOLKS_PRIMARY_STORE");
+
+      base.tear_down ();
+    }
+}
diff --git a/tests/lib/test-case.vala b/tests/lib/test-case.vala
index 11673ba..02d8c23 100644
--- a/tests/lib/test-case.vala
+++ b/tests/lib/test-case.vala
@@ -79,6 +79,9 @@ public abstract class Folks.TestCase : Object
           if (Folks.BuildConf.HAVE_TRACKER)
             locations += Folks.BuildConf.ABS_TOP_BUILDDIR + "/backends/tracker/.libs/tracker.so";
 
+          if (Folks.BuildConf.HAVE_BLUEZ)
+            locations += Folks.BuildConf.ABS_TOP_BUILDDIR + "/backends/bluez/.libs/bluez.so";
+
           Environment.set_variable ("FOLKS_BACKEND_PATH",
               string.joinv (":", locations), true);
         }


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