[folks] Bug 653777 — Add a helper function to create a writable persona



commit a44e2df4d696b2ceb1740e83196fd1943d60664f
Author: Philip Withnall <philip tecnocode co uk>
Date:   Tue Aug 9 18:23:12 2011 +0200

    Bug 653777 â Add a helper function to create a writable persona
    
    This adds IndividualAggregator.ensure_individual_property_writeable(), which
    returns a persona which has the specified property writeable in the specified
    individual. If no such persona exists already, a new one is created and
    linked to the individual. If that all fails, an error is returned.
    
    This allows for clients to write to properties of personas with confidence
    that the updates will be written out to the backend stores. (Achieved by
    calling IndividualAggregator.ensure_individual_property_writeable() first
    and only writing to the property if that call succeeds.)
    
    Closes: bgo#653777

 NEWS                             |    4 +
 folks/individual-aggregator.vala |  130 ++++++++++++
 tests/folks/aggregation.vala     |  409 ++++++++++++++++++++++++++++++++++++++
 3 files changed, 543 insertions(+), 0 deletions(-)
---
diff --git a/NEWS b/NEWS
index 47435b2..602f4ee 100644
--- a/NEWS
+++ b/NEWS
@@ -3,9 +3,13 @@ Overview of changes from libfolks 0.6.1 to libfolks 0.6.2
 
 Bugs fixed:
 * Bug 645056 â TpLowlevel library should have only static public functions
+* Bug 653777 â Would be nice to have a helper function to create a writable
+  persona
 
 API changes:
 * Add PersonaStore:always-writeable-properties property
+* Add IndividualAggregatorError.PROPERTY_NOT_WRITEABLE error
+* Add IndividualAggregator.ensure_individual_property_writeable()
 
 Overview of changes from libfolks 0.6.0 to libfolks 0.6.1
 =========================================================
diff --git a/folks/individual-aggregator.vala b/folks/individual-aggregator.vala
index 209a64e..f8f7160 100644
--- a/folks/individual-aggregator.vala
+++ b/folks/individual-aggregator.vala
@@ -45,6 +45,15 @@ public errordomain Folks.IndividualAggregatorError
    * @since 0.3.0
    */
   STORE_OFFLINE,
+
+  /**
+   * The { link PersonaStore} did not support writing to a property which the
+   * user requested to write to, or which was necessary to write to for storing
+   * linking information.
+   *
+   * @since UNRELEASED
+   */
+  PROPERTY_NOT_WRITEABLE,
 }
 
 /**
@@ -1301,4 +1310,125 @@ public class Folks.IndividualAggregator : Object
             }
         }
     }
+
+  /**
+   * Ensure that the given property is writeable for the given
+   * { link Individual}.
+   *
+   * This makes sure that there is at least one { link Persona} in the
+   * individual which has `property_name` in its
+   * { link Persona.writeable_properties}. If no such persona exists in the
+   * individual, a new one will be created and linked to the individual. (Note
+   * that due to the design of the aggregator, this will result in the previous
+   * individual being removed and replaced by a new one with the new persona;
+   * listen to the { link Individual.removed} signal to see the replacement.)
+   *
+   * It may not be possible to create a new persona which has the given property
+   * as writeable. In that case, a
+   * { link IndividualAggregatorError.NO_WRITEABLE_STORE} or
+   * { link IndividualAggregatorError.PROPERTY_NOT_WRITEABLE} error will be
+   * thrown.
+   *
+   * @param individual the individual for which `property_name` should be
+   * writeable
+   * @param property_name the name of the property which needs to be writeable
+   * (this should be in lower case using hyphens, e.g. âweb-service-addressesâ)
+   * @return a persona (new or existing) which has the given property as
+   * writeable
+   *
+   * @since UNRELEASED
+   */
+  public async Persona ensure_individual_property_writeable (
+      Individual individual, string property_name)
+      throws IndividualAggregatorError
+    {
+      debug ("ensure_individual_property_writeable: %s, %s",
+          individual.id, property_name);
+
+      /* See if the individual already contains the property we want. */
+      foreach (var p1 in individual.personas)
+        {
+          if (property_name in p1.writeable_properties)
+            {
+              debug ("    Returning existing persona: %s", p1.uid);
+              return p1;
+            }
+        }
+
+      /* Otherwise, create a new persona in the writeable store. If the
+       * writeable store doesn't exist or doesn't support writing to the given
+       * property, we try the other persona stores. */
+      var details = new HashTable<string, Value?> (str_hash, str_equal);
+      Persona? new_persona = null;
+
+      if (this._writeable_store != null &&
+          property_name in this._writeable_store.always_writeable_properties)
+        {
+          try
+            {
+              debug ("    Using writeable store");
+              new_persona = yield this.add_persona_from_details (null,
+                  this._writeable_store, details);
+            }
+          catch (IndividualAggregatorError e1)
+            {
+              /* Ignore it */
+              new_persona = null;
+            }
+        }
+
+      if (new_persona == null)
+        {
+          foreach (var s in this._stores.values)
+            {
+              if (s == this._writeable_store ||
+                  !(property_name in s.always_writeable_properties))
+                {
+                  /* Skip the store we've just tried */
+                  continue;
+                }
+
+              try
+                {
+                  debug ("    Using store %s", s.id);
+                  new_persona = yield this.add_persona_from_details (null, s,
+                      details);
+                }
+              catch (IndividualAggregatorError e2)
+                {
+                  /* Ignore it */
+                  new_persona = null;
+                  continue;
+                }
+            }
+        }
+
+      /* Throw an error if we haven't managed to find a suitable store */
+      if (new_persona == null && this._writeable_store == null)
+        {
+          throw new IndividualAggregatorError.NO_WRITEABLE_STORE (
+              _("Can't add personas with no writeable store."));
+        }
+      else if (new_persona == null)
+        {
+          throw new IndividualAggregatorError.PROPERTY_NOT_WRITEABLE (
+              _("Can't write to requested property (â%sâ) of the writeable store."),
+              property_name);
+        }
+
+      /* Link the persona to the existing individual */
+      var linking_personas = new HashSet<Persona> ();
+      linking_personas.add (new_persona);
+
+      foreach (var p2 in individual.personas)
+        {
+          linking_personas.add (p2);
+        }
+
+      debug ("    Linking personas to ensure %s property is writeable.",
+          property_name);
+      yield this.link_personas (linking_personas);
+
+      return new_persona;
+    }
 }
