* @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,
}
/**
}
}
}
+
+ /**
+ * 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;
+ }
}
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;
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)