diff --git a/tests/folks/aggregation.vala b/tests/folks/aggregation.vala
index a51271d..116329d 100644
--- a/tests/folks/aggregation.vala
+++ b/tests/folks/aggregation.vala
@@ -56,6 +56,12 @@ public class AggregationTests : Folks.TestCase
       this.add_test ("user", this.test_user);
       this.add_test ("untrusted store", this.test_untrusted_store);
       this.add_test ("refcounting", this.test_linked_individual_refcounting);
+      this.add_test ("ensure individual property writeable:trivial",
+          this.test_ensure_individual_property_writeable_trivial);
+      this.add_test ("ensure individual property writeable:add persona",
+          this.test_ensure_individual_property_writeable_add_persona);
+      this.add_test ("ensure individual property writeable:failure",
+          this.test_ensure_individual_property_writeable_failure);
 
       if (Environment.get_variable ("FOLKS_TEST_VALGRIND") != null)
           this._test_timeout = 10;
@@ -814,6 +820,409 @@ public class AggregationTests : Folks.TestCase
           assert (iter.get_value () == IndividualState.FINALISED);
         }
     }
+
+  /* Test that if an individual contains a persona with a given writeable
+   * property, calling
+   * IndividualAggregator.ensure_individual_property_writeable() on that
+   * individual and property returns the existing persona.
+   * We do this by creating a single key file persona and ensuring that its
+   * alias property is writeable. */
+  public void test_ensure_individual_property_writeable_trivial ()
+    {
+      var main_loop = new GLib.MainLoop (null, false);
+
+      this._kf_backend.set_up ("[0]\n" +
+          "protocol=travis example com\n");
+
+      Individual? individual = null;
+
+      /* Set up the aggregator */
+      var aggregator = new IndividualAggregator ();
+      aggregator.individuals_changed.connect ((added, removed, m, a, r) =>
+        {
+          assert (removed.size == 0);
+          assert (added.size == 1);
+
+          foreach (Individual i in added)
+            {
+              assert (individual == null);
+
+              individual = i;
+              main_loop.quit ();
+            }
+        });
+
+      /* Kill the main loop after a few seconds. */
+      Timeout.add_seconds (this._test_timeout, () =>
+        {
+          main_loop.quit ();
+          return false;
+        });
+
+      Idle.add (() =>
+        {
+          aggregator.prepare.begin ((s,r) =>
+            {
+              try
+                {
+                  aggregator.prepare.end (r);
+                }
+              catch (GLib.Error e1)
+                {
+                  GLib.critical ("Failed to prepare aggregator: %s",
+                    e1.message);
+                  assert_not_reached ();
+                }
+            });
+
+          return false;
+        });
+
+      main_loop.run ();
+
+      /* Check we've got the individual we want */
+      assert (individual != null);
+
+      Persona? persona = null;
+      foreach (var p in individual.personas)
+        {
+          persona = p;
+          break;
+        }
+
+      /* Try and ensure that the alias property is writeable */
+      assert (persona != null);
+      assert ("alias" in persona.writeable_properties);
+
+      Persona? writeable_persona = null;
+
+      /* Kill the main loop after a few seconds. */
+      Timeout.add_seconds (this._test_timeout, () =>
+        {
+          main_loop.quit ();
+          return false;
+        });
+
+      Idle.add (() =>
+        {
+          aggregator.ensure_individual_property_writeable.begin (individual,
+              "alias", (obj, res) =>
+            {
+              try
+                {
+                  writeable_persona =
+                      aggregator.ensure_individual_property_writeable.end (res);
+
+                  main_loop.quit ();
+                }
+              catch (Error e1)
+                {
+                  critical ("Failed to ensure property writeable: %s",
+                      e1.message);
+                  assert_not_reached ();
+                }
+            });
+
+          return false;
+        });
+
+      main_loop.run ();
+
+      assert (writeable_persona != null);
+      assert (writeable_persona == persona);
+
+      /* Clean up for the next test */
+      this._kf_backend.tear_down ();
+      aggregator = null;
+    }
+
+  /* Test that if an individual doesn't contain a persona with a given
+   * writeable property, but a persona store exists which can create personas
+   * with that writeable property, calling
+   * IndividualAggregator.ensure_individual_property_writeable() on that
+   * individual and property will create a new persona and link it to the
+   * existing individual.
+   * We do this by creating an empty key file store and a normal Telepathy
+   * store. We ensure that the im-addresses property of the individual (which
+   * contains only a Tpf.Persona) is writeable, which should result in creating
+   * a Kf.Persona and linking it to the individual. */
+  public void test_ensure_individual_property_writeable_add_persona ()
+    {
+      var main_loop = new GLib.MainLoop (null, false);
+
+      this._kf_backend.set_up ("");
+      void* account_handle = this._tp_backend.add_account ("protocol",
+          "me example com", "cm", "account");
+
+      Individual? individual = null;
+
+      /* Set up the aggregator */
+      var aggregator = new IndividualAggregator ();
+      var individuals_changed_id =
+          aggregator.individuals_changed.connect ((added, removed, m, a, r) =>
+        {
+          assert (removed.size == 0);
+
+          foreach (Individual i in added)
+            {
+              /* olivier example com */
+              if (i.id == "0e46c5e74f61908f49550d241f2a1651892a1695")
+                {
+                  assert (individual == null);
+                  individual = i;
+                  return;
+                }
+            }
+        });
+
+      /* Kill the main loop after a few seconds. */
+      var timeout_id = Timeout.add_seconds (this._test_timeout, () =>
+        {
+          main_loop.quit ();
+          return false;
+        });
+
+      Idle.add (() =>
+        {
+          aggregator.prepare.begin ((s,r) =>
+            {
+              try
+                {
+                  aggregator.prepare.end (r);
+                }
+              catch (GLib.Error e1)
+                {
+                  GLib.critical ("Failed to prepare aggregator: %s",
+                    e1.message);
+                  assert_not_reached ();
+                }
+            });
+
+          return false;
+        });
+
+      main_loop.run ();
+
+      /* Check we've got the individual we want */
+      assert (individual != null);
+
+      Persona? persona = null;
+      foreach (var p in individual.personas)
+        {
+          persona = p;
+          break;
+        }
+
+      /* Try and ensure that the im-addresses property is not writeable */
+      assert (persona != null);
+      assert (!("im-addresses" in persona.writeable_properties));
+
+      Persona? writeable_persona = null;
+
+      /* Remove the signal handler */
+      aggregator.disconnect (individuals_changed_id);
+      Source.remove (timeout_id);
+      aggregator.individuals_changed.connect ((added, removed, m, a, r) =>
+        {
+          foreach (Individual i in removed)
+            {
+              assert (individual != null);
+              assert (i == individual);
+              individual = null;
+            }
+
+          foreach (Individual i in added)
+            {
+              var got_tpf = false;
+              var got_kf = false;
+
+              /* We can't check for the desired individual by ID, since it's
+               * based on a Kf.Persona UID which is randomly generated. Instead,
+               * we have to check for the personas themselves. */
+              foreach (var p in i.personas)
+                {
+                  if (p.uid == "telepathy:/org/freedesktop/Telepathy/Account/cm/protocol/account:olivier example com")
+                    {
+                      got_tpf = true;
+                    }
+                  else if (p.store.type_id == "key-file")
+                    {
+                      got_kf = true;
+                    }
+                }
+
+              if (got_tpf == true && got_kf == true)
+                {
+                  individual = i;
+                  return;
+                }
+            }
+        });
+
+      /* Kill the main loop after a few seconds. */
+      Timeout.add_seconds (this._test_timeout, () =>
+        {
+          main_loop.quit ();
+          return false;
+        });
+
+      Idle.add (() =>
+        {
+          aggregator.ensure_individual_property_writeable.begin (individual,
+              "im-addresses", (obj, res) =>
+            {
+              try
+                {
+                  writeable_persona =
+                      aggregator.ensure_individual_property_writeable.end (res);
+
+                  main_loop.quit ();
+                }
+              catch (Error e1)
+                {
+                  critical ("Failed to ensure property writeable: %s",
+                      e1.message);
+                  assert_not_reached ();
+                }
+            });
+
+          return false;
+        });
+
+      main_loop.run ();
+
+      assert (writeable_persona != null);
+      assert (writeable_persona != persona);
+
+      /* Clean up for the next test */
+      this._tp_backend.remove_account (account_handle);
+      this._kf_backend.tear_down ();
+      individual = null;
+      persona = null;
+      writeable_persona = null;
+      aggregator = null;
+    }
+
+  /* Test that if an individual doesn't contain a persona which has a given
+   * writeable property, and no persona store exists which can create personas
+   * with that writeable property, calling
+   * IndividualAggregator.ensure_individual_property_writeable() on that
+   * individual and property throws an error.
+   * We do this by creating a single key file persona and attempting to ensure
+   * that its is-favourite property is writeable. Since the key file backend
+   * doesn't support is-favourite, and no other backends are available, this
+   * should fail. */
+  public void test_ensure_individual_property_writeable_failure ()
+    {
+      var main_loop = new GLib.MainLoop (null, false);
+
+      this._kf_backend.set_up ("[0]\n" +
+          "protocol=travis example com\n");
+
+      Individual? individual = null;
+
+      /* Set up the aggregator */
+      var aggregator = new IndividualAggregator ();
+      aggregator.individuals_changed.connect ((added, removed, m, a, r) =>
+        {
+          assert (removed.size == 0);
+          assert (added.size == 1);
+
+          foreach (Individual i in added)
+            {
+              assert (individual == null);
+
+              individual = i;
+              main_loop.quit ();
+            }
+        });
+
+      /* Kill the main loop after a few seconds. */
+      Timeout.add_seconds (this._test_timeout, () =>
+        {
+          main_loop.quit ();
+          return false;
+        });
+
+      Idle.add (() =>
+        {
+          aggregator.prepare.begin ((s,r) =>
+            {
+              try
+                {
+                  aggregator.prepare.end (r);
+                }
+              catch (GLib.Error e1)
+                {
+                  GLib.critical ("Failed to prepare aggregator: %s",
+                    e1.message);
+                  assert_not_reached ();
+                }
+            });
+
+          return false;
+        });
+
+      main_loop.run ();
+
+      /* Check we've got the individual we want */
+      assert (individual != null);
+
+      Persona? persona = null;
+      foreach (var p in individual.personas)
+        {
+          persona = p;
+          break;
+        }
+
+      /* Try and ensure that the is-favourite property is writeable */
+      assert (persona != null);
+      assert (!("is-favourite" in persona.writeable_properties));
+
+      Persona? writeable_persona = null;
+
+      /* Kill the main loop after a few seconds. */
+      Timeout.add_seconds (this._test_timeout, () =>
+        {
+          main_loop.quit ();
+          return false;
+        });
+
+      Idle.add (() =>
+        {
+          aggregator.ensure_individual_property_writeable.begin (individual,
+              "is-favourite", (obj, res) =>
+            {
+              try
+                {
+                  writeable_persona =
+                      aggregator.ensure_individual_property_writeable.end (res);
+                  assert_not_reached ();
+                }
+              catch (Error e1)
+                {
+                  /* We expect this error */
+                  if (!(e1 is IndividualAggregatorError.PROPERTY_NOT_WRITEABLE))
+                    {
+                      critical ("Wrong error received: %s", e1.message);
+                      assert_not_reached ();
+                    }
+
+                  main_loop.quit ();
+                }
+            });
+
+          return false;
+        });
+
+      main_loop.run ();
+
+      assert (writeable_persona == null);
+
+      /* Clean up for the next test */
+      this._kf_backend.tear_down ();
+      aggregator = null;
+    }
 }
 
 public int main (string[] args)



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