summaryrefslogtreecommitdiff
path: root/services/sync
diff options
context:
space:
mode:
authorPale Moon <git-repo@palemoon.org>2016-09-01 13:39:08 +0200
committerPale Moon <git-repo@palemoon.org>2016-09-01 13:39:08 +0200
commit3d8ce1a11a7347cc94a937719c4bc8df46fb8d14 (patch)
tree8c26ca375a6312751c00a27e1653fb6f189f0463 /services/sync
parente449bdb1ec3a82f204bffdd9c3c54069d086eee3 (diff)
downloadpalemoon-gre-3d8ce1a11a7347cc94a937719c4bc8df46fb8d14.tar.gz
Base import of Tycho code (warning: huge commit)
Diffstat (limited to 'services/sync')
-rw-r--r--services/sync/Makefile.in80
-rw-r--r--services/sync/SyncComponents.manifest4
-rw-r--r--services/sync/Weave.js101
-rw-r--r--services/sync/locales/Makefile.in15
-rw-r--r--services/sync/locales/en-US/errors.properties2
-rw-r--r--services/sync/locales/en-US/sync.properties17
-rw-r--r--services/sync/locales/moz.build1
-rw-r--r--services/sync/modules-testing/fakeservices.js2
-rw-r--r--services/sync/modules-testing/fxa_utils.js58
-rw-r--r--services/sync/modules-testing/utils.js198
-rw-r--r--services/sync/modules/FxaMigrator.jsm546
-rw-r--r--services/sync/modules/addonsreconciler.js8
-rw-r--r--services/sync/modules/addonutils.js8
-rw-r--r--services/sync/modules/browserid_identity.js786
-rw-r--r--services/sync/modules/constants.js4
-rw-r--r--services/sync/modules/engines.js373
-rw-r--r--services/sync/modules/engines/addons.js31
-rw-r--r--services/sync/modules/engines/apps.js136
-rw-r--r--services/sync/modules/engines/bookmarks.js134
-rw-r--r--services/sync/modules/engines/clients.js89
-rw-r--r--services/sync/modules/engines/forms.js125
-rw-r--r--services/sync/modules/engines/history.js33
-rw-r--r--services/sync/modules/engines/passwords.js253
-rw-r--r--services/sync/modules/engines/prefs.js44
-rw-r--r--services/sync/modules/engines/tabs.js292
-rw-r--r--services/sync/modules/healthreport.jsm262
-rw-r--r--services/sync/modules/identity.js125
-rw-r--r--services/sync/modules/jpakeclient.js10
-rw-r--r--services/sync/modules/keys.js6
-rw-r--r--services/sync/modules/main.js2
-rw-r--r--services/sync/modules/notifications.js19
-rw-r--r--services/sync/modules/policies.js324
-rw-r--r--services/sync/modules/record.js127
-rw-r--r--services/sync/modules/resource.js34
-rw-r--r--services/sync/modules/rest.js18
-rw-r--r--services/sync/modules/service.js365
-rw-r--r--services/sync/modules/stages/cluster.js26
-rw-r--r--services/sync/modules/stages/declined.js76
-rw-r--r--services/sync/modules/stages/enginesync.js106
-rw-r--r--services/sync/modules/status.js37
-rw-r--r--services/sync/modules/userapi.js10
-rw-r--r--services/sync/modules/util.js284
-rw-r--r--services/sync/moz.build62
-rw-r--r--services/sync/services-sync.js20
-rw-r--r--services/sync/tests/moz.build9
-rw-r--r--services/sync/tests/tps/addons/api/restartless-xpi@tests.mozilla.org.xml (renamed from services/sync/tests/tps/restartless-xpi.xml)4
-rw-r--r--services/sync/tests/tps/addons/api/unsigned-xpi@tests.mozilla.org.xml (renamed from services/sync/tests/tps/unsigned-xpi.xml)4
-rw-r--r--services/sync/tests/tps/addons/restartless.xpi (renamed from services/sync/tests/tps/restartless.xpi)bin485 -> 485 bytes
-rw-r--r--services/sync/tests/tps/addons/unsigned.xpi (renamed from services/sync/tests/tps/unsigned.xpi)bin452 -> 452 bytes
-rw-r--r--services/sync/tests/tps/mozmill_sanity.js9
-rw-r--r--services/sync/tests/tps/mozmill_sanity2.js48
-rw-r--r--services/sync/tests/tps/test_addon_reconciling.js5
-rw-r--r--services/sync/tests/tps/test_tabs.js8
-rw-r--r--services/sync/tests/unit/head_helpers.js82
-rw-r--r--services/sync/tests/unit/head_http_server.js53
-rw-r--r--services/sync/tests/unit/test_addons_engine.js25
-rw-r--r--services/sync/tests/unit/test_addons_reconciler.js8
-rw-r--r--services/sync/tests/unit/test_addons_store.js6
-rw-r--r--services/sync/tests/unit/test_addons_tracker.js8
-rw-r--r--services/sync/tests/unit/test_block_sync.js37
-rw-r--r--services/sync/tests/unit/test_bookmark_engine.js32
-rw-r--r--services/sync/tests/unit/test_bookmark_legacy_microsummaries_support.js4
-rw-r--r--services/sync/tests/unit/test_bookmark_livemarks.js8
-rw-r--r--services/sync/tests/unit/test_bookmark_order.js1
-rw-r--r--services/sync/tests/unit/test_bookmark_places_query_rewriting.js5
-rw-r--r--services/sync/tests/unit/test_bookmark_record.js8
-rw-r--r--services/sync/tests/unit/test_bookmark_smart_bookmarks.js14
-rw-r--r--services/sync/tests/unit/test_bookmark_store.js1
-rw-r--r--services/sync/tests/unit/test_bookmark_tracker.js6
-rw-r--r--services/sync/tests/unit/test_browserid_identity.js682
-rw-r--r--services/sync/tests/unit/test_clients_engine.js69
-rw-r--r--services/sync/tests/unit/test_clients_escape.js2
-rw-r--r--services/sync/tests/unit/test_collections_recovery.js11
-rw-r--r--services/sync/tests/unit/test_corrupt_keys.js19
-rw-r--r--services/sync/tests/unit/test_declined.js153
-rw-r--r--services/sync/tests/unit/test_engine.js37
-rw-r--r--services/sync/tests/unit/test_engine_abort.js17
-rw-r--r--services/sync/tests/unit/test_enginemanager.js49
-rw-r--r--services/sync/tests/unit/test_errorhandler.js610
-rw-r--r--services/sync/tests/unit/test_errorhandler_eol.js137
-rw-r--r--services/sync/tests/unit/test_errorhandler_filelog.js70
-rw-r--r--services/sync/tests/unit/test_errorhandler_sync_checkServerError.js144
-rw-r--r--services/sync/tests/unit/test_forms_store.js19
-rw-r--r--services/sync/tests/unit/test_forms_tracker.js4
-rw-r--r--services/sync/tests/unit/test_fxa_migration.js279
-rw-r--r--services/sync/tests/unit/test_fxa_migration_sentinel.js150
-rw-r--r--services/sync/tests/unit/test_fxa_node_reassignment.js321
-rw-r--r--services/sync/tests/unit/test_fxa_service_cluster.js68
-rw-r--r--services/sync/tests/unit/test_fxa_startOver.js63
-rw-r--r--services/sync/tests/unit/test_healthreport.js194
-rw-r--r--services/sync/tests/unit/test_healthreport_migration.js155
-rw-r--r--services/sync/tests/unit/test_history_engine.js26
-rw-r--r--services/sync/tests/unit/test_history_store.js6
-rw-r--r--services/sync/tests/unit/test_history_tracker.js9
-rw-r--r--services/sync/tests/unit/test_hmac_error.js5
-rw-r--r--services/sync/tests/unit/test_httpd_sync_server.js49
-rw-r--r--services/sync/tests/unit/test_identity_manager.js2
-rw-r--r--services/sync/tests/unit/test_interval_triggers.js59
-rw-r--r--services/sync/tests/unit/test_jpakeclient.js16
-rw-r--r--services/sync/tests/unit/test_keys.js11
-rw-r--r--services/sync/tests/unit/test_load_modules.js4
-rw-r--r--services/sync/tests/unit/test_node_reassignment.js126
-rw-r--r--services/sync/tests/unit/test_password_store.js4
-rw-r--r--services/sync/tests/unit/test_places_guid_downgrade.js1
-rw-r--r--services/sync/tests/unit/test_records_crypto.js8
-rw-r--r--services/sync/tests/unit/test_records_wbo.js6
-rw-r--r--services/sync/tests/unit/test_resource.js52
-rw-r--r--services/sync/tests/unit/test_resource_async.js97
-rw-r--r--services/sync/tests/unit/test_resource_header.js20
-rw-r--r--services/sync/tests/unit/test_resource_ua.js15
-rw-r--r--services/sync/tests/unit/test_score_triggers.js12
-rw-r--r--services/sync/tests/unit/test_sendcredentials_controller.js5
-rw-r--r--services/sync/tests/unit/test_service_attributes.js4
-rw-r--r--services/sync/tests/unit/test_service_changePassword.js15
-rw-r--r--services/sync/tests/unit/test_service_checkAccount.js3
-rw-r--r--services/sync/tests/unit/test_service_cluster.js11
-rw-r--r--services/sync/tests/unit/test_service_createAccount.js2
-rw-r--r--services/sync/tests/unit/test_service_detect_upgrade.js30
-rw-r--r--services/sync/tests/unit/test_service_getStorageInfo.js18
-rw-r--r--services/sync/tests/unit/test_service_login.js19
-rw-r--r--services/sync/tests/unit/test_service_passwordUTF8.js4
-rw-r--r--services/sync/tests/unit/test_service_persistLogin.js1
-rw-r--r--services/sync/tests/unit/test_service_startOver.js12
-rw-r--r--services/sync/tests/unit/test_service_startup.js16
-rw-r--r--services/sync/tests/unit/test_service_sync_401.js6
-rw-r--r--services/sync/tests/unit/test_service_sync_locked.js2
-rw-r--r--services/sync/tests/unit/test_service_sync_remoteSetup.js13
-rw-r--r--services/sync/tests/unit/test_service_sync_updateEnabledEngines.js26
-rw-r--r--services/sync/tests/unit/test_service_verifyLogin.js23
-rw-r--r--services/sync/tests/unit/test_service_wipeClient.js3
-rw-r--r--services/sync/tests/unit/test_service_wipeServer.js67
-rw-r--r--services/sync/tests/unit/test_status_checkSetup.js2
-rw-r--r--services/sync/tests/unit/test_syncengine.js21
-rw-r--r--services/sync/tests/unit/test_syncengine_sync.js239
-rw-r--r--services/sync/tests/unit/test_syncscheduler.js240
-rw-r--r--services/sync/tests/unit/test_syncstoragerequest.js24
-rw-r--r--services/sync/tests/unit/test_tab_engine.js150
-rw-r--r--services/sync/tests/unit/test_tab_store.js131
-rw-r--r--services/sync/tests/unit/test_tab_tracker.js20
-rw-r--r--services/sync/tests/unit/test_upgrade_old_sync_key.js2
-rw-r--r--services/sync/tests/unit/test_utils_getIcon.js18
-rw-r--r--services/sync/tests/unit/test_utils_json.js3
-rw-r--r--services/sync/tests/unit/test_utils_passphrase.js2
-rw-r--r--services/sync/tests/unit/test_warn_on_truncated_response.js95
-rw-r--r--services/sync/tests/unit/xpcshell.ini34
-rw-r--r--services/sync/tps/extensions/mozmill/chrome.manifest2
-rw-r--r--services/sync/tps/extensions/mozmill/defaults/preferences/debug.js7
-rw-r--r--services/sync/tps/extensions/mozmill/install.rdf71
-rw-r--r--services/sync/tps/extensions/mozmill/resource/driver/controller.js1150
-rw-r--r--services/sync/tps/extensions/mozmill/resource/driver/elementslib.js (renamed from services/sync/tps/extensions/mozmill/resource/modules/elementslib.js)215
-rw-r--r--services/sync/tps/extensions/mozmill/resource/driver/mozelement.js1163
-rw-r--r--services/sync/tps/extensions/mozmill/resource/driver/mozmill.js (renamed from services/sync/tps/extensions/mozmill/resource/modules/mozmill.js)229
-rw-r--r--services/sync/tps/extensions/mozmill/resource/driver/msgbroker.js58
-rw-r--r--services/sync/tps/extensions/mozmill/resource/modules/assertions.js465
-rw-r--r--services/sync/tps/extensions/mozmill/resource/modules/controller.js1002
-rw-r--r--services/sync/tps/extensions/mozmill/resource/modules/driver.js290
-rw-r--r--services/sync/tps/extensions/mozmill/resource/modules/errors.js102
-rw-r--r--services/sync/tps/extensions/mozmill/resource/modules/frame.js924
-rw-r--r--services/sync/tps/extensions/mozmill/resource/modules/init.js177
-rw-r--r--services/sync/tps/extensions/mozmill/resource/modules/inspection.js363
-rw-r--r--services/sync/tps/extensions/mozmill/resource/modules/jum.js231
-rw-r--r--services/sync/tps/extensions/mozmill/resource/modules/l10n.js11
-rw-r--r--services/sync/tps/extensions/mozmill/resource/modules/mozelement.js668
-rw-r--r--services/sync/tps/extensions/mozmill/resource/modules/stack.js43
-rw-r--r--services/sync/tps/extensions/mozmill/resource/modules/utils.js522
-rw-r--r--services/sync/tps/extensions/mozmill/resource/modules/windows.js292
-rw-r--r--services/sync/tps/extensions/mozmill/resource/stdlib/EventUtils.js907
-rw-r--r--services/sync/tps/extensions/mozmill/resource/stdlib/arrays.js44
-rw-r--r--services/sync/tps/extensions/mozmill/resource/stdlib/dom.js11
-rw-r--r--services/sync/tps/extensions/mozmill/resource/stdlib/httpd.js409
-rw-r--r--services/sync/tps/extensions/mozmill/resource/stdlib/objects.js3
-rw-r--r--services/sync/tps/extensions/mozmill/resource/stdlib/os.js40
-rw-r--r--services/sync/tps/extensions/mozmill/resource/stdlib/securable-module.js38
-rw-r--r--services/sync/tps/extensions/mozmill/resource/stdlib/strings.js2
-rw-r--r--services/sync/tps/extensions/mozmill/resource/stdlib/utils.js462
-rw-r--r--services/sync/tps/extensions/tps/chrome.manifest3
-rw-r--r--services/sync/tps/extensions/tps/components/tps-cmdline.js39
-rw-r--r--services/sync/tps/extensions/tps/install.rdf20
-rw-r--r--services/sync/tps/extensions/tps/modules/sync.jsm115
-rw-r--r--services/sync/tps/extensions/tps/resource/auth/fxaccounts.jsm96
-rw-r--r--services/sync/tps/extensions/tps/resource/auth/sync.jsm84
-rw-r--r--services/sync/tps/extensions/tps/resource/logger.jsm (renamed from services/sync/tps/extensions/tps/modules/logger.jsm)29
-rw-r--r--services/sync/tps/extensions/tps/resource/modules/addons.jsm (renamed from services/sync/tps/extensions/tps/modules/addons.jsm)18
-rw-r--r--services/sync/tps/extensions/tps/resource/modules/bookmarks.jsm (renamed from services/sync/tps/extensions/tps/modules/bookmarks.jsm)57
-rw-r--r--services/sync/tps/extensions/tps/resource/modules/forms.jsm (renamed from services/sync/tps/extensions/tps/modules/forms.jsm)10
-rw-r--r--services/sync/tps/extensions/tps/resource/modules/history.jsm (renamed from services/sync/tps/extensions/tps/modules/history.jsm)17
-rw-r--r--services/sync/tps/extensions/tps/resource/modules/passwords.jsm (renamed from services/sync/tps/extensions/tps/modules/passwords.jsm)14
-rw-r--r--services/sync/tps/extensions/tps/resource/modules/prefs.jsm (renamed from services/sync/tps/extensions/tps/modules/prefs.jsm)23
-rw-r--r--services/sync/tps/extensions/tps/resource/modules/tabs.jsm (renamed from services/sync/tps/extensions/tps/modules/tabs.jsm)0
-rw-r--r--services/sync/tps/extensions/tps/resource/modules/windows.jsm (renamed from services/sync/tps/extensions/tps/modules/windows.jsm)0
-rw-r--r--services/sync/tps/extensions/tps/resource/quit.js (renamed from services/sync/tps/extensions/tps/modules/quit.js)47
-rw-r--r--services/sync/tps/extensions/tps/resource/tps.jsm (renamed from services/sync/tps/extensions/tps/modules/tps.jsm)489
192 files changed, 14338 insertions, 7304 deletions
diff --git a/services/sync/Makefile.in b/services/sync/Makefile.in
index 6bf09d207..e86ee160f 100644
--- a/services/sync/Makefile.in
+++ b/services/sync/Makefile.in
@@ -2,91 +2,15 @@
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
-DEPTH := @DEPTH@
-topsrcdir := @top_srcdir@
-srcdir := @srcdir@
-VPATH := @srcdir@
-
-include $(DEPTH)/config/autoconf.mk
-
# Definitions used by constants.js.
-weave_version := 1.26.0
+weave_version := 1.40.0
weave_id := {340c2bbc-ce74-4362-90b5-7c26312808ef}
# Preprocess files.
SYNC_PP := modules/constants.js
SYNC_PP_FLAGS := \
-Dweave_version=$(weave_version) \
- -Dweave_id=$(weave_id)
+ -Dweave_id='$(weave_id)'
SYNC_PP_PATH = $(FINAL_TARGET)/modules/services-sync
PP_TARGETS += SYNC_PP
-# The set of core JavaScript modules for Sync. These are copied as-is.
-sync_modules := \
- addonsreconciler.js \
- addonutils.js \
- engines.js \
- identity.js \
- jpakeclient.js \
- keys.js \
- main.js \
- notifications.js \
- policies.js \
- record.js \
- resource.js \
- rest.js \
- service.js \
- status.js \
- userapi.js \
- util.js \
- $(NULL)
-
-# The set of JavaScript modules provide engines for Sync. These are
-# copied as-is.
-sync_engine_modules := \
- addons.js \
- apps.js \
- bookmarks.js \
- clients.js \
- forms.js \
- history.js \
- passwords.js \
- prefs.js \
- tabs.js \
- $(NULL)
-
-sync_stage_modules := \
- cluster.js \
- enginesync.js \
- $(NULL)
-
-sync_testing_modules := \
- fakeservices.js \
- rotaryengine.js \
- utils.js \
- $(NULL)
-
-EXTRA_COMPONENTS := \
- SyncComponents.manifest \
- Weave.js \
- $(NULL)
-
-PREF_JS_EXPORTS := $(srcdir)/services-sync.js
-
-# Install JS module files.
-SYNC_MAIN_FILES := $(addprefix modules/,$(sync_modules))
-SYNC_MAIN_DEST = $(FINAL_TARGET)/modules/services-sync
-INSTALL_TARGETS += SYNC_MAIN
-
-SYNC_ENGINES_FILES := $(addprefix modules/engines/,$(sync_engine_modules))
-SYNC_ENGINES_DEST = $(FINAL_TARGET)/modules/services-sync/engines
-INSTALL_TARGETS += SYNC_ENGINES
-
-SYNC_STAGES_FILES := $(addprefix modules/stages/,$(sync_stage_modules))
-SYNC_STAGES_DEST = $(FINAL_TARGET)/modules/services-sync/stages
-INSTALL_TARGETS += SYNC_STAGES
-
-TESTING_JS_MODULES := $(addprefix modules-testing/,$(sync_testing_modules))
-TESTING_JS_MODULE_DIR := services/sync
-
-include $(topsrcdir)/config/rules.mk
diff --git a/services/sync/SyncComponents.manifest b/services/sync/SyncComponents.manifest
index 2b315656c..b1d99f5e8 100644
--- a/services/sync/SyncComponents.manifest
+++ b/services/sync/SyncComponents.manifest
@@ -23,3 +23,7 @@ contract @mozilla.org/network/protocol/about;1?what=sync-log {d28f8a0b-95da-48f4
# Register resource aliases
# (Note, for tests these are also set up in addResourceAlias)
resource services-sync resource://gre/modules/services-sync/
+
+#ifdef MOZ_SERVICES_HEALTHREPORT
+category healthreport-js-provider-default SyncProvider resource://services-sync/healthreport.jsm
+#endif
diff --git a/services/sync/Weave.js b/services/sync/Weave.js
index f39eb12f5..80b81d36e 100644
--- a/services/sync/Weave.js
+++ b/services/sync/Weave.js
@@ -9,6 +9,8 @@ const Cu = Components.utils;
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/FileUtils.jsm");
+Cu.import("resource://gre/modules/Promise.jsm");
+Cu.import("resource://services-sync/util.js");
const SYNC_PREFS_BRANCH = "services.sync.";
@@ -24,13 +26,25 @@ const SYNC_PREFS_BRANCH = "services.sync.";
*
* If Sync is not configured, no extra Sync code is loaded. If an
* external component (say the UI) needs to interact with Sync, it
- * should do something like the following:
+ * should use the promise-base function whenLoaded() - something like the
+ * following:
*
* // 1. Grab a handle to the Sync XPCOM service.
* let service = Cc["@mozilla.org/weave/service;1"]
* .getService(Components.interfaces.nsISupports)
* .wrappedJSObject;
*
+ * // 2. Use the .then method of the promise.
+ * service.whenLoaded().then(() => {
+ * // You are free to interact with "Weave." objects.
+ * return;
+ * });
+ *
+ * And that's it! However, if you really want to avoid promises and do it
+ * old-school, then
+ *
+ * // 1. Get a reference to the service as done in (1) above.
+ *
* // 2. Check if the service has been initialized.
* if (service.ready) {
* // You are free to interact with "Weave." objects.
@@ -58,12 +72,68 @@ WeaveService.prototype = {
Ci.nsISupportsWeakReference]),
ensureLoaded: function () {
+ // If we are loaded and not using FxA, load the migration module.
+ if (!this.fxAccountsEnabled) {
+ Cu.import("resource://services-sync/FxaMigrator.jsm");
+ }
+
Components.utils.import("resource://services-sync/main.js");
// Side-effect of accessing the service is that it is instantiated.
Weave.Service;
},
+ whenLoaded: function() {
+ if (this.ready) {
+ return Promise.resolve();
+ }
+ let deferred = Promise.defer();
+
+ Services.obs.addObserver(function onReady() {
+ Services.obs.removeObserver(onReady, "weave:service:ready");
+ deferred.resolve();
+ }, "weave:service:ready", false);
+ this.ensureLoaded();
+ return deferred.promise;
+ },
+
+ /**
+ * Whether Firefox Accounts is enabled.
+ *
+ * @return bool
+ *
+ * This function is currently always returning false because we don't support
+ * the use of FxA/Sync-1.5 but do want to keep the code "just in case".
+ */
+ get fxAccountsEnabled() {
+ // Early exit: FxA not supported.
+ return false;
+
+ try {
+ // Old sync guarantees '@' will never appear in the username while FxA
+ // uses the FxA email address - so '@' is the flag we use.
+ let username = Services.prefs.getCharPref(SYNC_PREFS_BRANCH + "username");
+ return !username || username.includes('@');
+ } catch (_) {
+ return true; // No username == only allow FxA to be configured.
+ }
+ },
+
+ /**
+ * Whether Sync appears to be enabled.
+ *
+ * This returns true if all the Sync preferences for storing account
+ * and server configuration are populated.
+ *
+ * It does *not* perform a robust check to see if the client is working.
+ * For that, you'll want to check Weave.Status.checkSetup().
+ */
+ get enabled() {
+ let prefs = Services.prefs.getBranch(SYNC_PREFS_BRANCH);
+ return prefs.prefHasUserValue("username") &&
+ prefs.prefHasUserValue("clusterURL");
+ },
+
observe: function (subject, topic, data) {
switch (topic) {
case "app-startup":
@@ -77,20 +147,22 @@ WeaveService.prototype = {
this.timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
this.timer.initWithCallback({
notify: function() {
+ let isConfigured = false;
// We only load more if it looks like Sync is configured.
let prefs = Services.prefs.getBranch(SYNC_PREFS_BRANCH);
-
- if (!prefs.prefHasUserValue("username")) {
- return;
+ if (prefs.prefHasUserValue("username")) {
+ // We have a username. So, do a more thorough check. This will
+ // import a number of modules and thus increase memory
+ // accordingly. We could potentially copy code performed by
+ // this check into this file if our above code is yielding too
+ // many false positives.
+ Components.utils.import("resource://services-sync/main.js");
+ isConfigured = Weave.Status.checkSetup() != Weave.CLIENT_NOT_CONFIGURED;
}
-
- // We have a username. So, do a more thorough check. This will
- // import a number of modules and thus increase memory
- // accordingly. We could potentially copy code performed by
- // this check into this file if our above code is yielding too
- // many false positives.
- Components.utils.import("resource://services-sync/main.js");
- if (Weave.Status.checkSetup() != Weave.CLIENT_NOT_CONFIGURED) {
+ let getHistogramById = Services.telemetry.getHistogramById;
+ getHistogramById("WEAVE_CONFIGURED").add(isConfigured);
+ if (isConfigured) {
+ getHistogramById("WEAVE_CONFIGURED_MASTER_PASSWORD").add(Utils.mpEnabled());
this.ensureLoaded();
}
}.bind(this)
@@ -111,10 +183,11 @@ AboutWeaveLog.prototype = {
return 0;
},
- newChannel: function(aURI) {
+ newChannel: function(aURI, aLoadInfo) {
let dir = FileUtils.getDir("ProfD", ["weave", "logs"], true);
let uri = Services.io.newFileURI(dir);
- let channel = Services.io.newChannelFromURI(uri);
+ let channel = Services.io.newChannelFromURIWithLoadInfo(uri, aLoadInfo);
+
channel.originalURI = aURI;
// Ensure that the about page has the same privileges as a regular directory
diff --git a/services/sync/locales/Makefile.in b/services/sync/locales/Makefile.in
deleted file mode 100644
index 1975558f3..000000000
--- a/services/sync/locales/Makefile.in
+++ /dev/null
@@ -1,15 +0,0 @@
-# This Source Code Form is subject to the terms of the Mozilla Public
-# License, v. 2.0. If a copy of the MPL was not distributed with this
-# file, You can obtain one at http://mozilla.org/MPL/2.0/.
-
-DEPTH = @DEPTH@
-topsrcdir = @top_srcdir@
-srcdir = @srcdir@
-VPATH = @srcdir@
-relativesrcdir = @relativesrcdir@
-
-include $(DEPTH)/config/autoconf.mk
-
-DEFINES += -DAB_CD=$(AB_CD)
-
-include $(topsrcdir)/config/rules.mk
diff --git a/services/sync/locales/en-US/errors.properties b/services/sync/locales/en-US/errors.properties
index b8ff66b48..237e57176 100644
--- a/services/sync/locales/en-US/errors.properties
+++ b/services/sync/locales/en-US/errors.properties
@@ -12,7 +12,7 @@ error.login.reason.server = Server incorrectly configured
error.sync.failed_partial = One or more data types could not be synced
# LOCALIZATION NOTE (error.sync.reason.serverMaintenance): We removed the extraneous period from this string
-error.sync.reason.serverMaintenance = Pale Moon Sync server maintenance is underway, syncing will resume automatically
+error.sync.reason.serverMaintenance = Firefox Sync server maintenance is underway, syncing will resume automatically
invalid-captcha = Incorrect words, try again
weak-password = Use a stronger password
diff --git a/services/sync/locales/en-US/sync.properties b/services/sync/locales/en-US/sync.properties
index 0e28e9e8c..9f9d2f65e 100644
--- a/services/sync/locales/en-US/sync.properties
+++ b/services/sync/locales/en-US/sync.properties
@@ -2,7 +2,7 @@
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
-# %1: the user name (Ed), %2: the app name (Pale Moon), %3: the operating system (Android)
+# %1: the user name (Ed), %2: the app name (Firefox), %3: the operating system (Android)
client.name2 = %1$S's %2$S on %3$S
# %S is the date and time at which the last sync successfully completed
@@ -25,12 +25,10 @@ error.logout.description = Sync encountered an error while connecting. It's pro
error.sync.title = Error While Syncing
error.sync.description = Sync encountered an error while syncing: %1$S. Sync will automatically retry this action.
error.sync.prolonged_failure = Sync has not been able to complete during the last %1$S days. Please check your network settings.
-error.sync.no_node_found = The Sync server is a little busy right now, but you don't need to do anything about it. We'll start syncing your data as soon as we can!
-error.sync.no_node_found.title = Sync Delay
error.sync.serverStatusButton.label = Server Status
error.sync.serverStatusButton.accesskey = V
-error.sync.needUpdate.description = You need to update Pale Moon Sync to continue syncing your data.
-error.sync.needUpdate.label = Update Pale Moon Sync
+error.sync.needUpdate.description = You need to update Firefox Sync to continue syncing your data.
+error.sync.needUpdate.label = Update Firefox Sync
error.sync.needUpdate.accesskey = U
error.sync.tryAgainButton.label = Sync Now
error.sync.tryAgainButton.accesskey = S
@@ -40,4 +38,11 @@ error.sync.quota.label = Server Quota Exceeded
error.sync.quota.description = Sync failed because it exceeded the server quota. Please review which data to sync.
error.sync.viewQuotaButton.label = View Quota
error.sync.viewQuotaButton.accesskey = V
-
+warning.sync.eol.label = Service Shutting Down
+# %1: the app name (Firefox)
+warning.sync.eol.description = Your Firefox Sync service is shutting down soon. Upgrade %1$S to keep syncing.
+error.sync.eol.label = Service Unavailable
+# %1: the app name (Firefox)
+error.sync.eol.description = Your Firefox Sync service is no longer available. You need to upgrade %1$S to keep syncing.
+sync.eol.learnMore.label = Learn more
+sync.eol.learnMore.accesskey = L
diff --git a/services/sync/locales/moz.build b/services/sync/locales/moz.build
index 895d11993..3bbe67297 100644
--- a/services/sync/locales/moz.build
+++ b/services/sync/locales/moz.build
@@ -4,3 +4,4 @@
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+JAR_MANIFESTS += ['jar.mn']
diff --git a/services/sync/modules-testing/fakeservices.js b/services/sync/modules-testing/fakeservices.js
index b7c17b3d9..0e265937b 100644
--- a/services/sync/modules-testing/fakeservices.js
+++ b/services/sync/modules-testing/fakeservices.js
@@ -16,7 +16,7 @@ const {utils: Cu} = Components;
Cu.import("resource://services-sync/record.js");
Cu.import("resource://services-sync/util.js");
-let btoa = Cu.import("resource://services-common/log4moz.js").btoa;
+let btoa = Cu.import("resource://gre/modules/Log.jsm").btoa;
this.FakeFilesystemService = function FakeFilesystemService(contents) {
this.fakeContents = contents;
diff --git a/services/sync/modules-testing/fxa_utils.js b/services/sync/modules-testing/fxa_utils.js
new file mode 100644
index 000000000..4c622660a
--- /dev/null
+++ b/services/sync/modules-testing/fxa_utils.js
@@ -0,0 +1,58 @@
+"use strict";
+
+this.EXPORTED_SYMBOLS = [
+ "initializeIdentityWithTokenServerResponse",
+];
+
+const {utils: Cu} = Components;
+
+Cu.import("resource://gre/modules/Log.jsm");
+Cu.import("resource://services-sync/main.js");
+Cu.import("resource://services-sync/browserid_identity.js");
+Cu.import("resource://services-common/tokenserverclient.js");
+Cu.import("resource://testing-common/services/common/logging.js");
+Cu.import("resource://testing-common/services/sync/utils.js");
+
+// Create a new browserid_identity object and initialize it with a
+// mocked TokenServerClient which always receives the specified response.
+this.initializeIdentityWithTokenServerResponse = function(response) {
+ // First create a mock "request" object that well' hack into the token server.
+ // A log for it
+ let requestLog = Log.repository.getLogger("testing.mock-rest");
+ if (!requestLog.appenders.length) { // might as well see what it says :)
+ requestLog.addAppender(new Log.DumpAppender());
+ requestLog.level = Log.Level.Trace;
+ }
+
+ // A mock request object.
+ function MockRESTRequest(url) {};
+ MockRESTRequest.prototype = {
+ _log: requestLog,
+ setHeader: function() {},
+ get: function(callback) {
+ this.response = response;
+ callback.call(this);
+ }
+ }
+ // The mocked TokenServer client which will get the response.
+ function MockTSC() { }
+ MockTSC.prototype = new TokenServerClient();
+ MockTSC.prototype.constructor = MockTSC;
+ MockTSC.prototype.newRESTRequest = function(url) {
+ return new MockRESTRequest(url);
+ }
+ // Arrange for the same observerPrefix as browserid_identity uses.
+ MockTSC.prototype.observerPrefix = "weave:service";
+
+ // tie it all together.
+ Weave.Status.__authManager = Weave.Service.identity = new BrowserIDManager();
+ Weave.Service._clusterManager = Weave.Service.identity.createClusterManager(Weave.Service);
+ let browseridManager = Weave.Service.identity;
+ // a sanity check
+ if (!(browseridManager instanceof BrowserIDManager)) {
+ throw new Error("sync isn't configured for browserid_identity");
+ }
+ let mockTSC = new MockTSC()
+ configureFxAccountIdentity(browseridManager);
+ browseridManager._tokenServerClient = mockTSC;
+}
diff --git a/services/sync/modules-testing/utils.js b/services/sync/modules-testing/utils.js
index 61c121a77..faea8fb04 100644
--- a/services/sync/modules-testing/utils.js
+++ b/services/sync/modules-testing/utils.js
@@ -5,24 +5,32 @@
"use strict";
this.EXPORTED_SYMBOLS = [
- "TEST_CLUSTER_URL",
- "TEST_SERVER_URL",
"btoa", // It comes from a module import.
"encryptPayload",
+ "ensureLegacyIdentityManager",
"setBasicCredentials",
+ "makeIdentityConfig",
+ "configureFxAccountIdentity",
+ "configureIdentity",
"SyncTestingInfrastructure",
"waitForZeroTimer",
+ "Promise", // from a module import
+ "add_identity_test",
];
const {utils: Cu} = Components;
+Cu.import("resource://services-sync/status.js");
+Cu.import("resource://services-sync/identity.js");
Cu.import("resource://services-common/utils.js");
Cu.import("resource://services-crypto/utils.js");
-Cu.import("resource://testing-common/services-common/logging.js");
+Cu.import("resource://services-sync/util.js");
+Cu.import("resource://services-sync/browserid_identity.js");
+Cu.import("resource://testing-common/services/common/logging.js");
Cu.import("resource://testing-common/services/sync/fakeservices.js");
-
-this.TEST_SERVER_URL = "http://localhost:8080/";
-this.TEST_CLUSTER_URL = TEST_SERVER_URL;
+Cu.import("resource://gre/modules/FxAccounts.jsm");
+Cu.import("resource://gre/modules/FxAccountsCommon.js");
+Cu.import("resource://gre/modules/Promise.jsm");
/**
* First wait >100ms (nsITimers can take up to that much time to fire, so
@@ -42,6 +50,17 @@ this.waitForZeroTimer = function waitForZeroTimer(callback) {
CommonUtils.namedTimer(wait, 150, {}, "timer");
}
+/**
+ * Ensure Sync is configured with the "legacy" identity provider.
+ */
+this.ensureLegacyIdentityManager = function() {
+ let ns = {};
+ Cu.import("resource://services-sync/service.js", ns);
+
+ Status.__authManager = ns.Service.identity = new IdentityManager();
+ ns.Service._clusterManager = ns.Service.identity.createClusterManager(ns.Service);
+}
+
this.setBasicCredentials =
function setBasicCredentials(username, password, syncKey) {
let ns = {};
@@ -53,18 +72,135 @@ this.setBasicCredentials =
auth.syncKey = syncKey;
}
-this.SyncTestingInfrastructure =
- function SyncTestingInfrastructure(username, password, syncKey) {
+// Return an identity configuration suitable for testing with our identity
+// providers. |overrides| can specify overrides for any default values.
+this.makeIdentityConfig = function(overrides) {
+ // first setup the defaults.
+ let result = {
+ // Username used in both fxaccount and sync identity configs.
+ username: "foo",
+ // fxaccount specific credentials.
+ fxaccount: {
+ user: {
+ assertion: 'assertion',
+ email: 'email',
+ kA: 'kA',
+ kB: 'kB',
+ sessionToken: 'sessionToken',
+ uid: 'user_uid',
+ verified: true,
+ },
+ token: {
+ endpoint: Svc.Prefs.get("tokenServerURI"),
+ duration: 300,
+ id: "id",
+ key: "key",
+ // uid will be set to the username.
+ }
+ },
+ sync: {
+ // username will come from the top-level username
+ password: "whatever",
+ syncKey: "abcdeabcdeabcdeabcdeabcdea",
+ }
+ };
+
+ // Now handle any specified overrides.
+ if (overrides) {
+ if (overrides.username) {
+ result.username = overrides.username;
+ }
+ if (overrides.sync) {
+ // TODO: allow just some attributes to be specified
+ result.sync = overrides.sync;
+ }
+ if (overrides.fxaccount) {
+ // TODO: allow just some attributes to be specified
+ result.fxaccount = overrides.fxaccount;
+ }
+ }
+ return result;
+}
+
+// Configure an instance of an FxAccount identity provider with the specified
+// config (or the default config if not specified).
+this.configureFxAccountIdentity = function(authService,
+ config = makeIdentityConfig()) {
+ let MockInternal = {};
+ let fxa = new FxAccounts(MockInternal);
+
+ // until we get better test infrastructure for bid_identity, we set the
+ // signedin user's "email" to the username, simply as many tests rely on this.
+ config.fxaccount.user.email = config.username;
+ fxa.internal.currentAccountState.signedInUser = {
+ version: DATA_FORMAT_VERSION,
+ accountData: config.fxaccount.user
+ };
+ fxa.internal.currentAccountState.getCertificate = function(data, keyPair, mustBeValidUntil) {
+ this.cert = {
+ validUntil: fxa.internal.now() + CERT_LIFETIME,
+ cert: "certificate",
+ };
+ return Promise.resolve(this.cert.cert);
+ };
+
+ let mockTSC = { // TokenServerClient
+ getTokenFromBrowserIDAssertion: function(uri, assertion, cb) {
+ config.fxaccount.token.uid = config.username;
+ cb(null, config.fxaccount.token);
+ },
+ };
+ authService._fxaService = fxa;
+ authService._tokenServerClient = mockTSC;
+ // Set the "account" of the browserId manager to be the "email" of the
+ // logged in user of the mockFXA service.
+ authService._signedInUser = fxa.internal.currentAccountState.signedInUser.accountData;
+ authService._account = config.fxaccount.user.email;
+}
+
+this.configureIdentity = function(identityOverrides) {
+ let config = makeIdentityConfig(identityOverrides);
let ns = {};
Cu.import("resource://services-sync/service.js", ns);
- let auth = ns.Service.identity;
- auth.account = username || "foo";
- auth.basicPassword = password || "password";
- auth.syncKey = syncKey || "abcdeabcdeabcdeabcdeabcdea";
+ if (ns.Service.identity instanceof BrowserIDManager) {
+ // do the FxAccounts thang...
+ configureFxAccountIdentity(ns.Service.identity, config);
+ return ns.Service.identity.initializeWithCurrentIdentity().then(() => {
+ // need to wait until this identity manager is readyToAuthenticate.
+ return ns.Service.identity.whenReadyToAuthenticate.promise;
+ });
+ }
+ // old style identity provider.
+ setBasicCredentials(config.username, config.sync.password, config.sync.syncKey);
+ let deferred = Promise.defer();
+ deferred.resolve();
+ return deferred.promise;
+}
- ns.Service.serverURL = TEST_SERVER_URL;
- ns.Service.clusterURL = TEST_CLUSTER_URL;
+this.SyncTestingInfrastructure = function (server, username, password, syncKey) {
+ let ns = {};
+ Cu.import("resource://services-sync/service.js", ns);
+
+ ensureLegacyIdentityManager();
+ let config = makeIdentityConfig();
+ // XXX - hacks for the sync identity provider.
+ if (username)
+ config.username = username;
+ if (password)
+ config.sync.password = password;
+ if (syncKey)
+ config.sync.syncKey = syncKey;
+ let cb = Async.makeSpinningCallback();
+ configureIdentity(config).then(cb, cb);
+ cb.wait();
+
+ let i = server.identity;
+ let uri = i.primaryScheme + "://" + i.primaryHost + ":" +
+ i.primaryPort + "/";
+
+ ns.Service.serverURL = uri;
+ ns.Service.clusterURL = uri;
this.logStats = initTestLogging();
this.fakeFilesystem = new FakeFilesystemService({});
@@ -87,3 +223,37 @@ this.encryptPayload = function encryptPayload(cleartext) {
};
}
+// This helper can be used instead of 'add_test' or 'add_task' to run the
+// specified test function twice - once with the old-style sync identity
+// manager and once with the new-style BrowserID identity manager, to ensure
+// it works in both cases.
+//
+// * The test itself should be passed as 'test' - ie, test code will generally
+// pass |this|.
+// * The test function is a regular test function - although note that it must
+// be a generator - async operations should yield them, and run_next_test
+// mustn't be called.
+this.add_identity_test = function(test, testFunction) {
+ function note(what) {
+ let msg = "running test " + testFunction.name + " with " + what + " identity manager";
+ test.do_print(msg);
+ }
+ let ns = {};
+ Cu.import("resource://services-sync/service.js", ns);
+ // one task for the "old" identity manager.
+ test.add_task(function() {
+ note("sync");
+ let oldIdentity = Status._authManager;
+ ensureLegacyIdentityManager();
+ yield testFunction();
+ Status.__authManager = ns.Service.identity = oldIdentity;
+ });
+ // another task for the FxAccounts identity manager.
+ test.add_task(function() {
+ note("FxAccounts");
+ let oldIdentity = Status._authManager;
+ Status.__authManager = ns.Service.identity = new BrowserIDManager();
+ yield testFunction();
+ Status.__authManager = ns.Service.identity = oldIdentity;
+ });
+}
diff --git a/services/sync/modules/FxaMigrator.jsm b/services/sync/modules/FxaMigrator.jsm
new file mode 100644
index 000000000..605ee5d7f
--- /dev/null
+++ b/services/sync/modules/FxaMigrator.jsm
@@ -0,0 +1,546 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict;"
+
+const {classes: Cc, interfaces: Ci, results: Cr, utils: Cu} = Components;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Log.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/Task.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "fxAccounts",
+ "resource://gre/modules/FxAccounts.jsm");
+
+XPCOMUtils.defineLazyGetter(this, "WeaveService", function() {
+ return Cc["@mozilla.org/weave/service;1"]
+ .getService(Components.interfaces.nsISupports)
+ .wrappedJSObject;
+});
+
+XPCOMUtils.defineLazyModuleGetter(this, "Weave",
+ "resource://services-sync/main.js");
+
+// FxAccountsCommon.js doesn't use a "namespace", so create one here.
+let fxAccountsCommon = {};
+Cu.import("resource://gre/modules/FxAccountsCommon.js", fxAccountsCommon);
+
+// We send this notification whenever the "user" migration state changes.
+const OBSERVER_STATE_CHANGE_TOPIC = "fxa-migration:state-changed";
+// We also send the state notification when we *receive* this. This allows
+// consumers to avoid loading this module until it receives a notification
+// from us (which may never happen if there's no migration to do)
+const OBSERVER_STATE_REQUEST_TOPIC = "fxa-migration:state-request";
+
+// We send this notification whenever the migration is paused waiting for
+// something internal to complete.
+const OBSERVER_INTERNAL_STATE_CHANGE_TOPIC = "fxa-migration:internal-state-changed";
+
+// We use this notification so Sync's healthreport module can record telemetry
+// (actually via "health report") for us.
+const OBSERVER_INTERNAL_TELEMETRY_TOPIC = "fxa-migration:internal-telemetry";
+
+const OBSERVER_TOPICS = [
+ "xpcom-shutdown",
+ "weave:service:sync:start",
+ "weave:service:sync:finish",
+ "weave:service:sync:error",
+ "weave:eol",
+ OBSERVER_STATE_REQUEST_TOPIC,
+ fxAccountsCommon.ONLOGIN_NOTIFICATION,
+ fxAccountsCommon.ONLOGOUT_NOTIFICATION,
+ fxAccountsCommon.ONVERIFIED_NOTIFICATION,
+];
+
+// A list of preference names we write to the migration sentinel. We only
+// write ones that have a user-set value.
+const FXA_SENTINEL_PREFS = [
+ "identity.fxaccounts.auth.uri",
+ "identity.fxaccounts.remote.force_auth.uri",
+ "identity.fxaccounts.remote.signup.uri",
+ "identity.fxaccounts.remote.signin.uri",
+ "identity.fxaccounts.settings.uri",
+ "services.sync.tokenServerURI",
+];
+
+function Migrator() {
+ // Leave the log-level as Debug - Sync will setup log appenders such that
+ // these messages generally will not be seen unless other log related
+ // prefs are set.
+ this.log.level = Log.Level.Debug;
+
+ this._nextUserStatePromise = Promise.resolve();
+
+ for (let topic of OBSERVER_TOPICS) {
+ Services.obs.addObserver(this, topic, false);
+ }
+ // ._state is an optimization so we avoid sending redundant observer
+ // notifications when the state hasn't actually changed.
+ this._state = null;
+}
+
+Migrator.prototype = {
+ log: Log.repository.getLogger("Sync.SyncMigration"),
+
+ // What user action is necessary to push the migration forward?
+ // A |null| state means there is nothing to do. Note that a null state implies
+ // either. (a) no migration is necessary or (b) that the migrator module is
+ // waiting for something outside of the user's control - eg, sync to complete,
+ // the migration sentinel to be uploaded, etc. In most cases the wait will be
+ // short, but edge cases (eg, no network, sync bugs that prevent it stopping
+ // until shutdown) may require a significantly longer wait.
+ STATE_USER_FXA: "waiting for user to be signed in to FxA",
+ STATE_USER_FXA_VERIFIED: "waiting for a verified FxA user",
+
+ // What internal state are we at? This is primarily used for FHR reporting so
+ // we can determine why exactly we might be stalled.
+ STATE_INTERNAL_WAITING_SYNC_COMPLETE: "waiting for sync to complete",
+ STATE_INTERNAL_WAITING_WRITE_SENTINEL: "waiting for sentinel to be written",
+ STATE_INTERNAL_WAITING_START_OVER: "waiting for sync to reset itself",
+ STATE_INTERNAL_COMPLETE: "migration complete",
+
+ // Flags for the telemetry we record. The UI will call a helper to record
+ // the fact some UI was interacted with.
+ TELEMETRY_ACCEPTED: "accepted",
+ TELEMETRY_DECLINED: "declined",
+ TELEMETRY_UNLINKED: "unlinked",
+
+ finalize() {
+ for (let topic of OBSERVER_TOPICS) {
+ Services.obs.removeObserver(this, topic);
+ }
+ },
+
+ observe(subject, topic, data) {
+ this.log.debug("observed " + topic);
+ switch (topic) {
+ case "xpcom-shutdown":
+ this.finalize();
+ break;
+
+ case OBSERVER_STATE_REQUEST_TOPIC:
+ // someone has requested the state - send it.
+ this._queueCurrentUserState(true);
+ break;
+
+ default:
+ // some other observer that may affect our state has fired, so update.
+ this._queueCurrentUserState().then(
+ () => this.log.debug("update state from observer " + topic + " complete")
+ ).catch(err => {
+ let msg = "Failed to handle topic " + topic + ": " + err;
+ Cu.reportError(msg);
+ this.log.error(msg);
+ });
+ }
+ },
+
+ // Try and move to a state where we are blocked on a user action.
+ // This needs to be restartable, and the states may, in edge-cases, end
+ // up going backwards (eg, user logs out while we are waiting to be told
+ // about verification)
+ // This is called by our observer notifications - so if there is already
+ // a promise in-flight, it's possible we will miss something important - so
+ // we wait for the in-flight one to complete then fire another (ie, this
+ // is effectively a queue of promises)
+ _queueCurrentUserState(forceObserver = false) {
+ return this._nextUserStatePromise = this._nextUserStatePromise.then(
+ () => this._promiseCurrentUserState(forceObserver),
+ err => {
+ let msg = "Failed to determine the current user state: " + err;
+ Cu.reportError(msg);
+ this.log.error(msg);
+ return this._promiseCurrentUserState(forceObserver)
+ }
+ );
+ },
+
+ _promiseCurrentUserState: Task.async(function* (forceObserver) {
+ this.log.trace("starting _promiseCurrentUserState");
+ let update = (newState, email=null) => {
+ this.log.info("Migration state: '${state}' => '${newState}'",
+ {state: this._state, newState: newState});
+ if (forceObserver || newState !== this._state) {
+ this._state = newState;
+ let subject = Cc["@mozilla.org/supports-string;1"]
+ .createInstance(Ci.nsISupportsString);
+ subject.data = email || "";
+ Services.obs.notifyObservers(subject, OBSERVER_STATE_CHANGE_TOPIC, newState);
+ }
+ return newState;
+ }
+
+ // If we have no sync user, or are already using an FxA account we must
+ // be done.
+ if (WeaveService.fxAccountsEnabled) {
+ // should not be necessary, but if we somehow ended up with FxA enabled
+ // and sync blocked it would be bad - so better safe than sorry.
+ this.log.debug("FxA enabled - there's nothing to do!")
+ this._unblockSync();
+ return update(null);
+ }
+
+ // so we need to migrate - let's see how far along we are.
+ // If sync isn't in EOL mode, then we are still waiting for the server
+ // to offer the migration process - so no user action necessary.
+ let isEOL = false;
+ try {
+ isEOL = !!Services.prefs.getCharPref("services.sync.errorhandler.alert.mode");
+ } catch (e) {}
+
+ if (!isEOL) {
+ return update(null);
+ }
+
+ // So we are in EOL mode - have we a user?
+ let fxauser = yield fxAccounts.getSignedInUser();
+ if (!fxauser) {
+ // See if there is a migration sentinel so we can send the email
+ // address that was used on a different device for this account (ie, if
+ // this is a "join the party" migration rather than the first)
+ let sentinel = yield this._getSyncMigrationSentinel();
+ return update(this.STATE_USER_FXA, sentinel && sentinel.email);
+ }
+ if (!fxauser.verified) {
+ return update(this.STATE_USER_FXA_VERIFIED, fxauser.email);
+ }
+
+ // So we just have housekeeping to do - we aren't blocked on a user, so
+ // reflect that.
+ this.log.info("No next user state - doing some housekeeping");
+ update(null);
+
+ // We need to disable sync from automatically starting,
+ // and if we are currently syncing wait for it to complete.
+ this._blockSync();
+
+ // Are we currently syncing?
+ if (Weave.Service._locked) {
+ // our observers will kick us further along when complete.
+ this.log.info("waiting for sync to complete")
+ Services.obs.notifyObservers(null, OBSERVER_INTERNAL_STATE_CHANGE_TOPIC,
+ this.STATE_INTERNAL_WAITING_SYNC_COMPLETE);
+ return null;
+ }
+
+ // Write the migration sentinel if necessary.
+ Services.obs.notifyObservers(null, OBSERVER_INTERNAL_STATE_CHANGE_TOPIC,
+ this.STATE_INTERNAL_WAITING_WRITE_SENTINEL);
+ yield this._setMigrationSentinelIfNecessary();
+
+ // Get the list of enabled engines to we can restore that state.
+ let enginePrefs = this._getEngineEnabledPrefs();
+
+ // Must be ready to perform the actual migration.
+ this.log.info("Performing final sync migration steps");
+ // Do the actual migration. We setup one observer for when the new identity
+ // is about to be initialized so we can reset some key preferences - but
+ // there's no promise associated with this.
+ let observeStartOverIdentity;
+ Services.obs.addObserver(observeStartOverIdentity = () => {
+ this.log.info("observed that startOver is about to re-initialize the identity");
+ Services.obs.removeObserver(observeStartOverIdentity, "weave:service:start-over:init-identity");
+ // We've now reset all sync prefs - set the engine related prefs back to
+ // what they were.
+ for (let [prefName, prefType, prefVal] of enginePrefs) {
+ this.log.debug("Restoring pref ${prefName} (type=${prefType}) to ${prefVal}",
+ {prefName, prefType, prefVal});
+ switch (prefType) {
+ case Services.prefs.PREF_BOOL:
+ Services.prefs.setBoolPref(prefName, prefVal);
+ break;
+ case Services.prefs.PREF_STRING:
+ Services.prefs.setCharPref(prefName, prefVal);
+ break;
+ default:
+ // _getEngineEnabledPrefs doesn't return any other type...
+ Cu.reportError("unknown engine pref type for " + prefName + ": " + prefType);
+ }
+ }
+ }, "weave:service:start-over:init-identity", false);
+
+ // And another observer for the startOver being fully complete - the only
+ // reason for this is so we can wait until everything is fully reset.
+ let startOverComplete = new Promise((resolve, reject) => {
+ let observe;
+ Services.obs.addObserver(observe = () => {
+ this.log.info("observed that startOver is complete");
+ Services.obs.removeObserver(observe, "weave:service:start-over:finish");
+ resolve();
+ }, "weave:service:start-over:finish", false);
+ });
+
+ Weave.Service.startOver();
+ // need to wait for an observer.
+ Services.obs.notifyObservers(null, OBSERVER_INTERNAL_STATE_CHANGE_TOPIC,
+ this.STATE_INTERNAL_WAITING_START_OVER);
+ yield startOverComplete;
+ // observer fired, now kick things off with the FxA user.
+ this.log.info("scheduling initial FxA sync.");
+ // Note we technically don't need to unblockSync as by now all sync prefs
+ // have been reset - but it doesn't hurt.
+ this._unblockSync();
+ Weave.Service.scheduler.scheduleNextSync(0);
+
+ // Tell the front end that migration is now complete -- Sync is now
+ // configured with an FxA user.
+ forceObserver = true;
+ this.log.info("Migration complete");
+ update(null);
+
+ Services.obs.notifyObservers(null, OBSERVER_INTERNAL_STATE_CHANGE_TOPIC,
+ this.STATE_INTERNAL_COMPLETE);
+ return null;
+ }),
+
+ /* Return an object with the preferences we care about */
+ _getSentinelPrefs() {
+ let result = {};
+ for (let pref of FXA_SENTINEL_PREFS) {
+ if (Services.prefs.prefHasUserValue(pref)) {
+ result[pref] = Services.prefs.getCharPref(pref);
+ }
+ }
+ return result;
+ },
+
+ /* Apply any preferences we've obtained from the sentinel */
+ _applySentinelPrefs(savedPrefs) {
+ for (let pref of FXA_SENTINEL_PREFS) {
+ if (savedPrefs[pref]) {
+ Services.prefs.setCharPref(pref, savedPrefs[pref]);
+ }
+ }
+ },
+
+ /* Ask sync to upload the migration sentinel */
+ _setSyncMigrationSentinel: Task.async(function* () {
+ yield WeaveService.whenLoaded();
+ let signedInUser = yield fxAccounts.getSignedInUser();
+ let sentinel = {
+ email: signedInUser.email,
+ uid: signedInUser.uid,
+ verified: signedInUser.verified,
+ prefs: this._getSentinelPrefs(),
+ };
+ yield Weave.Service.setFxAMigrationSentinel(sentinel);
+ }),
+
+ /* Ask sync to upload the migration sentinal if we (or any other linked device)
+ haven't previously written one.
+ */
+ _setMigrationSentinelIfNecessary: Task.async(function* () {
+ if (!(yield this._getSyncMigrationSentinel())) {
+ this.log.info("writing the migration sentinel");
+ yield this._setSyncMigrationSentinel();
+ }
+ }),
+
+ /* Ask sync to return a migration sentinel if one exists, otherwise return null */
+ _getSyncMigrationSentinel: Task.async(function* () {
+ yield WeaveService.whenLoaded();
+ let sentinel = yield Weave.Service.getFxAMigrationSentinel();
+ this.log.debug("got migration sentinel ${}", sentinel);
+ return sentinel;
+ }),
+
+ _getDefaultAccountName: Task.async(function* (sentinel) {
+ // Requires looking to see if other devices have written a migration
+ // sentinel (eg, see _haveSynchedMigrationSentinel), and if not, see if
+ // the legacy account name appears to be a valid email address (via the
+ // services.sync.account pref), otherwise return null.
+ // NOTE: Sync does all this synchronously via nested event loops, but we
+ // expose a promise to make future migration to an async-sync easier.
+ if (sentinel && sentinel.email) {
+ this.log.info("defaultAccountName found via sentinel: ${}", sentinel.email);
+ return sentinel.email;
+ }
+ // No previous migrations, so check the existing account name.
+ let account = Weave.Service.identity.account;
+ if (account && account.contains("@")) {
+ this.log.info("defaultAccountName found via legacy account name: {}", account);
+ return account;
+ }
+ this.log.info("defaultAccountName could not find an account");
+ return null;
+ }),
+
+ // Prevent sync from automatically starting
+ _blockSync() {
+ Weave.Service.scheduler.blockSync();
+ },
+
+ _unblockSync() {
+ Weave.Service.scheduler.unblockSync();
+ },
+
+ /* Return a list of [prefName, prefType, prefVal] for all engine related
+ preferences.
+ */
+ _getEngineEnabledPrefs() {
+ let result = [];
+ for (let engine of Weave.Service.engineManager.getAll()) {
+ let prefName = "services.sync.engine." + engine.prefName;
+ let prefVal;
+ try {
+ prefVal = Services.prefs.getBoolPref(prefName);
+ result.push([prefName, Services.prefs.PREF_BOOL, prefVal]);
+ } catch (ex) {} /* just skip this pref */
+ }
+ // and the declined list.
+ try {
+ let prefName = "services.sync.declinedEngines";
+ let prefVal = Services.prefs.getCharPref(prefName);
+ result.push([prefName, Services.prefs.PREF_STRING, prefVal]);
+ } catch (ex) {}
+ return result;
+ },
+
+ /* return true if all engines are enabled, false otherwise. */
+ _allEnginesEnabled() {
+ return Weave.Service.engineManager.getAll().every(e => e.enabled);
+ },
+
+ /*
+ * Some helpers for the UI to try and move to the next state.
+ */
+
+ // Open a UI for the user to create a Firefox Account. This should only be
+ // called while we are in the STATE_USER_FXA state. When the user completes
+ // the creation we'll see an ONLOGIN_NOTIFICATION notification from FxA and
+ // we'll move to either the STATE_USER_FXA_VERIFIED state or we'll just
+ // complete the migration if they login as an already verified user.
+ createFxAccount: Task.async(function* (win) {
+ let {url, options} = yield this.getFxAccountCreationOptions();
+ win.switchToTabHavingURI(url, true, options);
+ // An FxA observer will fire when the user completes this, which will
+ // cause us to move to the next "user blocked" state and notify via our
+ // observer notification.
+ }),
+
+ // Returns an object with properties "url" and "options", suitable for
+ // opening FxAccounts to create/signin to FxA suitable for the migration
+ // state. The caller of this is responsible for the actual opening of the
+ // page.
+ // This should only be called while we are in the STATE_USER_FXA state. When
+ // the user completes the creation we'll see an ONLOGIN_NOTIFICATION
+ // notification from FxA and we'll move to either the STATE_USER_FXA_VERIFIED
+ // state or we'll just complete the migration if they login as an already
+ // verified user.
+ getFxAccountCreationOptions: Task.async(function* (win) {
+ // warn if we aren't in the expected state - but go ahead anyway!
+ if (this._state != this.STATE_USER_FXA) {
+ this.log.warn("getFxAccountCreationOptions called in an unexpected state: ${}", this._state);
+ }
+ // We need to obtain the sentinel and apply any prefs that might be
+ // specified *before* attempting to setup FxA as the prefs might
+ // specify custom servers etc.
+ let sentinel = yield this._getSyncMigrationSentinel();
+ if (sentinel && sentinel.prefs) {
+ this._applySentinelPrefs(sentinel.prefs);
+ }
+ // If we already have a sentinel then we assume the user has previously
+ // created the specified account, so just ask to sign-in.
+ let action = sentinel ? "signin" : "signup";
+ // See if we can find a default account name to use.
+ let email = yield this._getDefaultAccountName(sentinel);
+ let tail = email ? "&email=" + encodeURIComponent(email) : "";
+ // A special flag so server-side metrics can tell this is part of migration.
+ tail += "&migration=sync11";
+ // We want to ask FxA to offer a "Customize Sync" checkbox iff any engines
+ // are disabled.
+ let customize = !this._allEnginesEnabled();
+ tail += "&customizeSync=" + customize;
+
+ // We assume the caller of this is going to actually use it, so record
+ // telemetry now.
+ this.recordTelemetry(this.TELEMETRY_ACCEPTED);
+ return {
+ url: "about:accounts?action=" + action + tail,
+ options: {ignoreFragment: true, replaceQueryString: true}
+ };
+ }),
+
+ // Ask the FxA servers to re-send a verification mail for the currently
+ // logged in user. This should only be called while we are in the
+ // STATE_USER_FXA_VERIFIED state. When the user clicks on the link in
+ // the mail we should see an ONVERIFIED_NOTIFICATION which will cause us
+ // to complete the migration.
+ resendVerificationMail: Task.async(function * (win) {
+ // warn if we aren't in the expected state - but go ahead anyway!
+ if (this._state != this.STATE_USER_FXA_VERIFIED) {
+ this.log.warn("resendVerificationMail called in an unexpected state: ${}", this._state);
+ }
+ let ok = true;
+ try {
+ yield fxAccounts.resendVerificationEmail();
+ } catch (ex) {
+ this.log.error("Failed to resend verification mail: ${}", ex);
+ ok = false;
+ }
+ this.recordTelemetry(this.TELEMETRY_ACCEPTED);
+ let fxauser = yield fxAccounts.getSignedInUser();
+ let sb = Services.strings.createBundle("chrome://browser/locale/accounts.properties");
+
+ let heading = ok ?
+ sb.formatStringFromName("verificationSentHeading", [fxauser.email], 1) :
+ sb.GetStringFromName("verificationNotSentHeading");
+ let title = sb.GetStringFromName(ok ? "verificationSentTitle" : "verificationNotSentTitle");
+ let description = sb.GetStringFromName(ok ? "verificationSentDescription"
+ : "verificationNotSentDescription");
+
+ let factory = Cc["@mozilla.org/prompter;1"]
+ .getService(Ci.nsIPromptFactory);
+ let prompt = factory.getPrompt(win, Ci.nsIPrompt);
+ let bag = prompt.QueryInterface(Ci.nsIWritablePropertyBag2);
+ bag.setPropertyAsBool("allowTabModal", true);
+
+ prompt.alert(title, heading + "\n\n" + description);
+ }),
+
+ // "forget" about the current Firefox account. This should only be called
+ // while we are in the STATE_USER_FXA_VERIFIED state. After this we will
+ // see an ONLOGOUT_NOTIFICATION, which will cause the migrator to return back
+ // to the STATE_USER_FXA state, from where they can choose a different account.
+ forgetFxAccount: Task.async(function * () {
+ // warn if we aren't in the expected state - but go ahead anyway!
+ if (this._state != this.STATE_USER_FXA_VERIFIED) {
+ this.log.warn("forgetFxAccount called in an unexpected state: ${}", this._state);
+ }
+ return fxAccounts.signOut();
+ }),
+
+ recordTelemetry(flag) {
+ // Note the value is the telemetry field name - but this is an
+ // implementation detail which could be changed later.
+ switch (flag) {
+ case this.TELEMETRY_ACCEPTED:
+ case this.TELEMETRY_UNLINKED:
+ case this.TELEMETRY_DECLINED:
+ Services.obs.notifyObservers(null, OBSERVER_INTERNAL_TELEMETRY_TOPIC, flag);
+ break;
+ default:
+ throw new Error("Unexpected telemetry flag: " + flag);
+ }
+ },
+
+ get learnMoreLink() {
+ try {
+ var url = Services.prefs.getCharPref("app.support.baseURL");
+ } catch (err) {
+ return null;
+ }
+ url += "sync-upgrade";
+ let sb = Services.strings.createBundle("chrome://weave/locale/services/sync.properties");
+ return {
+ text: sb.GetStringFromName("sync.eol.learnMore.label"),
+ href: Services.urlFormatter.formatURL(url),
+ };
+ },
+};
+
+// We expose a singleton
+this.EXPORTED_SYMBOLS = ["fxaMigrator"];
+let fxaMigrator = new Migrator();
diff --git a/services/sync/modules/addonsreconciler.js b/services/sync/modules/addonsreconciler.js
index 7ae681e6b..2e838e885 100644
--- a/services/sync/modules/addonsreconciler.js
+++ b/services/sync/modules/addonsreconciler.js
@@ -19,7 +19,7 @@
const Cu = Components.utils;
-Cu.import("resource://services-common/log4moz.js");
+Cu.import("resource://gre/modules/Log.jsm");
Cu.import("resource://services-sync/util.js");
Cu.import("resource://gre/modules/AddonManager.jsm");
@@ -113,9 +113,9 @@ this.EXPORTED_SYMBOLS = ["AddonsReconciler", "CHANGE_INSTALLED",
* heed them like they were normal. In the end, the state is proper.
*/
this.AddonsReconciler = function AddonsReconciler() {
- this._log = Log4Moz.repository.getLogger("Sync.AddonsReconciler");
+ this._log = Log.repository.getLogger("Sync.AddonsReconciler");
let level = Svc.Prefs.get("log.logger.addonsreconciler", "Debug");
- this._log.level = Log4Moz.Level[level];
+ this._log.level = Log.Level[level];
Svc.Obs.add("xpcom-shutdown", this.stopListening, this);
};
@@ -140,7 +140,7 @@ AddonsReconciler.prototype = {
*/
_shouldPersist: true,
- /** log4moz logger instance */
+ /** Log logger instance */
_log: null,
/**
diff --git a/services/sync/modules/addonutils.js b/services/sync/modules/addonutils.js
index fee7649f6..54b441b9e 100644
--- a/services/sync/modules/addonutils.js
+++ b/services/sync/modules/addonutils.js
@@ -9,17 +9,17 @@ this.EXPORTED_SYMBOLS = ["AddonUtils"];
const {interfaces: Ci, utils: Cu} = Components;
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
-Cu.import("resource://services-common/log4moz.js");
+Cu.import("resource://gre/modules/Log.jsm");
Cu.import("resource://services-sync/util.js");
XPCOMUtils.defineLazyModuleGetter(this, "AddonManager",
"resource://gre/modules/AddonManager.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "AddonRepository",
- "resource://gre/modules/AddonRepository.jsm");
+ "resource://gre/modules/addons/AddonRepository.jsm");
function AddonUtilsInternal() {
- this._log = Log4Moz.repository.getLogger("Sync.AddonUtils");
- this._log.Level = Log4Moz.Level[Svc.Prefs.get("log.logger.addonutils")];
+ this._log = Log.repository.getLogger("Sync.AddonUtils");
+ this._log.Level = Log.Level[Svc.Prefs.get("log.logger.addonutils")];
}
AddonUtilsInternal.prototype = {
/**
diff --git a/services/sync/modules/browserid_identity.js b/services/sync/modules/browserid_identity.js
new file mode 100644
index 000000000..bc8ea6b30
--- /dev/null
+++ b/services/sync/modules/browserid_identity.js
@@ -0,0 +1,786 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+this.EXPORTED_SYMBOLS = ["BrowserIDManager"];
+
+const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
+
+Cu.import("resource://gre/modules/Log.jsm");
+Cu.import("resource://services-common/async.js");
+Cu.import("resource://services-common/utils.js");
+Cu.import("resource://services-common/tokenserverclient.js");
+Cu.import("resource://services-crypto/utils.js");
+Cu.import("resource://services-sync/identity.js");
+Cu.import("resource://services-sync/util.js");
+Cu.import("resource://services-common/tokenserverclient.js");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://services-sync/constants.js");
+Cu.import("resource://gre/modules/Promise.jsm");
+Cu.import("resource://services-sync/stages/cluster.js");
+Cu.import("resource://gre/modules/FxAccounts.jsm");
+
+// Lazy imports to prevent unnecessary load on startup.
+XPCOMUtils.defineLazyModuleGetter(this, "Weave",
+ "resource://services-sync/main.js");
+
+XPCOMUtils.defineLazyModuleGetter(this, "BulkKeyBundle",
+ "resource://services-sync/keys.js");
+
+XPCOMUtils.defineLazyModuleGetter(this, "fxAccounts",
+ "resource://gre/modules/FxAccounts.jsm");
+
+XPCOMUtils.defineLazyGetter(this, 'log', function() {
+ let log = Log.repository.getLogger("Sync.BrowserIDManager");
+ log.level = Log.Level[Svc.Prefs.get("log.logger.identity")] || Log.Level.Error;
+ return log;
+});
+
+// FxAccountsCommon.js doesn't use a "namespace", so create one here.
+let fxAccountsCommon = {};
+Cu.import("resource://gre/modules/FxAccountsCommon.js", fxAccountsCommon);
+
+const OBSERVER_TOPICS = [
+ fxAccountsCommon.ONLOGIN_NOTIFICATION,
+ fxAccountsCommon.ONLOGOUT_NOTIFICATION,
+];
+
+const PREF_SYNC_SHOW_CUSTOMIZATION = "services.sync-setup.ui.showCustomizationDialog";
+
+function deriveKeyBundle(kB) {
+ let out = CryptoUtils.hkdf(kB, undefined,
+ "identity.mozilla.com/picl/v1/oldsync", 2*32);
+ let bundle = new BulkKeyBundle();
+ // [encryptionKey, hmacKey]
+ bundle.keyPair = [out.slice(0, 32), out.slice(32, 64)];
+ return bundle;
+}
+
+/*
+ General authentication error for abstracting authentication
+ errors from multiple sources (e.g., from FxAccounts, TokenServer).
+ details is additional details about the error - it might be a string, or
+ some other error object (which should do the right thing when toString() is
+ called on it)
+*/
+function AuthenticationError(details) {
+ this.details = details;
+}
+
+AuthenticationError.prototype = {
+ toString: function() {
+ return "AuthenticationError(" + this.details + ")";
+ }
+}
+
+this.BrowserIDManager = function BrowserIDManager() {
+ // NOTE: _fxaService and _tokenServerClient are replaced with mocks by
+ // the test suite.
+ this._fxaService = fxAccounts;
+ this._tokenServerClient = new TokenServerClient();
+ this._tokenServerClient.observerPrefix = "weave:service";
+ // will be a promise that resolves when we are ready to authenticate
+ this.whenReadyToAuthenticate = null;
+ this._log = log;
+};
+
+this.BrowserIDManager.prototype = {
+ __proto__: IdentityManager.prototype,
+
+ _fxaService: null,
+ _tokenServerClient: null,
+ // https://docs.services.mozilla.com/token/apis.html
+ _token: null,
+ _signedInUser: null, // the signedinuser we got from FxAccounts.
+
+ // null if no error, otherwise a LOGIN_FAILED_* value that indicates why
+ // we failed to authenticate (but note it might not be an actual
+ // authentication problem, just a transient network error or similar)
+ _authFailureReason: null,
+
+ // it takes some time to fetch a sync key bundle, so until this flag is set,
+ // we don't consider the lack of a keybundle as a failure state.
+ _shouldHaveSyncKeyBundle: false,
+
+ get readyToAuthenticate() {
+ // We are finished initializing when we *should* have a sync key bundle,
+ // although we might not actually have one due to auth failures etc.
+ return this._shouldHaveSyncKeyBundle;
+ },
+
+ get needsCustomization() {
+ try {
+ return Services.prefs.getBoolPref(PREF_SYNC_SHOW_CUSTOMIZATION);
+ } catch (e) {
+ return false;
+ }
+ },
+
+ initialize: function() {
+ for (let topic of OBSERVER_TOPICS) {
+ Services.obs.addObserver(this, topic, false);
+ }
+ return this.initializeWithCurrentIdentity();
+ },
+
+ /**
+ * Ensure the user is logged in. Returns a promise that resolves when
+ * the user is logged in, or is rejected if the login attempt has failed.
+ */
+ ensureLoggedIn: function() {
+ if (!this._shouldHaveSyncKeyBundle) {
+ // We are already in the process of logging in.
+ return this.whenReadyToAuthenticate.promise;
+ }
+
+ // If we are already happy then there is nothing more to do.
+ if (this._syncKeyBundle) {
+ return Promise.resolve();
+ }
+
+ // Similarly, if we have a previous failure that implies an explicit
+ // re-entering of credentials by the user is necessary we don't take any
+ // further action - an observer will fire when the user does that.
+ if (Weave.Status.login == LOGIN_FAILED_LOGIN_REJECTED) {
+ return Promise.reject();
+ }
+
+ // So - we've a previous auth problem and aren't currently attempting to
+ // log in - so fire that off.
+ this.initializeWithCurrentIdentity();
+ return this.whenReadyToAuthenticate.promise;
+ },
+
+ finalize: function() {
+ // After this is called, we can expect Service.identity != this.
+ for (let topic of OBSERVER_TOPICS) {
+ Services.obs.removeObserver(this, topic);
+ }
+ this.resetCredentials();
+ this._signedInUser = null;
+ return Promise.resolve();
+ },
+
+ offerSyncOptions: function () {
+ // If the user chose to "Customize sync options" when signing
+ // up with Firefox Accounts, ask them to choose what to sync.
+ const url = "chrome://browser/content/sync/customize.xul";
+ const features = "centerscreen,chrome,modal,dialog,resizable=no";
+ let win = Services.wm.getMostRecentWindow("navigator:browser");
+
+ let data = {accepted: false};
+ win.openDialog(url, "_blank", features, data);
+
+ return data;
+ },
+
+ initializeWithCurrentIdentity: function(isInitialSync=false) {
+ // While this function returns a promise that resolves once we've started
+ // the auth process, that process is complete when
+ // this.whenReadyToAuthenticate.promise resolves.
+ this._log.trace("initializeWithCurrentIdentity");
+
+ // Reset the world before we do anything async.
+ this.whenReadyToAuthenticate = Promise.defer();
+ this.whenReadyToAuthenticate.promise.then(null, (err) => {
+ this._log.error("Could not authenticate", err);
+ });
+
+ // initializeWithCurrentIdentity() can be called after the
+ // identity module was first initialized, e.g., after the
+ // user completes a force authentication, so we should make
+ // sure all credentials are reset before proceeding.
+ this.resetCredentials();
+ this._authFailureReason = null;
+
+ return this._fxaService.getSignedInUser().then(accountData => {
+ if (!accountData) {
+ this._log.info("initializeWithCurrentIdentity has no user logged in");
+ this.account = null;
+ // and we are as ready as we can ever be for auth.
+ this._shouldHaveSyncKeyBundle = true;
+ this.whenReadyToAuthenticate.reject("no user is logged in");
+ return;
+ }
+
+ this.account = accountData.email;
+ this._updateSignedInUser(accountData);
+ // The user must be verified before we can do anything at all; we kick
+ // this and the rest of initialization off in the background (ie, we
+ // don't return the promise)
+ this._log.info("Waiting for user to be verified.");
+ this._fxaService.whenVerified(accountData).then(accountData => {
+ this._updateSignedInUser(accountData);
+ this._log.info("Starting fetch for key bundle.");
+ if (this.needsCustomization) {
+ let data = this.offerSyncOptions();
+ if (data.accepted) {
+ Services.prefs.clearUserPref(PREF_SYNC_SHOW_CUSTOMIZATION);
+
+ // Mark any non-selected engines as declined.
+ Weave.Service.engineManager.declineDisabled();
+ } else {
+ // Log out if the user canceled the dialog.
+ return this._fxaService.signOut();
+ }
+ }
+ }).then(() => {
+ return this._fetchTokenForUser();
+ }).then(token => {
+ this._token = token;
+ this._shouldHaveSyncKeyBundle = true; // and we should actually have one...
+ this.whenReadyToAuthenticate.resolve();
+ this._log.info("Background fetch for key bundle done");
+ Weave.Status.login = LOGIN_SUCCEEDED;
+ if (isInitialSync) {
+ this._log.info("Doing initial sync actions");
+ Svc.Prefs.set("firstSync", "resetClient");
+ Services.obs.notifyObservers(null, "weave:service:setup-complete", null);
+ Weave.Utils.nextTick(Weave.Service.sync, Weave.Service);
+ }
+ }).then(null, err => {
+ this._shouldHaveSyncKeyBundle = true; // but we probably don't have one...
+ this.whenReadyToAuthenticate.reject(err);
+ // report what failed...
+ this._log.error("Background fetch for key bundle failed", err);
+ });
+ // and we are done - the fetch continues on in the background...
+ }).then(null, err => {
+ this._log.error("Processing logged in account", err);
+ });
+ },
+
+ _updateSignedInUser: function(userData) {
+ // This object should only ever be used for a single user. It is an
+ // error to update the data if the user changes (but updates are still
+ // necessary, as each call may add more attributes to the user).
+ // We start with no user, so an initial update is always ok.
+ if (this._signedInUser && this._signedInUser.email != userData.email) {
+ throw new Error("Attempting to update to a different user.")
+ }
+ this._signedInUser = userData;
+ },
+
+ logout: function() {
+ // This will be called when sync fails (or when the account is being
+ // unlinked etc). It may have failed because we got a 401 from a sync
+ // server, so we nuke the token. Next time sync runs and wants an
+ // authentication header, we will notice the lack of the token and fetch a
+ // new one.
+ this._token = null;
+ },
+
+ observe: function (subject, topic, data) {
+ this._log.debug("observed " + topic);
+ switch (topic) {
+ case fxAccountsCommon.ONLOGIN_NOTIFICATION:
+ // This should only happen if we've been initialized without a current
+ // user - otherwise we'd have seen the LOGOUT notification and been
+ // thrown away.
+ // The exception is when we've initialized with a user that needs to
+ // reauth with the server - in that case we will also get here, but
+ // should have the same identity.
+ // initializeWithCurrentIdentity will throw and log if these constraints
+ // aren't met, so just go ahead and do the init.
+ this.initializeWithCurrentIdentity(true);
+ break;
+
+ case fxAccountsCommon.ONLOGOUT_NOTIFICATION:
+ Weave.Service.startOver();
+ // startOver will cause this instance to be thrown away, so there's
+ // nothing else to do.
+ break;
+ }
+ },
+
+ /**
+ * Compute the sha256 of the message bytes. Return bytes.
+ */
+ _sha256: function(message) {
+ let hasher = Cc["@mozilla.org/security/hash;1"]
+ .createInstance(Ci.nsICryptoHash);
+ hasher.init(hasher.SHA256);
+ return CryptoUtils.digestBytes(message, hasher);
+ },
+
+ /**
+ * Compute the X-Client-State header given the byte string kB.
+ *
+ * Return string: hex(first16Bytes(sha256(kBbytes)))
+ */
+ _computeXClientState: function(kBbytes) {
+ return CommonUtils.bytesAsHex(this._sha256(kBbytes).slice(0, 16), false);
+ },
+
+ /**
+ * Provide override point for testing token expiration.
+ */
+ _now: function() {
+ return this._fxaService.now()
+ },
+
+ get _localtimeOffsetMsec() {
+ return this._fxaService.localtimeOffsetMsec;
+ },
+
+ usernameFromAccount: function(val) {
+ // we don't differentiate between "username" and "account"
+ return val;
+ },
+
+ /**
+ * Obtains the HTTP Basic auth password.
+ *
+ * Returns a string if set or null if it is not set.
+ */
+ get basicPassword() {
+ this._log.error("basicPassword getter should be not used in BrowserIDManager");
+ return null;
+ },
+
+ /**
+ * Set the HTTP basic password to use.
+ *
+ * Changes will not persist unless persistSyncCredentials() is called.
+ */
+ set basicPassword(value) {
+ throw "basicPassword setter should be not used in BrowserIDManager";
+ },
+
+ /**
+ * Obtain the Sync Key.
+ *
+ * This returns a 26 character "friendly" Base32 encoded string on success or
+ * null if no Sync Key could be found.
+ *
+ * If the Sync Key hasn't been set in this session, this will look in the
+ * password manager for the sync key.
+ */
+ get syncKey() {
+ if (this.syncKeyBundle) {
+ // TODO: This is probably fine because the code shouldn't be
+ // using the sync key directly (it should use the sync key
+ // bundle), but I don't like it. We should probably refactor
+ // code that is inspecting this to not do validation on this
+ // field directly and instead call a isSyncKeyValid() function
+ // that we can override.
+ return "99999999999999999999999999";
+ }
+ else {
+ return null;
+ }
+ },
+
+ set syncKey(value) {
+ throw "syncKey setter should be not used in BrowserIDManager";
+ },
+
+ get syncKeyBundle() {
+ return this._syncKeyBundle;
+ },
+
+ /**
+ * Resets/Drops all credentials we hold for the current user.
+ */
+ resetCredentials: function() {
+ this.resetSyncKey();
+ this._token = null;
+ },
+
+ /**
+ * Resets/Drops the sync key we hold for the current user.
+ */
+ resetSyncKey: function() {
+ this._syncKey = null;
+ this._syncKeyBundle = null;
+ this._syncKeyUpdated = true;
+ this._shouldHaveSyncKeyBundle = false;
+ },
+
+ /**
+ * Pre-fetches any information that might help with migration away from this
+ * identity. Called after every sync and is really just an optimization that
+ * allows us to avoid a network request for when we actually need the
+ * migration info.
+ */
+ prefetchMigrationSentinel: function(service) {
+ // nothing to do here until we decide to migrate away from FxA.
+ },
+
+ /**
+ * Return credentials hosts for this identity only.
+ */
+ _getSyncCredentialsHosts: function() {
+ return Utils.getSyncCredentialsHostsFxA();
+ },
+
+ /**
+ * The current state of the auth credentials.
+ *
+ * This essentially validates that enough credentials are available to use
+ * Sync. It doesn't check we have all the keys we need as the master-password
+ * may have been locked when we tried to get them - we rely on
+ * unlockAndVerifyAuthState to check that for us.
+ */
+ get currentAuthState() {
+ if (this._authFailureReason) {
+ this._log.info("currentAuthState returning " + this._authFailureReason +
+ " due to previous failure");
+ return this._authFailureReason;
+ }
+ // TODO: need to revisit this. Currently this isn't ready to go until
+ // both the username and syncKeyBundle are both configured and having no
+ // username seems to make things fail fast so that's good.
+ if (!this.username) {
+ return LOGIN_FAILED_NO_USERNAME;
+ }
+
+ return STATUS_OK;
+ },
+
+ // Do we currently have keys, or do we have enough that we should be able
+ // to successfully fetch them?
+ _canFetchKeys: function() {
+ let userData = this._signedInUser;
+ // a keyFetchToken means we can almost certainly grab them.
+ // kA and kB means we already have them.
+ return userData && (userData.keyFetchToken || (userData.kA && userData.kB));
+ },
+
+ /**
+ * Verify the current auth state, unlocking the master-password if necessary.
+ *
+ * Returns a promise that resolves with the current auth state after
+ * attempting to unlock.
+ */
+ unlockAndVerifyAuthState: function() {
+ if (this._canFetchKeys()) {
+ log.debug("unlockAndVerifyAuthState already has (or can fetch) sync keys");
+ return Promise.resolve(STATUS_OK);
+ }
+ // so no keys - ensure MP unlocked.
+ if (!Utils.ensureMPUnlocked()) {
+ // user declined to unlock, so we don't know if they are stored there.
+ log.debug("unlockAndVerifyAuthState: user declined to unlock master-password");
+ return Promise.resolve(MASTER_PASSWORD_LOCKED);
+ }
+ // now we are unlocked we must re-fetch the user data as we may now have
+ // the details that were previously locked away.
+ return this._fxaService.getSignedInUser().then(
+ accountData => {
+ this._updateSignedInUser(accountData);
+ // If we still can't get keys it probably means the user authenticated
+ // without unlocking the MP or cleared the saved logins, so we've now
+ // lost them - the user will need to reauth before continuing.
+ let result = this._canFetchKeys() ? STATUS_OK : LOGIN_FAILED_LOGIN_REJECTED;
+ log.debug("unlockAndVerifyAuthState re-fetched credentials and is returning", result);
+ return result;
+ }
+ );
+ },
+
+ /**
+ * Do we have a non-null, not yet expired token for the user currently
+ * signed in?
+ */
+ hasValidToken: function() {
+ // If pref is set to ignore cached authentication credentials for debugging,
+ // then return false to force the fetching of a new token.
+ let ignoreCachedAuthCredentials = false;
+ try {
+ ignoreCachedAuthCredentials = Svc.Prefs.get("debug.ignoreCachedAuthCredentials");
+ } catch(e) {
+ // Pref doesn't exist
+ }
+ if (ignoreCachedAuthCredentials) {
+ return false;
+ }
+ if (!this._token) {
+ return false;
+ }
+ if (this._token.expiration < this._now()) {
+ return false;
+ }
+ return true;
+ },
+
+ // Refresh the sync token for our user. Returns a promise that resolves
+ // with a token (which may be null in one sad edge-case), or rejects with an
+ // error.
+ _fetchTokenForUser: function() {
+ let tokenServerURI = Svc.Prefs.get("tokenServerURI");
+ if (tokenServerURI.endsWith("/")) { // trailing slashes cause problems...
+ tokenServerURI = tokenServerURI.slice(0, -1);
+ }
+ let log = this._log;
+ let client = this._tokenServerClient;
+ let fxa = this._fxaService;
+ let userData = this._signedInUser;
+
+ // We need kA and kB for things to work. If we don't have them, just
+ // return null for the token - sync calling unlockAndVerifyAuthState()
+ // before actually syncing will setup the error states if necessary.
+ if (!this._canFetchKeys()) {
+ log.info("Unable to fetch keys (master-password locked?), so aborting token fetch");
+ return Promise.resolve(null);
+ }
+
+ let maybeFetchKeys = () => {
+ // This is called at login time and every time we need a new token - in
+ // the latter case we already have kA and kB, so optimise that case.
+ if (userData.kA && userData.kB) {
+ return;
+ }
+ log.info("Fetching new keys");
+ return this._fxaService.getKeys().then(
+ newUserData => {
+ userData = newUserData;
+ this._updateSignedInUser(userData); // throws if the user changed.
+ }
+ );
+ }
+
+ let getToken = (tokenServerURI, assertion) => {
+ log.debug("Getting a token");
+ let deferred = Promise.defer();
+ let cb = function (err, token) {
+ if (err) {
+ return deferred.reject(err);
+ }
+ log.debug("Successfully got a sync token");
+ return deferred.resolve(token);
+ };
+
+ let kBbytes = CommonUtils.hexToBytes(userData.kB);
+ let headers = {"X-Client-State": this._computeXClientState(kBbytes)};
+ client.getTokenFromBrowserIDAssertion(tokenServerURI, assertion, cb, headers);
+ return deferred.promise;
+ }
+
+ let getAssertion = () => {
+ log.info("Getting an assertion from", tokenServerURI);
+ let audience = Services.io.newURI(tokenServerURI, null, null).prePath;
+ return fxa.getAssertion(audience);
+ };
+
+ // wait until the account email is verified and we know that
+ // getAssertion() will return a real assertion (not null).
+ return fxa.whenVerified(this._signedInUser)
+ .then(() => maybeFetchKeys())
+ .then(() => getAssertion())
+ .then(assertion => getToken(tokenServerURI, assertion))
+ .then(token => {
+ // TODO: Make it be only 80% of the duration, so refresh the token
+ // before it actually expires. This is to avoid sync storage errors
+ // otherwise, we get a nasty notification bar briefly. Bug 966568.
+ token.expiration = this._now() + (token.duration * 1000) * 0.80;
+ if (!this._syncKeyBundle) {
+ // We are given kA/kB as hex.
+ this._syncKeyBundle = deriveKeyBundle(Utils.hexToBytes(userData.kB));
+ }
+ return token;
+ })
+ .then(null, err => {
+ // TODO: unify these errors - we need to handle errors thrown by
+ // both tokenserverclient and hawkclient.
+ // A tokenserver error thrown based on a bad response.
+ if (err.response && err.response.status === 401) {
+ err = new AuthenticationError(err);
+ // A hawkclient error.
+ } else if (err.code && err.code === 401) {
+ err = new AuthenticationError(err);
+ }
+
+ // TODO: write tests to make sure that different auth error cases are handled here
+ // properly: auth error getting assertion, auth error getting token (invalid generation
+ // and client-state error)
+ if (err instanceof AuthenticationError) {
+ this._log.error("Authentication error in _fetchTokenForUser", err);
+ // set it to the "fatal" LOGIN_FAILED_LOGIN_REJECTED reason.
+ this._authFailureReason = LOGIN_FAILED_LOGIN_REJECTED;
+ } else {
+ this._log.error("Non-authentication error in _fetchTokenForUser", err);
+ // for now assume it is just a transient network related problem
+ // (although sadly, it might also be a regular unhandled exception)
+ this._authFailureReason = LOGIN_FAILED_NETWORK_ERROR;
+ }
+ // this._authFailureReason being set to be non-null in the above if clause
+ // ensures we are in the correct currentAuthState, and
+ // this._shouldHaveSyncKeyBundle being true ensures everything that cares knows
+ // that there is no authentication dance still under way.
+ this._shouldHaveSyncKeyBundle = true;
+ Weave.Status.login = this._authFailureReason;
+ Services.obs.notifyObservers(null, "weave:ui:login:error", null);
+ throw err;
+ });
+ },
+
+ // Returns a promise that is resolved when we have a valid token for the
+ // current user stored in this._token. When resolved, this._token is valid.
+ _ensureValidToken: function() {
+ if (this.hasValidToken()) {
+ this._log.debug("_ensureValidToken already has one");
+ return Promise.resolve();
+ }
+ // reset this._token as a safety net to reduce the possibility of us
+ // repeatedly attempting to use an invalid token if _fetchTokenForUser throws.
+ this._token = null;
+ return this._fetchTokenForUser().then(
+ token => {
+ this._token = token;
+ }
+ );
+ },
+
+ getResourceAuthenticator: function () {
+ return this._getAuthenticationHeader.bind(this);
+ },
+
+ /**
+ * Obtain a function to be used for adding auth to RESTRequest instances.
+ */
+ getRESTRequestAuthenticator: function() {
+ return this._addAuthenticationHeader.bind(this);
+ },
+
+ /**
+ * @return a Hawk HTTP Authorization Header, lightly wrapped, for the .uri
+ * of a RESTRequest or AsyncResponse object.
+ */
+ _getAuthenticationHeader: function(httpObject, method) {
+ let cb = Async.makeSpinningCallback();
+ this._ensureValidToken().then(cb, cb);
+ try {
+ cb.wait();
+ } catch (ex) {
+ this._log.error("Failed to fetch a token for authentication", ex);
+ return null;
+ }
+ if (!this._token) {
+ return null;
+ }
+ let credentials = {algorithm: "sha256",
+ id: this._token.id,
+ key: this._token.key,
+ };
+ method = method || httpObject.method;
+
+ // Get the local clock offset from the Firefox Accounts server. This should
+ // be close to the offset from the storage server.
+ let options = {
+ now: this._now(),
+ localtimeOffsetMsec: this._localtimeOffsetMsec,
+ credentials: credentials,
+ };
+
+ let headerValue = CryptoUtils.computeHAWK(httpObject.uri, method, options);
+ return {headers: {authorization: headerValue.field}};
+ },
+
+ _addAuthenticationHeader: function(request, method) {
+ let header = this._getAuthenticationHeader(request, method);
+ if (!header) {
+ return null;
+ }
+ request.setHeader("authorization", header.headers.authorization);
+ return request;
+ },
+
+ createClusterManager: function(service) {
+ return new BrowserIDClusterManager(service);
+ }
+
+};
+
+/* An implementation of the ClusterManager for this identity
+ */
+
+function BrowserIDClusterManager(service) {
+ ClusterManager.call(this, service);
+}
+
+BrowserIDClusterManager.prototype = {
+ __proto__: ClusterManager.prototype,
+
+ _findCluster: function() {
+ let endPointFromIdentityToken = function() {
+ // The only reason (in theory ;) that we can end up with a null token
+ // is when this.identity._canFetchKeys() returned false. In turn, this
+ // should only happen if the master-password is locked or the credentials
+ // storage is screwed, and in those cases we shouldn't have started
+ // syncing so shouldn't get here anyway.
+ // But better safe than sorry! To keep things clearer, throw an explicit
+ // exception - the message will appear in the logs and the error will be
+ // treated as transient.
+ if (!this.identity._token) {
+ throw new Error("Can't get a cluster URL as we can't fetch keys.");
+ }
+ let endpoint = this.identity._token.endpoint;
+ // For Sync 1.5 storage endpoints, we use the base endpoint verbatim.
+ // However, it should end in "/" because we will extend it with
+ // well known path components. So we add a "/" if it's missing.
+ if (!endpoint.endsWith("/")) {
+ endpoint += "/";
+ }
+ log.debug("_findCluster returning " + endpoint);
+ return endpoint;
+ }.bind(this);
+
+ // Spinningly ensure we are ready to authenticate and have a valid token.
+ let promiseClusterURL = function() {
+ return this.identity.whenReadyToAuthenticate.promise.then(
+ () => {
+ // We need to handle node reassignment here. If we are being asked
+ // for a clusterURL while the service already has a clusterURL, then
+ // it's likely a 401 was received using the existing token - in which
+ // case we just discard the existing token and fetch a new one.
+ if (this.service.clusterURL) {
+ log.debug("_findCluster found existing clusterURL, so discarding the current token");
+ this.identity._token = null;
+ }
+ return this.identity._ensureValidToken();
+ }
+ ).then(endPointFromIdentityToken
+ );
+ }.bind(this);
+
+ let cb = Async.makeSpinningCallback();
+ promiseClusterURL().then(function (clusterURL) {
+ cb(null, clusterURL);
+ }).then(
+ null, err => {
+ log.info("Failed to fetch the cluster URL", err);
+ // service.js's verifyLogin() method will attempt to fetch a cluster
+ // URL when it sees a 401. If it gets null, it treats it as a "real"
+ // auth error and sets Status.login to LOGIN_FAILED_LOGIN_REJECTED, which
+ // in turn causes a notification bar to appear informing the user they
+ // need to re-authenticate.
+ // On the other hand, if fetching the cluster URL fails with an exception,
+ // verifyLogin() assumes it is a transient error, and thus doesn't show
+ // the notification bar under the assumption the issue will resolve
+ // itself.
+ // Thus:
+ // * On a real 401, we must return null.
+ // * On any other problem we must let an exception bubble up.
+ if (err instanceof AuthenticationError) {
+ // callback with no error and a null result - cb.wait() returns null.
+ cb(null, null);
+ } else {
+ // callback with an error - cb.wait() completes by raising an exception.
+ cb(err);
+ }
+ });
+ return cb.wait();
+ },
+
+ getUserBaseURL: function() {
+ // Legacy Sync and FxA Sync construct the userBaseURL differently. Legacy
+ // Sync appends path components onto an empty path, and in FxA Sync the
+ // token server constructs this for us in an opaque manner. Since the
+ // cluster manager already sets the clusterURL on Service and also has
+ // access to the current identity, we added this functionality here.
+ return this.service.clusterURL;
+ }
+}
diff --git a/services/sync/modules/constants.js b/services/sync/modules/constants.js
index 9b6535d34..c8d66d921 100644
--- a/services/sync/modules/constants.js
+++ b/services/sync/modules/constants.js
@@ -54,6 +54,9 @@ HMAC_EVENT_INTERVAL: 600000,
// How long to wait between sync attempts if the Master Password is locked.
MASTER_PASSWORD_LOCKED_RETRY_INTERVAL: 15 * 60 * 1000, // 15 minutes
+// The default for how long we "block" sync from running when doing a migration.
+DEFAULT_BLOCK_PERIOD: 2 * 24 * 60 * 60 * 1000, // 2 days
+
// Separate from the ID fetch batch size to allow tuning for mobile.
MOBILE_BATCH_SIZE: 50,
@@ -119,6 +122,7 @@ LOGIN_FAILED_NETWORK_ERROR: "error.login.reason.network",
LOGIN_FAILED_SERVER_ERROR: "error.login.reason.server",
LOGIN_FAILED_INVALID_PASSPHRASE: "error.login.reason.recoverykey",
LOGIN_FAILED_LOGIN_REJECTED: "error.login.reason.account",
+LOGIN_FAILED_NOT_READY: "error.login.reason.initializing",
// sync failure status codes
METARECORD_DOWNLOAD_FAIL: "error.sync.reason.metarecord_download_fail",
diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js
index 74be244e8..49569f11d 100644
--- a/services/sync/modules/engines.js
+++ b/services/sync/modules/engines.js
@@ -13,7 +13,7 @@ this.EXPORTED_SYMBOLS = [
const {classes: Cc, interfaces: Ci, results: Cr, utils: Cu} = Components;
Cu.import("resource://services-common/async.js");
-Cu.import("resource://services-common/log4moz.js");
+Cu.import("resource://gre/modules/Log.jsm");
Cu.import("resource://services-common/observers.js");
Cu.import("resource://services-common/utils.js");
Cu.import("resource://services-sync/constants.js");
@@ -42,16 +42,22 @@ this.Tracker = function Tracker(name, engine) {
this.name = this.file = name.toLowerCase();
this.engine = engine;
- this._log = Log4Moz.repository.getLogger("Sync.Tracker." + name);
+ this._log = Log.repository.getLogger("Sync.Tracker." + name);
let level = Svc.Prefs.get("log.logger.engine." + this.name, "Debug");
- this._log.level = Log4Moz.Level[level];
+ this._log.level = Log.Level[level];
this._score = 0;
this._ignored = [];
this.ignoreAll = false;
this.changedIDs = {};
this.loadChangedIDs();
-}
+
+ Svc.Obs.add("weave:engine:start-tracking", this);
+ Svc.Obs.add("weave:engine:stop-tracking", this);
+
+ Svc.Prefs.observe("engine." + this.engine.prefName, this);
+};
+
Tracker.prototype = {
/*
* Score can be called as often as desired to decide which engines to sync
@@ -73,7 +79,7 @@ Tracker.prototype = {
},
// Should be called by service everytime a sync has been done for an engine
- resetScore: function T_resetScore() {
+ resetScore: function () {
this._score = 0;
},
@@ -88,7 +94,7 @@ Tracker.prototype = {
this._log.debug("Not saving changedIDs.");
return;
}
- Utils.namedTimer(function() {
+ Utils.namedTimer(function () {
this._log.debug("Saving changed IDs to " + this.file);
Utils.jsonSave("changes/" + this.file, this, this.changedIDs, cb);
}, 1000, this, "_lazySave");
@@ -112,39 +118,43 @@ Tracker.prototype = {
// being processed, or that shouldn't be synced.
// But note: not persisted to disk
- ignoreID: function T_ignoreID(id) {
+ ignoreID: function (id) {
this.unignoreID(id);
this._ignored.push(id);
},
- unignoreID: function T_unignoreID(id) {
+ unignoreID: function (id) {
let index = this._ignored.indexOf(id);
if (index != -1)
this._ignored.splice(index, 1);
},
- addChangedID: function addChangedID(id, when) {
+ addChangedID: function (id, when) {
if (!id) {
this._log.warn("Attempted to add undefined ID to tracker");
return false;
}
- if (this.ignoreAll || (id in this._ignored))
+
+ if (this.ignoreAll || (id in this._ignored)) {
return false;
+ }
- // Default to the current time in seconds if no time is provided
- if (when == null)
+ // Default to the current time in seconds if no time is provided.
+ if (when == null) {
when = Math.floor(Date.now() / 1000);
+ }
- // Add/update the entry if we have a newer time
+ // Add/update the entry if we have a newer time.
if ((this.changedIDs[id] || -Infinity) < when) {
this._log.trace("Adding changed ID: " + id + ", " + when);
this.changedIDs[id] = when;
this.saveChangedIDs(this.onSavedChangedIDs);
}
+
return true;
},
- removeChangedID: function T_removeChangedID(id) {
+ removeChangedID: function (id) {
if (!id) {
this._log.warn("Attempted to remove undefined ID to tracker");
return false;
@@ -159,10 +169,69 @@ Tracker.prototype = {
return true;
},
- clearChangedIDs: function T_clearChangedIDs() {
+ clearChangedIDs: function () {
this._log.trace("Clearing changed ID list");
this.changedIDs = {};
this.saveChangedIDs();
+ },
+
+ _isTracking: false,
+
+ // Override these in your subclasses.
+ startTracking: function () {
+ },
+
+ stopTracking: function () {
+ },
+
+ engineIsEnabled: function () {
+ if (!this.engine) {
+ // Can't tell -- we must be running in a test!
+ return true;
+ }
+ return this.engine.enabled;
+ },
+
+ onEngineEnabledChanged: function (engineEnabled) {
+ if (engineEnabled == this._isTracking) {
+ return;
+ }
+
+ if (engineEnabled) {
+ this.startTracking();
+ this._isTracking = true;
+ } else {
+ this.stopTracking();
+ this._isTracking = false;
+ this.clearChangedIDs();
+ }
+ },
+
+ observe: function (subject, topic, data) {
+ switch (topic) {
+ case "weave:engine:start-tracking":
+ if (!this.engineIsEnabled()) {
+ return;
+ }
+ this._log.trace("Got start-tracking.");
+ if (!this._isTracking) {
+ this.startTracking();
+ this._isTracking = true;
+ }
+ return;
+ case "weave:engine:stop-tracking":
+ this._log.trace("Got stop-tracking.");
+ if (this._isTracking) {
+ this.stopTracking();
+ this._isTracking = false;
+ }
+ return;
+ case "nsPref:changed":
+ if (data == PREFS_BRANCH + "engine." + this.engine.prefName) {
+ this.onEngineEnabledChanged(this.engine.enabled);
+ }
+ return;
+ }
}
};
@@ -197,9 +266,9 @@ this.Store = function Store(name, engine) {
this.name = name.toLowerCase();
this.engine = engine;
- this._log = Log4Moz.repository.getLogger("Sync.Store." + name);
+ this._log = Log.repository.getLogger("Sync.Store." + name);
let level = Svc.Prefs.get("log.logger.engine." + this.name, "Debug");
- this._log.level = Log4Moz.Level[level];
+ this._log.level = Log.Level[level];
XPCOMUtils.defineLazyGetter(this, "_timer", function() {
return Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
@@ -228,7 +297,7 @@ Store.prototype = {
* @param records Array of records to apply
* @return Array of record IDs which did not apply cleanly
*/
- applyIncomingBatch: function applyIncomingBatch(records) {
+ applyIncomingBatch: function (records) {
let failed = [];
for each (let record in records) {
try {
@@ -260,7 +329,7 @@ Store.prototype = {
* @param record
* Record to apply
*/
- applyIncoming: function Store_applyIncoming(record) {
+ applyIncoming: function (record) {
if (record.deleted)
this.remove(record);
else if (!this.itemExists(record.id))
@@ -280,7 +349,7 @@ Store.prototype = {
* @param record
* The store record to create an item from
*/
- create: function Store_create(record) {
+ create: function (record) {
throw "override create in a subclass";
},
@@ -293,7 +362,7 @@ Store.prototype = {
* @param record
* The store record to delete an item from
*/
- remove: function Store_remove(record) {
+ remove: function (record) {
throw "override remove in a subclass";
},
@@ -306,7 +375,7 @@ Store.prototype = {
* @param record
* The record to use to update an item from
*/
- update: function Store_update(record) {
+ update: function (record) {
throw "override update in a subclass";
},
@@ -320,7 +389,7 @@ Store.prototype = {
* string record ID
* @return boolean indicating whether record exists locally
*/
- itemExists: function Store_itemExists(id) {
+ itemExists: function (id) {
throw "override itemExists in a subclass";
},
@@ -338,7 +407,7 @@ Store.prototype = {
* constructor for the newly-created record.
* @return record type for this engine
*/
- createRecord: function Store_createRecord(id, collection) {
+ createRecord: function (id, collection) {
throw "override createRecord in a subclass";
},
@@ -350,7 +419,7 @@ Store.prototype = {
* @param newID
* string new record ID
*/
- changeItemID: function Store_changeItemID(oldID, newID) {
+ changeItemID: function (oldID, newID) {
throw "override changeItemID in a subclass";
},
@@ -360,7 +429,7 @@ Store.prototype = {
* @return Object with ID strings as keys and values of true. The values
* are ignored.
*/
- getAllIDs: function Store_getAllIDs() {
+ getAllIDs: function () {
throw "override getAllIDs in a subclass";
},
@@ -374,7 +443,7 @@ Store.prototype = {
* can be thought of as clearing out all state and restoring the "new
* browser" state.
*/
- wipe: function Store_wipe() {
+ wipe: function () {
throw "override wipe in a subclass";
}
};
@@ -383,19 +452,22 @@ this.EngineManager = function EngineManager(service) {
this.service = service;
this._engines = {};
- this._log = Log4Moz.repository.getLogger("Sync.EngineManager");
- this._log.level = Log4Moz.Level[Svc.Prefs.get(
- "log.logger.service.engines", "Debug")];
+
+ // This will be populated by Service on startup.
+ this._declined = new Set();
+ this._log = Log.repository.getLogger("Sync.EngineManager");
+ this._log.level = Log.Level[Svc.Prefs.get("log.logger.service.engines", "Debug")];
}
EngineManager.prototype = {
- get: function get(name) {
+ get: function (name) {
// Return an array of engines if we have an array of names
if (Array.isArray(name)) {
let engines = [];
name.forEach(function(name) {
let engine = this.get(name);
- if (engine)
+ if (engine) {
engines.push(engine);
+ }
}, this);
return engines;
}
@@ -403,18 +475,80 @@ EngineManager.prototype = {
let engine = this._engines[name];
if (!engine) {
this._log.debug("Could not get engine: " + name);
- if (Object.keys)
+ if (Object.keys) {
this._log.debug("Engines are: " + JSON.stringify(Object.keys(this._engines)));
+ }
}
return engine;
},
- getAll: function getAll() {
+ getAll: function () {
return [engine for ([name, engine] in Iterator(this._engines))];
},
- getEnabled: function getEnabled() {
- return this.getAll().filter(function(engine) engine.enabled);
+ /**
+ * N.B., does not pay attention to the declined list.
+ */
+ getEnabled: function () {
+ return this.getAll()
+ .filter((engine) => engine.enabled)
+ .sort((a, b) => a.syncPriority - b.syncPriority);
+ },
+
+ get enabledEngineNames() {
+ return [e.name for each (e in this.getEnabled())];
+ },
+
+ persistDeclined: function () {
+ Svc.Prefs.set("declinedEngines", [...this._declined].join(","));
+ },
+
+ /**
+ * Returns an array.
+ */
+ getDeclined: function () {
+ return [...this._declined];
+ },
+
+ setDeclined: function (engines) {
+ this._declined = new Set(engines);
+ this.persistDeclined();
+ },
+
+ isDeclined: function (engineName) {
+ return this._declined.has(engineName);
+ },
+
+ /**
+ * Accepts a Set or an array.
+ */
+ decline: function (engines) {
+ for (let e of engines) {
+ this._declined.add(e);
+ }
+ this.persistDeclined();
+ },
+
+ undecline: function (engines) {
+ for (let e of engines) {
+ this._declined.delete(e);
+ }
+ this.persistDeclined();
+ },
+
+ /**
+ * Mark any non-enabled engines as declined.
+ *
+ * This is useful after initial customization during setup.
+ */
+ declineDisabled: function () {
+ for (let e of this.getAll()) {
+ if (!e.enabled) {
+ this._log.debug("Declining disabled engine " + e.name);
+ this._declined.add(e.name);
+ }
+ }
+ this.persistDeclined();
},
/**
@@ -425,19 +559,20 @@ EngineManager.prototype = {
* Engine object used to get an instance of the engine
* @return The engine object if anything failed
*/
- register: function register(engineObject) {
- if (Array.isArray(engineObject))
+ register: function (engineObject) {
+ if (Array.isArray(engineObject)) {
return engineObject.map(this.register, this);
+ }
try {
let engine = new engineObject(this.service);
let name = engine.name;
- if (name in this._engines)
+ if (name in this._engines) {
this._log.error("Engine '" + name + "' is already registered!");
- else
+ } else {
this._engines[name] = engine;
- }
- catch(ex) {
+ }
+ } catch (ex) {
this._log.error(CommonUtils.exceptionStr(ex));
let mesg = ex.message ? ex.message : ex;
@@ -452,14 +587,15 @@ EngineManager.prototype = {
}
},
- unregister: function unregister(val) {
+ unregister: function (val) {
let name = val;
- if (val instanceof Engine)
+ if (val instanceof Engine) {
name = val.name;
+ }
delete this._engines[name];
},
- clear: function clear() {
+ clear: function () {
for (let name in this._engines) {
delete this._engines[name];
}
@@ -476,9 +612,9 @@ this.Engine = function Engine(name, service) {
this.service = service;
this._notify = Utils.notify("weave:engine:");
- this._log = Log4Moz.repository.getLogger("Sync.Engine." + this.Name);
+ this._log = Log.repository.getLogger("Sync.Engine." + this.Name);
let level = Svc.Prefs.get("log.logger.engine." + this.name, "Debug");
- this._log.level = Log4Moz.Level[level];
+ this._log.level = Log.Level[level];
this._tracker; // initialize tracker to load previously changed IDs
this._log.debug("Engine initialized");
@@ -492,45 +628,58 @@ Engine.prototype = {
// Signal to the engine that processing further records is pointless.
eEngineAbortApplyIncoming: "error.engine.abort.applyincoming",
- get prefName() this.name,
- get enabled() Svc.Prefs.get("engine." + this.prefName, false),
- set enabled(val) Svc.Prefs.set("engine." + this.prefName, !!val),
+ get prefName() {
+ return this.name;
+ },
+
+ get enabled() {
+ return Svc.Prefs.get("engine." + this.prefName, false);
+ },
- get score() this._tracker.score,
+ set enabled(val) {
+ Svc.Prefs.set("engine." + this.prefName, !!val);
+ },
+
+ get score() {
+ return this._tracker.score;
+ },
get _store() {
let store = new this._storeObj(this.Name, this);
- this.__defineGetter__("_store", function() store);
+ this.__defineGetter__("_store", () => store);
return store;
},
get _tracker() {
let tracker = new this._trackerObj(this.Name, this);
- this.__defineGetter__("_tracker", function() tracker);
+ this.__defineGetter__("_tracker", () => tracker);
return tracker;
},
- sync: function Engine_sync() {
- if (!this.enabled)
+ sync: function () {
+ if (!this.enabled) {
return;
+ }
- if (!this._sync)
+ if (!this._sync) {
throw "engine does not implement _sync method";
+ }
this._notify("sync", this.name, this._sync)();
},
/**
- * Get rid of any local meta-data
+ * Get rid of any local meta-data.
*/
- resetClient: function Engine_resetClient() {
- if (!this._resetClient)
+ resetClient: function () {
+ if (!this._resetClient) {
throw "engine does not implement _resetClient method";
+ }
this._notify("reset-client", this.name, this._resetClient)();
},
- _wipeClient: function Engine__wipeClient() {
+ _wipeClient: function () {
this.resetClient();
this._log.debug("Deleting all local data");
this._tracker.ignoreAll = true;
@@ -539,7 +688,7 @@ Engine.prototype = {
this._tracker.clearChangedIDs();
},
- wipeClient: function Engine_wipeClient() {
+ wipeClient: function () {
this._notify("wipe-client", this.name, this._wipeClient)();
}
};
@@ -563,27 +712,44 @@ SyncEngine.prototype = {
__proto__: Engine.prototype,
_recordObj: CryptoWrapper,
version: 1,
-
+
+ // Which sortindex to use when retrieving records for this engine.
+ _defaultSort: undefined,
+
+ // A relative priority to use when computing an order
+ // for engines to be synced. Higher-priority engines
+ // (lower numbers) are synced first.
+ // It is recommended that a unique value be used for each engine,
+ // in order to guarantee a stable sequence.
+ syncPriority: 0,
+
// How many records to pull in a single sync. This is primarily to avoid very
// long first syncs against profiles with many history records.
downloadLimit: null,
-
+
// How many records to pull at one time when specifying IDs. This is to avoid
// URI length limitations.
guidFetchBatchSize: DEFAULT_GUID_FETCH_BATCH_SIZE,
mobileGUIDFetchBatchSize: DEFAULT_MOBILE_GUID_FETCH_BATCH_SIZE,
-
+
// How many records to process in a single batch.
applyIncomingBatchSize: DEFAULT_STORE_BATCH_SIZE,
- get storageURL() Svc.Prefs.get("clusterURL") + SYNC_API_VERSION +
- "/" + this.service.identity.username + "/storage/",
+ get storageURL() {
+ return this.service.storageURL;
+ },
- get engineURL() this.storageURL + this.name,
+ get engineURL() {
+ return this.storageURL + this.name;
+ },
- get cryptoKeysURL() this.storageURL + "crypto/keys",
+ get cryptoKeysURL() {
+ return this.storageURL + "crypto/keys";
+ },
- get metaURL() this.storageURL + "meta/global",
+ get metaURL() {
+ return this.storageURL + "meta/global";
+ },
get syncID() {
// Generate a random syncID if we don't have one
@@ -606,27 +772,30 @@ SyncEngine.prototype = {
// Store the value as a string to keep floating point precision
Svc.Prefs.set(this.name + ".lastSync", value.toString());
},
- resetLastSync: function SyncEngine_resetLastSync() {
+ resetLastSync: function () {
this._log.debug("Resetting " + this.name + " last sync time");
Svc.Prefs.reset(this.name + ".lastSync");
Svc.Prefs.set(this.name + ".lastSync", "0");
this.lastSyncLocal = 0;
},
- get toFetch() this._toFetch,
+ get toFetch() {
+ return this._toFetch;
+ },
set toFetch(val) {
+ let cb = (error) => this._log.error(Utils.exceptionStr(error));
// Coerce the array to a string for more efficient comparison.
if (val + "" == this._toFetch) {
return;
}
this._toFetch = val;
Utils.namedTimer(function () {
- Utils.jsonSave("toFetch/" + this.name, this, val);
+ Utils.jsonSave("toFetch/" + this.name, this, val, cb);
}, 0, this, "_toFetchDelay");
},
- loadToFetch: function loadToFetch() {
- // Initialize to empty if there's no file
+ loadToFetch: function () {
+ // Initialize to empty if there's no file.
this._toFetch = [];
Utils.jsonLoad("toFetch/" + this.name, this, function(toFetch) {
if (toFetch) {
@@ -635,19 +804,22 @@ SyncEngine.prototype = {
});
},
- get previousFailed() this._previousFailed,
+ get previousFailed() {
+ return this._previousFailed;
+ },
set previousFailed(val) {
+ let cb = (error) => this._log.error(Utils.exceptionStr(error));
// Coerce the array to a string for more efficient comparison.
if (val + "" == this._previousFailed) {
return;
}
this._previousFailed = val;
Utils.namedTimer(function () {
- Utils.jsonSave("failed/" + this.name, this, val);
+ Utils.jsonSave("failed/" + this.name, this, val, cb);
}, 0, this, "_previousFailedDelay");
},
- loadPreviousFailed: function loadPreviousFailed() {
+ loadPreviousFailed: function () {
// Initialize to empty if there's no file
this._previousFailed = [];
Utils.jsonLoad("failed/" + this.name, this, function(previousFailed) {
@@ -673,12 +845,12 @@ SyncEngine.prototype = {
* can override this method to bypass the tracker for certain or all
* changed items.
*/
- getChangedIDs: function getChangedIDs() {
+ getChangedIDs: function () {
return this._tracker.changedIDs;
},
- // Create a new record using the store and add in crypto fields
- _createRecord: function SyncEngine__createRecord(id) {
+ // Create a new record using the store and add in crypto fields.
+ _createRecord: function (id) {
let record = this._store.createRecord(id, this.name);
record.id = id;
record.collection = this.name;
@@ -686,7 +858,7 @@ SyncEngine.prototype = {
},
// Any setup that needs to happen at the beginning of each sync.
- _syncStartup: function SyncEngine__syncStartup() {
+ _syncStartup: function () {
// Determine if we need to wipe on outdated versions
let metaGlobal = this.service.recordManager.get(this.metaURL);
@@ -783,13 +955,17 @@ SyncEngine.prototype = {
newitems = this._itemSource();
}
+ if (this._defaultSort) {
+ newitems.sort = this._defaultSort;
+ }
+
if (isMobile) {
batchSize = MOBILE_BATCH_SIZE;
}
newitems.newer = this.lastSync;
newitems.full = true;
newitems.limit = batchSize;
-
+
// applied => number of items that should be applied.
// failed => number of items that failed in this sync.
// newFailed => number of items that failed for the first time in this sync.
@@ -1031,11 +1207,11 @@ SyncEngine.prototype = {
*
* @return GUID of the similar item; falsy otherwise
*/
- _findDupe: function _findDupe(item) {
+ _findDupe: function (item) {
// By default, assume there's no dupe items for the engine
},
- _deleteId: function _deleteId(id) {
+ _deleteId: function (id) {
this._tracker.removeChangedID(id);
// Remember this id to delete at the end of sync
@@ -1055,8 +1231,8 @@ SyncEngine.prototype = {
* @return boolean
* Truthy if incoming record should be applied. False if not.
*/
- _reconcile: function _reconcile(item) {
- if (this._log.level <= Log4Moz.Level.Trace) {
+ _reconcile: function (item) {
+ if (this._log.level <= Log.Level.Trace) {
this._log.trace("Incoming: " + item);
}
@@ -1227,8 +1403,8 @@ SyncEngine.prototype = {
return remoteIsNewer;
},
- // Upload outgoing records
- _uploadOutgoing: function SyncEngine__uploadOutgoing() {
+ // Upload outgoing records.
+ _uploadOutgoing: function () {
this._log.trace("Uploading local changes to server.");
let modifiedIDs = Object.keys(this._modified);
@@ -1273,7 +1449,7 @@ SyncEngine.prototype = {
for each (let id in modifiedIDs) {
try {
let out = this._createRecord(id);
- if (this._log.level <= Log4Moz.Level.Trace)
+ if (this._log.level <= Log.Level.Trace)
this._log.trace("Outgoing: " + out);
out.encrypt(this.service.collectionKeys.keyForCollection(this.name));
@@ -1298,7 +1474,7 @@ SyncEngine.prototype = {
// Any cleanup necessary.
// Save the current snapshot so as to calculate changes at next sync
- _syncFinish: function SyncEngine__syncFinish() {
+ _syncFinish: function () {
this._log.trace("Finishing up sync");
this._tracker.resetScore();
@@ -1325,9 +1501,10 @@ SyncEngine.prototype = {
}
},
- _syncCleanup: function _syncCleanup() {
- if (!this._modified)
+ _syncCleanup: function () {
+ if (!this._modified) {
return;
+ }
// Mark failed WBOs as changed again so they are reuploaded next time.
for (let [id, when] in Iterator(this._modified)) {
@@ -1336,7 +1513,7 @@ SyncEngine.prototype = {
this._modified = {};
},
- _sync: function SyncEngine__sync() {
+ _sync: function () {
try {
this._syncStartup();
Observers.notify("weave:engine:sync:status", "process-incoming");
@@ -1349,7 +1526,7 @@ SyncEngine.prototype = {
}
},
- canDecrypt: function canDecrypt() {
+ canDecrypt: function () {
// Report failure even if there's nothing to decrypt
let canDecrypt = false;
@@ -1377,13 +1554,13 @@ SyncEngine.prototype = {
return canDecrypt;
},
- _resetClient: function SyncEngine__resetClient() {
+ _resetClient: function () {
this.resetLastSync();
this.previousFailed = [];
this.toFetch = [];
},
- wipeServer: function wipeServer() {
+ wipeServer: function () {
let response = this.service.resource(this.engineURL).delete();
if (response.status != 200 && response.status != 404) {
throw response;
@@ -1391,7 +1568,7 @@ SyncEngine.prototype = {
this._resetClient();
},
- removeClientData: function removeClientData() {
+ removeClientData: function () {
// Implement this method in engines that store client specific data
// on the server.
},
@@ -1412,7 +1589,7 @@ SyncEngine.prototype = {
*
* All return values will be part of the kRecoveryStrategy enumeration.
*/
- handleHMACMismatch: function handleHMACMismatch(item, mayRetry) {
+ handleHMACMismatch: function (item, mayRetry) {
// By default we either try again, or bail out noisily.
return (this.service.handleHMACEvent() && mayRetry) ?
SyncEngine.kRecoveryStrategy.retry :
diff --git a/services/sync/modules/engines/addons.js b/services/sync/modules/engines/addons.js
index d8cf014be..ab3131c30 100644
--- a/services/sync/modules/engines/addons.js
+++ b/services/sync/modules/engines/addons.js
@@ -48,7 +48,7 @@ Cu.import("resource://gre/modules/XPCOMUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "AddonManager",
"resource://gre/modules/AddonManager.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "AddonRepository",
- "resource://gre/modules/AddonRepository.jsm");
+ "resource://gre/modules/addons/AddonRepository.jsm");
this.EXPORTED_SYMBOLS = ["AddonsEngine"];
@@ -119,6 +119,8 @@ AddonsEngine.prototype = {
_recordObj: AddonRecord,
version: 1,
+ syncPriority: 5,
+
_reconciler: null,
/**
@@ -655,9 +657,6 @@ AddonsStore.prototype = {
*/
function AddonsTracker(name, engine) {
Tracker.call(this, name, engine);
-
- Svc.Obs.add("weave:engine:start-tracking", this);
- Svc.Obs.add("weave:engine:stop-tracking", this);
}
AddonsTracker.prototype = {
__proto__: Tracker.prototype,
@@ -691,20 +690,16 @@ AddonsTracker.prototype = {
this.score += SCORE_INCREMENT_XLARGE;
},
- observe: function(subject, topic, data) {
- switch (topic) {
- case "weave:engine:start-tracking":
- if (this.engine.enabled) {
- this.reconciler.startListening();
- }
+ startTracking: function() {
+ if (this.engine.enabled) {
+ this.reconciler.startListening();
+ }
- this.reconciler.addChangeListener(this);
- break;
+ this.reconciler.addChangeListener(this);
+ },
- case "weave:engine:stop-tracking":
- this.reconciler.removeChangeListener(this);
- this.reconciler.stopListening();
- break;
- }
- }
+ stopTracking: function() {
+ this.reconciler.removeChangeListener(this);
+ this.reconciler.stopListening();
+ },
};
diff --git a/services/sync/modules/engines/apps.js b/services/sync/modules/engines/apps.js
deleted file mode 100644
index 58967acad..000000000
--- a/services/sync/modules/engines/apps.js
+++ /dev/null
@@ -1,136 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-"use strict";
-
-this.EXPORTED_SYMBOLS = ['AppsEngine', 'AppRec'];
-
-const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
-
-Cu.import("resource://services-sync/util.js");
-Cu.import("resource://services-sync/record.js");
-Cu.import("resource://services-sync/engines.js");
-Cu.import("resource://services-sync/constants.js");
-Cu.import("resource://gre/modules/XPCOMUtils.jsm");
-Cu.import("resource://gre/modules/Webapps.jsm");
-
-this.AppRec = function AppRec(collection, id) {
- CryptoWrapper.call(this, collection, id);
-}
-
-AppRec.prototype = {
- __proto__: CryptoWrapper.prototype,
- _logName: "Sync.Record.App"
-}
-
-Utils.deferGetSet(AppRec, "cleartext", ["value"]);
-
-function AppStore(name, engine) {
- Store.call(this, name, engine);
-}
-
-AppStore.prototype = {
- __proto__: Store.prototype,
-
- getAllIDs: function getAllIDs() {
- let apps = DOMApplicationRegistry.getAllIDs();
- return apps;
- },
-
- changeItemID: function changeItemID(oldID, newID) {
- this._log.trace("AppsStore does not support changeItemID");
- },
-
- itemExists: function itemExists(guid) {
- return DOMApplicationRegistry.itemExists(guid);
- },
-
- createRecord: function createRecord(guid, collection) {
- let record = new AppRec(collection, guid);
- let app = DOMApplicationRegistry.getAppById(guid);
-
- if (app) {
- app.syncId = guid;
- let callback = Async.makeSyncCallback();
- DOMApplicationRegistry.getManifestFor(app.origin, function(aManifest) {
- app.manifest = aManifest;
- callback();
- });
- Async.waitForSyncCallback(callback);
- record.value = app;
- } else {
- record.deleted = true;
- }
-
- return record;
- },
-
- applyIncomingBatch: function applyIncomingBatch(aRecords) {
- let callback = Async.makeSyncCallback();
- DOMApplicationRegistry.updateApps(aRecords, callback);
- Async.waitForSyncCallback(callback);
- return [];
- },
-
- wipe: function wipe(record) {
- let callback = Async.makeSyncCallback();
- DOMApplicationRegistry.wipe(callback);
- Async.waitForSyncCallback(callback);
- }
-}
-
-
-function AppTracker(name, engine) {
- Tracker.call(this, name, engine);
- Svc.Obs.add("weave:engine:start-tracking", this);
- Svc.Obs.add("weave:engine:stop-tracking", this);
-}
-
-AppTracker.prototype = {
- __proto__: Tracker.prototype,
- QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver]),
-
- _enabled: false,
-
- observe: function(aSubject, aTopic, aData) {
- switch (aTopic) {
- case "webapps-sync-install":
- case "webapps-sync-uninstall":
- // ask for immediate sync. not sure if we really need this or
- // if a lower score increment would be enough
- let app;
- this.score += SCORE_INCREMENT_XLARGE;
- try {
- app = JSON.parse(aData);
- } catch (e) {
- this._log.error("JSON.parse failed in observer " + e);
- return;
- }
- this.addChangedID(app.id);
- break;
- case "weave:engine:start-tracking":
- this._enabled = true;
- Svc.Obs.add("webapps-sync-install", this);
- Svc.Obs.add("webapps-sync-uninstall", this);
- break;
- case "weave:engine:stop-tracking":
- this._enabled = false;
- Svc.Obs.remove("webapps-sync-install", this);
- Svc.Obs.remove("webapps-sync-uninstall", this);
- break;
- }
- }
-}
-
-this.AppsEngine = function AppsEngine(service) {
- SyncEngine.call(this, "Apps", service);
-}
-
-AppsEngine.prototype = {
- __proto__: SyncEngine.prototype,
- _storeObj: AppStore,
- _trackerObj: AppTracker,
- _recordObj: AppRec,
- applyIncomingBatchSize: APPS_STORE_BATCH_SIZE
-}
diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js
index 7ef86cc73..1936afc3f 100644
--- a/services/sync/modules/engines/bookmarks.js
+++ b/services/sync/modules/engines/bookmarks.js
@@ -185,12 +185,24 @@ let kSpecialIds = {
return null;
},
- get menu() PlacesUtils.bookmarksMenuFolderId,
- get places() PlacesUtils.placesRootId,
- get tags() PlacesUtils.tagsFolderId,
- get toolbar() PlacesUtils.toolbarFolderId,
- get unfiled() PlacesUtils.unfiledBookmarksFolderId,
- get mobile() this.findMobileRoot(true),
+ get menu() {
+ return PlacesUtils.bookmarksMenuFolderId;
+ },
+ get places() {
+ return PlacesUtils.placesRootId;
+ },
+ get tags() {
+ return PlacesUtils.tagsFolderId;
+ },
+ get toolbar() {
+ return PlacesUtils.toolbarFolderId;
+ },
+ get unfiled() {
+ return PlacesUtils.unfiledBookmarksFolderId;
+ },
+ get mobile() {
+ return this.findMobileRoot(true);
+ },
};
this.BookmarksEngine = function BookmarksEngine(service) {
@@ -202,6 +214,9 @@ BookmarksEngine.prototype = {
_storeObj: BookmarksStore,
_trackerObj: BookmarksTracker,
version: 2,
+ _defaultSort: "index",
+
+ syncPriority: 4,
_sync: function _sync() {
let engine = this;
@@ -349,31 +364,42 @@ BookmarksEngine.prototype = {
Task.spawn(function() {
// For first-syncs, make a backup for the user to restore
if (this.lastSync == 0) {
+ this._log.debug("Bookmarks backup starting.");
yield PlacesBackups.create(null, true);
+ this._log.debug("Bookmarks backup done.");
}
+ }.bind(this)).then(
+ cb, ex => {
+ // Failure to create a backup is somewhat bad, but probably not bad
+ // enough to prevent syncing of bookmarks - so just log the error and
+ // continue.
+ this._log.warn("Got exception \"" + Utils.exceptionStr(ex) +
+ "\" backing up bookmarks, but continuing with sync.");
+ cb();
+ }
+ );
- this.__defineGetter__("_guidMap", function() {
- // Create a mapping of folder titles and separator positions to GUID.
- // We do this lazily so that we don't do any work unless we reconcile
- // incoming items.
- let guidMap;
- try {
- guidMap = this._buildGUIDMap();
- } catch (ex) {
- this._log.warn("Got exception \"" + Utils.exceptionStr(ex) +
- "\" building GUID map." +
- " Skipping all other incoming items.");
- throw {code: Engine.prototype.eEngineAbortApplyIncoming,
- cause: ex};
- }
- delete this._guidMap;
- return this._guidMap = guidMap;
- });
-
- this._store._childrenToOrder = {};
- cb();
- }.bind(this));
cb.wait();
+
+ this.__defineGetter__("_guidMap", function() {
+ // Create a mapping of folder titles and separator positions to GUID.
+ // We do this lazily so that we don't do any work unless we reconcile
+ // incoming items.
+ let guidMap;
+ try {
+ guidMap = this._buildGUIDMap();
+ } catch (ex) {
+ this._log.warn("Got exception \"" + Utils.exceptionStr(ex) +
+ "\" building GUID map." +
+ " Skipping all other incoming items.");
+ throw {code: Engine.prototype.eEngineAbortApplyIncoming,
+ cause: ex};
+ }
+ delete this._guidMap;
+ return this._guidMap = guidMap;
+ });
+
+ this._store._childrenToOrder = {};
},
_processIncoming: function (newitems) {
@@ -428,7 +454,7 @@ function BookmarksStore(name, engine) {
// Explicitly nullify our references to our cached services so we don't leak
Svc.Obs.add("places-shutdown", function() {
- for each ([query, stmt] in Iterator(this._stmts)) {
+ for each (let [query, stmt] in Iterator(this._stmts)) {
stmt.finalize();
}
this._stmts = {};
@@ -731,10 +757,10 @@ BookmarksStore.prototype = {
feedURI: Utils.makeURI(record.feedUri),
siteURI: siteURI,
guid: record.id};
- PlacesUtils.livemarks.addLivemark(livemarkObj,
- function (aStatus, aLivemark) {
- spinningCb(null, [aStatus, aLivemark]);
- });
+ PlacesUtils.livemarks.addLivemark(livemarkObj).then(
+ aLivemark => { spinningCb(null, [Components.results.NS_OK, aLivemark]) },
+ () => { spinningCb(null, [Components.results.NS_ERROR_UNEXPECTED, aLivemark]) }
+ );
let [status, livemark] = spinningCb.wait();
if (!Components.isSuccessCode(status)) {
@@ -1257,7 +1283,7 @@ BookmarksStore.prototype = {
}
// Filter out any null/undefined/empty tags.
- tags = tags.filter(function(t) t);
+ tags = tags.filter(t => t);
// Temporarily tag a dummy URI to preserve tag ids when untagging.
let dummyURI = Utils.makeURI("about:weave#BStore_tagURI");
@@ -1298,34 +1324,28 @@ function BookmarksTracker(name, engine) {
Tracker.call(this, name, engine);
Svc.Obs.add("places-shutdown", this);
- Svc.Obs.add("weave:engine:start-tracking", this);
- Svc.Obs.add("weave:engine:stop-tracking", this);
}
BookmarksTracker.prototype = {
__proto__: Tracker.prototype,
- _enabled: false,
+ startTracking: function() {
+ PlacesUtils.bookmarks.addObserver(this, true);
+ Svc.Obs.add("bookmarks-restore-begin", this);
+ Svc.Obs.add("bookmarks-restore-success", this);
+ Svc.Obs.add("bookmarks-restore-failed", this);
+ },
+
+ stopTracking: function() {
+ PlacesUtils.bookmarks.removeObserver(this);
+ Svc.Obs.remove("bookmarks-restore-begin", this);
+ Svc.Obs.remove("bookmarks-restore-success", this);
+ Svc.Obs.remove("bookmarks-restore-failed", this);
+ },
+
observe: function observe(subject, topic, data) {
- switch (topic) {
- case "weave:engine:start-tracking":
- if (!this._enabled) {
- PlacesUtils.bookmarks.addObserver(this, true);
- Svc.Obs.add("bookmarks-restore-begin", this);
- Svc.Obs.add("bookmarks-restore-success", this);
- Svc.Obs.add("bookmarks-restore-failed", this);
- this._enabled = true;
- }
- break;
- case "weave:engine:stop-tracking":
- if (this._enabled) {
- PlacesUtils.bookmarks.removeObserver(this);
- Svc.Obs.remove("bookmarks-restore-begin", this);
- Svc.Obs.remove("bookmarks-restore-success", this);
- Svc.Obs.remove("bookmarks-restore-failed", this);
- this._enabled = false;
- }
- break;
+ Tracker.prototype.observe.call(this, subject, topic, data);
+ switch (topic) {
case "bookmarks-restore-begin":
this._log.debug("Ignoring changes from importing bookmarks.");
this.ignoreAll = true;
@@ -1437,9 +1457,9 @@ BookmarksTracker.prototype = {
},
_ensureMobileQuery: function _ensureMobileQuery() {
- let find = function (val)
+ let find = val =>
PlacesUtils.annotations.getItemsWithAnnotation(ORGANIZERQUERY_ANNO, {}).filter(
- function (id) PlacesUtils.annotations.getItemAnnotation(id, ORGANIZERQUERY_ANNO) == val
+ id => PlacesUtils.annotations.getItemAnnotation(id, ORGANIZERQUERY_ANNO) == val
);
// Don't continue if the Library isn't ready
diff --git a/services/sync/modules/engines/clients.js b/services/sync/modules/engines/clients.js
index e891ce119..f423242c9 100644
--- a/services/sync/modules/engines/clients.js
+++ b/services/sync/modules/engines/clients.js
@@ -18,6 +18,8 @@ Cu.import("resource://services-sync/util.js");
const CLIENTS_TTL = 1814400; // 21 days
const CLIENTS_TTL_REFRESH = 604800; // 7 days
+const SUPPORTED_PROTOCOL_VERSIONS = ["1.1", "1.5"];
+
this.ClientsRec = function ClientsRec(collection, id) {
CryptoWrapper.call(this, collection, id);
}
@@ -27,7 +29,11 @@ ClientsRec.prototype = {
ttl: CLIENTS_TTL
};
-Utils.deferGetSet(ClientsRec, "cleartext", ["name", "type", "commands"]);
+Utils.deferGetSet(ClientsRec,
+ "cleartext",
+ ["name", "type", "commands",
+ "version", "protocols",
+ "formfactor", "os", "appPackage", "application", "device"]);
this.ClientEngine = function ClientEngine(service) {
@@ -69,6 +75,28 @@ ClientEngine.prototype = {
return stats;
},
+ /**
+ * Obtain information about device types.
+ *
+ * Returns a Map of device types to integer counts.
+ */
+ get deviceTypes() {
+ let counts = new Map();
+
+ counts.set(this.localType, 1);
+
+ for each (let record in this._store._remoteClients) {
+ let type = record.type;
+ if (!counts.has(type)) {
+ counts.set(type, 0);
+ }
+
+ counts.set(type, counts.get(type) + 1);
+ }
+
+ return counts;
+ },
+
get localID() {
// Generate a random GUID id we don't have one
let localID = Svc.Prefs.get("client.GUID", "");
@@ -76,35 +104,17 @@ ClientEngine.prototype = {
},
set localID(value) Svc.Prefs.set("client.GUID", value),
+ get brandName() {
+ let brand = new StringBundle("chrome://branding/locale/brand.properties");
+ return brand.get("brandShortName");
+ },
+
get localName() {
let localName = Svc.Prefs.get("client.name", "");
if (localName != "")
return localName;
- // Generate a client name if we don't have a useful one yet
- let env = Cc["@mozilla.org/process/environment;1"]
- .getService(Ci.nsIEnvironment);
- let user = env.get("USER") || env.get("USERNAME") ||
- Svc.Prefs.get("account") || Svc.Prefs.get("username");
-
- let appName;
- let brand = new StringBundle("chrome://branding/locale/brand.properties");
- let brandName = brand.get("brandShortName");
- try {
- let syncStrings = new StringBundle("chrome://browser/locale/sync.properties");
- appName = syncStrings.getFormattedString("sync.defaultAccountApplication", [brandName]);
- } catch (ex) {}
- appName = appName || brandName;
-
- let system =
- // 'device' is defined on unix systems
- Cc["@mozilla.org/system-info;1"].getService(Ci.nsIPropertyBag2).get("device") ||
- // hostname of the system, usually assigned by the user or admin
- Cc["@mozilla.org/system-info;1"].getService(Ci.nsIPropertyBag2).get("host") ||
- // fall back on ua info string
- Cc["@mozilla.org/network/protocol;1?name=http"].getService(Ci.nsIHttpProtocolHandler).oscpu;
-
- return this.localName = Str.sync.get("client.name2", [user, appName, system]);
+ return this.localName = Utils.getDefaultDeviceName();
},
set localName(value) Svc.Prefs.set("client.name", value),
@@ -132,7 +142,9 @@ ClientEngine.prototype = {
},
// Treat reset the same as wiping for locally cached clients
- _resetClient: function _resetClient() this._wipeClient(),
+ _resetClient() {
+ this._wipeClient();
+ },
_wipeClient: function _wipeClient() {
SyncEngine.prototype._resetClient.call(this);
@@ -236,7 +248,7 @@ ClientEngine.prototype = {
this.clearCommands();
// Process each command in order.
- for each ({command: command, args: args} in commands) {
+ for each (let {command, args} in commands) {
this._log.debug("Processing command: " + command + "(" + args + ")");
let engines = [args[0]];
@@ -368,7 +380,9 @@ function ClientStore(name, engine) {
ClientStore.prototype = {
__proto__: Store.prototype,
- create: function create(record) this.update(record),
+ create(record) {
+ this.update(record)
+ },
update: function update(record) {
// Only grab commands from the server; local name/type always wins
@@ -386,14 +400,27 @@ ClientStore.prototype = {
record.name = this.engine.localName;
record.type = this.engine.localType;
record.commands = this.engine.localCommands;
- }
- else
+ record.version = Services.appinfo.version;
+ record.protocols = SUPPORTED_PROTOCOL_VERSIONS;
+
+ // Optional fields.
+ record.os = Services.appinfo.OS; // "Darwin"
+ record.appPackage = Services.appinfo.ID;
+ record.application = this.engine.brandName // "Nightly"
+
+ // We can't compute these yet.
+ // record.device = ""; // Bug 1100723
+ // record.formfactor = ""; // Bug 1100722
+ } else {
record.cleartext = this._remoteClients[id];
+ }
return record;
},
- itemExists: function itemExists(id) id in this.getAllIDs(),
+ itemExists(id) {
+ return id in this.getAllIDs();
+ },
getAllIDs: function getAllIDs() {
let ids = {};
diff --git a/services/sync/modules/engines/forms.js b/services/sync/modules/engines/forms.js
index bd74e55bb..d26d57176 100644
--- a/services/sync/modules/engines/forms.js
+++ b/services/sync/modules/engines/forms.js
@@ -14,7 +14,7 @@ Cu.import("resource://services-sync/record.js");
Cu.import("resource://services-common/async.js");
Cu.import("resource://services-sync/util.js");
Cu.import("resource://services-sync/constants.js");
-Cu.import("resource://services-common/log4moz.js");
+Cu.import("resource://gre/modules/Log.jsm");
const FORMS_TTL = 5184000; // 60 days
@@ -31,7 +31,7 @@ Utils.deferGetSet(FormRec, "cleartext", ["name", "value"]);
let FormWrapper = {
- _log: Log4Moz.repository.getLogger("Sync.Engine.Forms"),
+ _log: Log.repository.getLogger("Sync.Engine.Forms"),
_getEntryCols: ["fieldname", "value"],
_guidCols: ["guid"],
@@ -53,6 +53,9 @@ let FormWrapper = {
},
_updateSpinningly: function(changes) {
+ if (!Svc.FormHistory.enabled) {
+ return; // update isn't going to do anything.
+ }
let cb = Async.makeSpinningCallback();
let callbacks = {
handleCompletion: function(reason) {
@@ -104,6 +107,8 @@ FormEngine.prototype = {
_recordObj: FormRec,
applyIncomingBatchSize: FORMS_STORE_BATCH_SIZE,
+ syncPriority: 6,
+
get prefName() "history",
_findDupe: function _findDupe(item) {
@@ -202,53 +207,32 @@ FormStore.prototype = {
function FormTracker(name, engine) {
Tracker.call(this, name, engine);
- Svc.Obs.add("weave:engine:start-tracking", this);
- Svc.Obs.add("weave:engine:stop-tracking", this);
- Svc.Obs.add("profile-change-teardown", this);
}
FormTracker.prototype = {
__proto__: Tracker.prototype,
QueryInterface: XPCOMUtils.generateQI([
- Ci.nsIFormSubmitObserver,
Ci.nsIObserver,
Ci.nsISupportsWeakReference]),
- _enabled: false,
+ startTracking: function() {
+ Svc.Obs.add("satchel-storage-changed", this);
+ },
+
+ stopTracking: function() {
+ Svc.Obs.remove("satchel-storage-changed", this);
+ },
+
observe: function (subject, topic, data) {
+ Tracker.prototype.observe.call(this, subject, topic, data);
+
switch (topic) {
- case "weave:engine:start-tracking":
- if (!this._enabled) {
- Svc.Obs.add("form-notifier", this);
- Svc.Obs.add("satchel-storage-changed", this);
- // HTMLFormElement doesn't use the normal observer/observe
- // pattern and looks up nsIFormSubmitObservers to .notify()
- // them so add manually to observers
- Cc["@mozilla.org/observer-service;1"]
- .getService(Ci.nsIObserverService)
- .addObserver(this, "earlyformsubmit", true);
- this._enabled = true;
- }
- break;
- case "weave:engine:stop-tracking":
- if (this._enabled) {
- Svc.Obs.remove("form-notifier", this);
- Svc.Obs.remove("satchel-storage-changed", this);
- Cc["@mozilla.org/observer-service;1"]
- .getService(Ci.nsIObserverService)
- .removeObserver(this, "earlyformsubmit");
- this._enabled = false;
- }
- break;
case "satchel-storage-changed":
if (data == "formhistory-add" || data == "formhistory-remove") {
let guid = subject.QueryInterface(Ci.nsISupportsString).toString();
this.trackEntry(guid);
}
break;
- case "profile-change-teardown":
- FormWrapper._finalize();
- break;
}
},
@@ -256,79 +240,4 @@ FormTracker.prototype = {
this.addChangedID(guid);
this.score += SCORE_INCREMENT_MEDIUM;
},
-
- notify: function (formElement, aWindow, actionURI) {
- if (this.ignoreAll) {
- return;
- }
-
- this._log.trace("Form submission notification for " + actionURI.spec);
-
- // XXX Bug 487541 Copy the logic from nsFormHistory::Notify to avoid
- // divergent logic, which can lead to security issues, until there's a
- // better way to get satchel's results like with a notification.
-
- // Determine if a dom node has the autocomplete attribute set to "off"
- let completeOff = function(domNode) {
- let autocomplete = domNode.getAttribute("autocomplete");
- return autocomplete && autocomplete.search(/^off$/i) == 0;
- }
-
- if (completeOff(formElement)) {
- this._log.trace("Form autocomplete set to off");
- return;
- }
-
- /* Get number of elements in form, add points and changedIDs */
- let len = formElement.length;
- let elements = formElement.elements;
- for (let i = 0; i < len; i++) {
- let el = elements.item(i);
-
- // Grab the name for debugging, but check if empty when satchel would
- let name = el.name;
- if (name === "") {
- name = el.id;
- }
-
- if (!(el instanceof Ci.nsIDOMHTMLInputElement)) {
- this._log.trace(name + " is not a DOMHTMLInputElement: " + el);
- continue;
- }
-
- if (el.type.search(/^text$/i) != 0) {
- this._log.trace(name + "'s type is not 'text': " + el.type);
- continue;
- }
-
- if (completeOff(el)) {
- this._log.trace(name + "'s autocomplete set to off");
- continue;
- }
-
- if (el.value === "") {
- this._log.trace(name + "'s value is empty");
- continue;
- }
-
- if (el.value == el.defaultValue) {
- this._log.trace(name + "'s value is the default");
- continue;
- }
-
- if (name === "") {
- this._log.trace("Text input element has no name or id");
- continue;
- }
-
- // Get the GUID on a delay so that it can be added to the DB first...
- Utils.nextTick(function() {
- this._log.trace("Logging form element: " + [name, el.value]);
- let guid = FormWrapper.getGUID(name, el.value);
- if (guid) {
- this.trackEntry(guid);
- }
- }, this);
- }
- }
};
diff --git a/services/sync/modules/engines/history.js b/services/sync/modules/engines/history.js
index 03bbe1348..99ecb4506 100644
--- a/services/sync/modules/engines/history.js
+++ b/services/sync/modules/engines/history.js
@@ -11,9 +11,10 @@ const Cr = Components.results;
const HISTORY_TTL = 5184000; // 60 days
+Cu.import("resource://gre/modules/PlacesUtils.jsm", this);
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://services-common/async.js");
-Cu.import("resource://services-common/log4moz.js");
+Cu.import("resource://gre/modules/Log.jsm");
Cu.import("resource://services-sync/constants.js");
Cu.import("resource://services-sync/engines.js");
Cu.import("resource://services-sync/record.js");
@@ -40,7 +41,9 @@ HistoryEngine.prototype = {
_storeObj: HistoryStore,
_trackerObj: HistoryTracker,
downloadLimit: MAX_HISTORY_DOWNLOAD,
- applyIncomingBatchSize: HISTORY_STORE_BATCH_SIZE
+ applyIncomingBatchSize: HISTORY_STORE_BATCH_SIZE,
+
+ syncPriority: 7,
};
function HistoryStore(name, engine) {
@@ -348,28 +351,18 @@ HistoryStore.prototype = {
function HistoryTracker(name, engine) {
Tracker.call(this, name, engine);
- Svc.Obs.add("weave:engine:start-tracking", this);
- Svc.Obs.add("weave:engine:stop-tracking", this);
}
HistoryTracker.prototype = {
__proto__: Tracker.prototype,
- _enabled: false,
- observe: function observe(subject, topic, data) {
- switch (topic) {
- case "weave:engine:start-tracking":
- if (!this._enabled) {
- PlacesUtils.history.addObserver(this, true);
- this._enabled = true;
- }
- break;
- case "weave:engine:stop-tracking":
- if (this._enabled) {
- PlacesUtils.history.removeObserver(this);
- this._enabled = false;
- }
- break;
- }
+ startTracking: function() {
+ this._log.info("Adding Places observer.");
+ PlacesUtils.history.addObserver(this, true);
+ },
+
+ stopTracking: function() {
+ this._log.info("Removing Places observer.");
+ PlacesUtils.history.removeObserver(this);
},
QueryInterface: XPCOMUtils.generateQI([
diff --git a/services/sync/modules/engines/passwords.js b/services/sync/modules/engines/passwords.js
index 5952b01f3..994b59767 100644
--- a/services/sync/modules/engines/passwords.js
+++ b/services/sync/modules/engines/passwords.js
@@ -4,10 +4,7 @@
this.EXPORTED_SYMBOLS = ['PasswordEngine', 'LoginRec'];
-const Cu = Components.utils;
-const Cc = Components.classes;
-const Ci = Components.interfaces;
-const Cr = Components.results;
+const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
Cu.import("resource://services-sync/record.js");
Cu.import("resource://services-sync/constants.js");
@@ -22,8 +19,10 @@ LoginRec.prototype = {
_logName: "Sync.Record.Login",
};
-Utils.deferGetSet(LoginRec, "cleartext", ["hostname", "formSubmitURL",
- "httpRealm", "username", "password", "usernameField", "passwordField"]);
+Utils.deferGetSet(LoginRec, "cleartext", [
+ "hostname", "formSubmitURL",
+ "httpRealm", "username", "password", "usernameField", "passwordField",
+ ]);
this.PasswordEngine = function PasswordEngine(service) {
@@ -34,72 +33,84 @@ PasswordEngine.prototype = {
_storeObj: PasswordStore,
_trackerObj: PasswordTracker,
_recordObj: LoginRec,
+
applyIncomingBatchSize: PASSWORDS_STORE_BATCH_SIZE,
- _syncFinish: function _syncFinish() {
+ syncPriority: 2,
+
+ _syncFinish: function () {
SyncEngine.prototype._syncFinish.call(this);
- // Delete the weave credentials from the server once
- if (!Svc.Prefs.get("deletePwd", false)) {
+ // Delete the Weave credentials from the server once.
+ if (!Svc.Prefs.get("deletePwdFxA", false)) {
try {
- let ids = Services.logins.findLogins({}, PWDMGR_HOST, "", "")
- .map(function(info) {
- return info.QueryInterface(Components.interfaces.nsILoginMetaInfo).guid;
- });
- let coll = new Collection(this.engineURL, null, this.service);
- coll.ids = ids;
- let ret = coll.delete();
- this._log.debug("Delete result: " + ret);
-
- Svc.Prefs.set("deletePwd", true);
- }
- catch(ex) {
+ let ids = [];
+ for (let host of Utils.getSyncCredentialsHosts()) {
+ for (let info of Services.logins.findLogins({}, host, "", "")) {
+ ids.push(info.QueryInterface(Components.interfaces.nsILoginMetaInfo).guid);
+ }
+ }
+ if (ids.length) {
+ let coll = new Collection(this.engineURL, null, this.service);
+ coll.ids = ids;
+ let ret = coll.delete();
+ this._log.debug("Delete result: " + ret);
+ if (!ret.success && ret.status != 400) {
+ // A non-400 failure means try again next time.
+ return;
+ }
+ } else {
+ this._log.debug("Didn't find any passwords to delete");
+ }
+ // If there were no ids to delete, or we succeeded, or got a 400,
+ // record success.
+ Svc.Prefs.set("deletePwdFxA", true);
+ Svc.Prefs.reset("deletePwd"); // The old prefname we previously used.
+ } catch (ex) {
this._log.debug("Password deletes failed: " + Utils.exceptionStr(ex));
}
}
},
- _findDupe: function _findDupe(item) {
+ _findDupe: function (item) {
let login = this._store._nsLoginInfoFromRecord(item);
- if (!login)
+ if (!login) {
return;
+ }
+
+ let logins = Services.logins.findLogins({}, login.hostname, login.formSubmitURL, login.httpRealm);
- let logins = Services.logins.findLogins(
- {}, login.hostname, login.formSubmitURL, login.httpRealm);
this._store._sleep(0); // Yield back to main thread after synchronous operation.
- // Look for existing logins that match the hostname but ignore the password
- for each (let local in logins)
- if (login.matches(local, true) && local instanceof Ci.nsILoginMetaInfo)
+ // Look for existing logins that match the hostname, but ignore the password.
+ for each (let local in logins) {
+ if (login.matches(local, true) && local instanceof Ci.nsILoginMetaInfo) {
return local.guid;
- }
+ }
+ }
+ },
};
function PasswordStore(name, engine) {
Store.call(this, name, engine);
- this._nsLoginInfo = new Components.Constructor(
- "@mozilla.org/login-manager/loginInfo;1", Ci.nsILoginInfo, "init");
-
- XPCOMUtils.defineLazyGetter(this, "DBConnection", function() {
- return Services.logins.QueryInterface(Ci.nsIInterfaceRequestor)
- .getInterface(Ci.mozIStorageConnection);
- });
+ this._nsLoginInfo = new Components.Constructor("@mozilla.org/login-manager/loginInfo;1", Ci.nsILoginInfo, "init");
}
PasswordStore.prototype = {
__proto__: Store.prototype,
- _nsLoginInfoFromRecord: function PasswordStore__nsLoginInfoRec(record) {
- if (record.formSubmitURL &&
- record.httpRealm) {
- this._log.warn("Record " + record.id +
- " has both formSubmitURL and httpRealm. Skipping.");
+ _nsLoginInfoFromRecord: function (record) {
+ function nullUndefined(x) {
+ return (x == undefined) ? null : x;
+ }
+
+ if (record.formSubmitURL && record.httpRealm) {
+ this._log.warn("Record " + record.id + " has both formSubmitURL and httpRealm. Skipping.");
return null;
}
-
+
// Passing in "undefined" results in an empty string, which later
// counts as a value. Explicitly `|| null` these fields according to JS
// truthiness. Records with empty strings or null will be unmolested.
- function nullUndefined(x) (x == undefined) ? null : x;
let info = new this._nsLoginInfo(record.hostname,
nullUndefined(record.formSubmitURL),
nullUndefined(record.httpRealm),
@@ -112,46 +123,32 @@ PasswordStore.prototype = {
return info;
},
- _getLoginFromGUID: function PasswordStore__getLoginFromGUID(id) {
- let prop = Cc["@mozilla.org/hash-property-bag;1"].
- createInstance(Ci.nsIWritablePropertyBag2);
+ _getLoginFromGUID: function (id) {
+ let prop = Cc["@mozilla.org/hash-property-bag;1"].createInstance(Ci.nsIWritablePropertyBag2);
prop.setPropertyAsAUTF8String("guid", id);
let logins = Services.logins.searchLogins({}, prop);
this._sleep(0); // Yield back to main thread after synchronous operation.
+
if (logins.length > 0) {
this._log.trace(logins.length + " items matching " + id + " found.");
return logins[0];
- } else {
- this._log.trace("No items matching " + id + " found. Ignoring");
}
- return null;
- },
- applyIncomingBatch: function applyIncomingBatch(records) {
- if (!this.DBConnection) {
- return Store.prototype.applyIncomingBatch.call(this, records);
- }
-
- return Utils.runInTransaction(this.DBConnection, function() {
- return Store.prototype.applyIncomingBatch.call(this, records);
- }, this);
- },
-
- applyIncoming: function applyIncoming(record) {
- Store.prototype.applyIncoming.call(this, record);
- this._sleep(0); // Yield back to main thread after synchronous operation.
+ this._log.trace("No items matching " + id + " found. Ignoring");
+ return null;
},
- getAllIDs: function PasswordStore__getAllIDs() {
+ getAllIDs: function () {
let items = {};
let logins = Services.logins.getAllLogins({});
for (let i = 0; i < logins.length; i++) {
- // Skip over Weave password/passphrase entries
+ // Skip over Weave password/passphrase entries.
let metaInfo = logins[i].QueryInterface(Ci.nsILoginMetaInfo);
- if (metaInfo.hostname == PWDMGR_HOST)
+ if (Utils.getSyncCredentialsHosts().has(metaInfo.hostname)) {
continue;
+ }
items[metaInfo.guid] = metaInfo;
}
@@ -159,7 +156,7 @@ PasswordStore.prototype = {
return items;
},
- changeItemID: function PasswordStore__changeItemID(oldID, newID) {
+ changeItemID: function (oldID, newID) {
this._log.trace("Changing item ID: " + oldID + " to " + newID);
let oldLogin = this._getLoginFromGUID(oldID);
@@ -172,41 +169,43 @@ PasswordStore.prototype = {
return;
}
- let prop = Cc["@mozilla.org/hash-property-bag;1"].
- createInstance(Ci.nsIWritablePropertyBag2);
+ let prop = Cc["@mozilla.org/hash-property-bag;1"]
+ .createInstance(Ci.nsIWritablePropertyBag2);
prop.setPropertyAsAUTF8String("guid", newID);
Services.logins.modifyLogin(oldLogin, prop);
},
- itemExists: function PasswordStore__itemExists(id) {
- if (this._getLoginFromGUID(id))
- return true;
- return false;
+ itemExists: function (id) {
+ return !!this._getLoginFromGUID(id);
},
- createRecord: function createRecord(id, collection) {
+ createRecord: function (id, collection) {
let record = new LoginRec(collection, id);
let login = this._getLoginFromGUID(id);
- if (login) {
- record.hostname = login.hostname;
- record.formSubmitURL = login.formSubmitURL;
- record.httpRealm = login.httpRealm;
- record.username = login.username;
- record.password = login.password;
- record.usernameField = login.usernameField;
- record.passwordField = login.passwordField;
- }
- else
+ if (!login) {
record.deleted = true;
+ return record;
+ }
+
+ record.hostname = login.hostname;
+ record.formSubmitURL = login.formSubmitURL;
+ record.httpRealm = login.httpRealm;
+ record.username = login.username;
+ record.password = login.password;
+ record.usernameField = login.usernameField;
+ record.passwordField = login.passwordField;
+
return record;
},
- create: function PasswordStore__create(record) {
+ create: function (record) {
let login = this._nsLoginInfoFromRecord(record);
- if (!login)
+ if (!login) {
return;
+ }
+
this._log.debug("Adding login for " + record.hostname);
this._log.trace("httpRealm: " + JSON.stringify(login.httpRealm) + "; " +
"formSubmitURL: " + JSON.stringify(login.formSubmitURL));
@@ -218,7 +217,7 @@ PasswordStore.prototype = {
}
},
- remove: function PasswordStore__remove(record) {
+ remove: function (record) {
this._log.trace("Removing login " + record.id);
let loginItem = this._getLoginFromGUID(record.id);
@@ -230,7 +229,7 @@ PasswordStore.prototype = {
Services.logins.removeLogin(loginItem);
},
- update: function PasswordStore__update(record) {
+ update: function (record) {
let loginItem = this._getLoginFromGUID(record.id);
if (!loginItem) {
this._log.debug("Skipping update for unknown item: " + record.hostname);
@@ -239,8 +238,10 @@ PasswordStore.prototype = {
this._log.debug("Updating " + record.hostname);
let newinfo = this._nsLoginInfoFromRecord(record);
- if (!newinfo)
+ if (!newinfo) {
return;
+ }
+
try {
Services.logins.modifyLogin(loginItem, newinfo);
} catch(ex) {
@@ -250,9 +251,9 @@ PasswordStore.prototype = {
}
},
- wipe: function PasswordStore_wipe() {
+ wipe: function () {
Services.logins.removeAllLogins();
- }
+ },
};
function PasswordTracker(name, engine) {
@@ -263,49 +264,43 @@ function PasswordTracker(name, engine) {
PasswordTracker.prototype = {
__proto__: Tracker.prototype,
- _enabled: false,
- observe: function PasswordTracker_observe(aSubject, aTopic, aData) {
- switch (aTopic) {
- case "weave:engine:start-tracking":
- if (!this._enabled) {
- Svc.Obs.add("passwordmgr-storage-changed", this);
- this._enabled = true;
- }
- return;
- case "weave:engine:stop-tracking":
- if (this._enabled) {
- Svc.Obs.remove("passwordmgr-storage-changed", this);
- this._enabled = false;
- }
- return;
- }
+ startTracking: function () {
+ Svc.Obs.add("passwordmgr-storage-changed", this);
+ },
+
+ stopTracking: function () {
+ Svc.Obs.remove("passwordmgr-storage-changed", this);
+ },
+
+ observe: function (subject, topic, data) {
+ Tracker.prototype.observe.call(this, subject, topic, data);
- if (this.ignoreAll)
+ if (this.ignoreAll) {
return;
+ }
// A single add, remove or change or removing all items
// will trigger a sync for MULTI_DEVICE.
- switch (aData) {
- case 'modifyLogin':
- aSubject = aSubject.QueryInterface(Ci.nsIArray).
- queryElementAt(1, Ci.nsILoginMetaInfo);
- // fallthrough
- case 'addLogin':
- case 'removeLogin':
- // Skip over Weave password/passphrase changes
- aSubject.QueryInterface(Ci.nsILoginMetaInfo).
- QueryInterface(Ci.nsILoginInfo);
- if (aSubject.hostname == PWDMGR_HOST)
- break;
+ switch (data) {
+ case "modifyLogin":
+ subject = subject.QueryInterface(Ci.nsIArray).queryElementAt(1, Ci.nsILoginMetaInfo);
+ // Fall through.
+ case "addLogin":
+ case "removeLogin":
+ // Skip over Weave password/passphrase changes.
+ subject.QueryInterface(Ci.nsILoginMetaInfo).QueryInterface(Ci.nsILoginInfo);
+ if (Utils.getSyncCredentialsHosts().has(subject.hostname)) {
+ break;
+ }
- this.score += SCORE_INCREMENT_XLARGE;
- this._log.trace(aData + ": " + aSubject.guid);
- this.addChangedID(aSubject.guid);
- break;
- case 'removeAllLogins':
- this._log.trace(aData);
- this.score += SCORE_INCREMENT_XLARGE;
- break;
+ this.score += SCORE_INCREMENT_XLARGE;
+ this._log.trace(data + ": " + subject.guid);
+ this.addChangedID(subject.guid);
+ break;
+ case "removeAllLogins":
+ this._log.trace(data);
+ this.score += SCORE_INCREMENT_XLARGE;
+ break;
}
- }
+ },
};
diff --git a/services/sync/modules/engines/prefs.js b/services/sync/modules/engines/prefs.js
index 2418cd0dc..49d73dbef 100644
--- a/services/sync/modules/engines/prefs.js
+++ b/services/sync/modules/engines/prefs.js
@@ -41,6 +41,8 @@ PrefsEngine.prototype = {
_recordObj: PrefRec,
version: 2,
+ syncPriority: 1,
+
getChangedIDs: function getChangedIDs() {
// No need for a proper timestamp (no conflict resolution needed).
let changedIDs = {};
@@ -67,7 +69,7 @@ PrefsEngine.prototype = {
function PrefStore(name, engine) {
Store.call(this, name, engine);
- Svc.Obs.add("profile-before-change", function() {
+ Svc.Obs.add("profile-before-change", function () {
this.__prefs = null;
}, this);
}
@@ -214,38 +216,36 @@ PrefTracker.prototype = {
__prefs: null,
get _prefs() {
- if (!this.__prefs)
+ if (!this.__prefs) {
this.__prefs = new Preferences();
+ }
return this.__prefs;
},
- _enabled: false,
- observe: function(aSubject, aTopic, aData) {
- switch (aTopic) {
- case "weave:engine:start-tracking":
- if (!this._enabled) {
- Cc["@mozilla.org/preferences-service;1"]
- .getService(Ci.nsIPrefBranch).addObserver("", this, false);
- this._enabled = true;
- }
- break;
- case "weave:engine:stop-tracking":
- if (this._enabled)
- this._enabled = false;
- // Fall through to clean up.
+ startTracking: function () {
+ Services.prefs.addObserver("", this, false);
+ },
+
+ stopTracking: function () {
+ this.__prefs = null;
+ Services.prefs.removeObserver("", this);
+ },
+
+ observe: function (subject, topic, data) {
+ Tracker.prototype.observe.call(this, subject, topic, data);
+
+ switch (topic) {
case "profile-before-change":
- this.__prefs = null;
- Cc["@mozilla.org/preferences-service;1"]
- .getService(Ci.nsIPrefBranch).removeObserver("", this);
+ this.stopTracking();
break;
case "nsPref:changed":
// Trigger a sync for MULTI-DEVICE for a change that determines
// which prefs are synced or a regular pref change.
- if (aData.indexOf(WEAVE_SYNC_PREFS) == 0 ||
- this._prefs.get(WEAVE_SYNC_PREFS + aData, false)) {
+ if (data.indexOf(WEAVE_SYNC_PREFS) == 0 ||
+ this._prefs.get(WEAVE_SYNC_PREFS + data, false)) {
this.score += SCORE_INCREMENT_XLARGE;
this.modified = true;
- this._log.trace("Preference " + aData + " changed");
+ this._log.trace("Preference " + data + " changed");
}
break;
}
diff --git a/services/sync/modules/engines/tabs.js b/services/sync/modules/engines/tabs.js
index 24dbf0b2f..1fce737d2 100644
--- a/services/sync/modules/engines/tabs.js
+++ b/services/sync/modules/engines/tabs.js
@@ -2,13 +2,12 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-this.EXPORTED_SYMBOLS = ['TabEngine', 'TabSetRecord'];
+this.EXPORTED_SYMBOLS = ["TabEngine", "TabSetRecord"];
-const Cc = Components.classes;
-const Ci = Components.interfaces;
-const Cu = Components.utils;
+const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
-const TABS_TTL = 604800; // 7 days
+const TABS_TTL = 604800; // 7 days.
+const TAB_ENTRIES_LIMIT = 25; // How many URLs to include in tab history.
Cu.import("resource://gre/modules/Preferences.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
@@ -27,7 +26,7 @@ this.TabSetRecord = function TabSetRecord(collection, id) {
TabSetRecord.prototype = {
__proto__: CryptoWrapper.prototype,
_logName: "Sync.Record.Tabs",
- ttl: TABS_TTL
+ ttl: TABS_TTL,
};
Utils.deferGetSet(TabSetRecord, "cleartext", ["clientName", "tabs"]);
@@ -36,7 +35,7 @@ Utils.deferGetSet(TabSetRecord, "cleartext", ["clientName", "tabs"]);
this.TabEngine = function TabEngine(service) {
SyncEngine.call(this, "Tabs", service);
- // Reset the client on every startup so that we fetch recent tabs
+ // Reset the client on every startup so that we fetch recent tabs.
this._resetClient();
}
TabEngine.prototype = {
@@ -45,7 +44,9 @@ TabEngine.prototype = {
_trackerObj: TabTracker,
_recordObj: TabSetRecord,
- getChangedIDs: function getChangedIDs() {
+ syncPriority: 3,
+
+ getChangedIDs: function () {
// No need for a proper timestamp (no conflict resolution needed).
let changedIDs = {};
if (this._tracker.modified)
@@ -53,39 +54,46 @@ TabEngine.prototype = {
return changedIDs;
},
- // API for use by Weave UI code to give user choices of tabs to open:
- getAllClients: function TabEngine_getAllClients() {
+ // API for use by Sync UI code to give user choices of tabs to open.
+ getAllClients: function () {
return this._store._remoteClients;
},
- getClientById: function TabEngine_getClientById(id) {
+ getClientById: function (id) {
return this._store._remoteClients[id];
},
- _resetClient: function TabEngine__resetClient() {
+ _resetClient: function () {
SyncEngine.prototype._resetClient.call(this);
this._store.wipe();
this._tracker.modified = true;
},
- removeClientData: function removeClientData() {
+ removeClientData: function () {
let url = this.engineURL + "/" + this.service.clientsEngine.localID;
this.service.resource(url).delete();
},
- /* The intent is not to show tabs in the menu if they're already
- * open locally. There are a couple ways to interpret this: for
- * instance, we could do it by removing a tab from the list when
- * you open it -- but then if you close it, you can't get back to
- * it. So the way I'm doing it here is to not show a tab in the menu
- * if you have a tab open to the same URL, even though this means
- * that as soon as you navigate anywhere, the original tab will
- * reappear in the menu.
+ /**
+ * Return a Set of open URLs.
*/
- locallyOpenTabMatchesURL: function TabEngine_localTabMatches(url) {
- return this._store.getAllTabs().some(function(tab) {
- return tab.urlHistory[0] == url;
- });
+ getOpenURLs: function () {
+ let urls = new Set();
+ for (let entry of this._store.getAllTabs()) {
+ urls.add(entry.urlHistory[0]);
+ }
+ return urls;
+ },
+
+ _reconcile: function (item) {
+ // Skip our own record.
+ // TabStore.itemExists tests only against our local client ID.
+ if (this._store.itemExists(item.id)) {
+ this._log.trace("Ignoring incoming tab item because of its id: " + item.id);
+ return false;
+ }
+
+ return SyncEngine.prototype._reconcile.call(this, item);
}
};
@@ -96,69 +104,90 @@ function TabStore(name, engine) {
TabStore.prototype = {
__proto__: Store.prototype,
- itemExists: function TabStore_itemExists(id) {
+ itemExists: function (id) {
return id == this.engine.service.clientsEngine.localID;
},
- /**
- * Return the recorded last used time of the provided tab, or
- * 0 if none is present.
- * The result will always be an integer value.
- */
- tabLastUsed: function tabLastUsed(tab) {
- // weaveLastUsed will only be set if the tab was ever selected (or
- // opened after Sync was running).
- let weaveLastUsed = tab.extData && tab.extData.weaveLastUsed;
- if (!weaveLastUsed) {
- return 0;
- }
- return parseInt(weaveLastUsed, 10) || 0;
+ getWindowEnumerator: function () {
+ return Services.wm.getEnumerator("navigator:browser");
+ },
+
+ shouldSkipWindow: function (win) {
+ return win.closed ||
+ PrivateBrowsingUtils.isWindowPrivate(win);
+ },
+
+ getTabState: function (tab) {
+ return JSON.parse(Svc.Session.getTabState(tab));
},
- getAllTabs: function getAllTabs(filter) {
+ getAllTabs: function (filter) {
let filteredUrls = new RegExp(Svc.Prefs.get("engine.tabs.filteredUrls"), "i");
let allTabs = [];
- let currentState = JSON.parse(Svc.Session.getBrowserState());
- let tabLastUsed = this.tabLastUsed;
- currentState.windows.forEach(function(window) {
- if (window.isPrivate) {
- return;
+ let winEnum = this.getWindowEnumerator();
+ while (winEnum.hasMoreElements()) {
+ let win = winEnum.getNext();
+ if (this.shouldSkipWindow(win)) {
+ continue;
}
- window.tabs.forEach(function(tab) {
+
+ for (let tab of win.gBrowser.tabs) {
+ tabState = this.getTabState(tab);
+
// Make sure there are history entries to look at.
- if (!tab.entries.length)
- return;
- // Until we store full or partial history, just grab the current entry.
- // index is 1 based, so make sure we adjust.
- let entry = tab.entries[tab.index - 1];
-
- // Filter out some urls if necessary. SessionStore can return empty
- // tabs in some cases - easiest thing is to just ignore them for now.
- if (!entry.url || filter && filteredUrls.test(entry.url))
- return;
-
- // I think it's also possible that attributes[.image] might not be set
- // so handle that as well.
+ if (!tabState || !tabState.entries.length) {
+ continue;
+ }
+
+ let acceptable = !filter ? (url) => url :
+ (url) => url && !filteredUrls.test(url);
+
+ let entries = tabState.entries;
+ let index = tabState.index;
+ let current = entries[index - 1];
+
+ // We ignore the tab completely if the current entry url is
+ // not acceptable (we need something accurate to open).
+ if (!acceptable(current.url)) {
+ continue;
+ }
+
+ // The element at `index` is the current page. Previous URLs were
+ // previously visited URLs; subsequent URLs are in the 'forward' stack,
+ // which we can't represent in Sync, so we truncate here.
+ let candidates = (entries.length == index) ?
+ entries :
+ entries.slice(0, index);
+
+ let urls = candidates.map((entry) => entry.url)
+ .filter(acceptable)
+ .reverse(); // Because Sync puts current at index 0, and history after.
+
+ // Truncate if necessary.
+ if (urls.length > TAB_ENTRIES_LIMIT) {
+ urls.length = TAB_ENTRIES_LIMIT;
+ }
+
allTabs.push({
- title: entry.title || "",
- urlHistory: [entry.url],
- icon: tab.attributes && tab.attributes.image || "",
- lastUsed: tabLastUsed(tab)
+ title: current.title || "",
+ urlHistory: urls,
+ icon: tabState.attributes && tabState.attributes.image || "",
+ lastUsed: Math.floor((tabState.lastAccessed || 0) / 1000),
});
- });
- });
+ }
+ }
return allTabs;
},
- createRecord: function createRecord(id, collection) {
+ createRecord: function (id, collection) {
let record = new TabSetRecord(collection, id);
record.clientName = this.engine.service.clientsEngine.localName;
// Sort tabs in descending-used order to grab the most recently used
- let tabs = this.getAllTabs(true).sort(function(a, b) {
+ let tabs = this.getAllTabs(true).sort(function (a, b) {
return b.lastUsed - a.lastUsed;
});
@@ -178,7 +207,7 @@ TabStore.prototype = {
}
this._log.trace("Created tabs " + tabs.length + " of " + origLength);
- tabs.forEach(function(tab) {
+ tabs.forEach(function (tab) {
this._log.trace("Wrapping tab: " + JSON.stringify(tab));
}, this);
@@ -186,7 +215,7 @@ TabStore.prototype = {
return record;
},
- getAllIDs: function TabStore_getAllIds() {
+ getAllIDs: function () {
// Don't report any tabs if all windows are in private browsing for
// first syncs.
let ids = {};
@@ -212,31 +241,38 @@ TabStore.prototype = {
return ids;
},
- wipe: function TabStore_wipe() {
+ wipe: function () {
this._remoteClients = {};
},
- create: function TabStore_create(record) {
+ create: function (record) {
this._log.debug("Adding remote tabs from " + record.clientName);
this._remoteClients[record.id] = record.cleartext;
- // Lose some precision, but that's good enough (seconds)
+ // Lose some precision, but that's good enough (seconds).
let roundModify = Math.floor(record.modified / 1000);
let notifyState = Svc.Prefs.get("notifyTabState");
- // If there's no existing pref, save this first modified time
- if (notifyState == null)
+
+ // If there's no existing pref, save this first modified time.
+ if (notifyState == null) {
Svc.Prefs.set("notifyTabState", roundModify);
- // Don't change notifyState if it's already 0 (don't notify)
- else if (notifyState == 0)
return;
- // We must have gotten a new tab that isn't the same as last time
- else if (notifyState != roundModify)
+ }
+
+ // Don't change notifyState if it's already 0 (don't notify).
+ if (notifyState == 0) {
+ return;
+ }
+
+ // We must have gotten a new tab that isn't the same as last time.
+ if (notifyState != roundModify) {
Svc.Prefs.set("notifyTabState", 0);
+ }
},
- update: function update(record) {
+ update: function (record) {
this._log.trace("Ignoring tab updates as local ones win");
- }
+ },
};
@@ -245,7 +281,7 @@ function TabTracker(name, engine) {
Svc.Obs.add("weave:engine:start-tracking", this);
Svc.Obs.add("weave:engine:stop-tracking", this);
- // Make sure "this" pointer is always set correctly for event listeners
+ // Make sure "this" pointer is always set correctly for event listeners.
this.onTab = Utils.bind2(this, this.onTab);
this._unregisterListeners = Utils.bind2(this, this._unregisterListeners);
}
@@ -254,16 +290,17 @@ TabTracker.prototype = {
QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver]),
- loadChangedIDs: function loadChangedIDs() {
+ loadChangedIDs: function () {
// Don't read changed IDs from disk at start up.
},
- clearChangedIDs: function clearChangedIDs() {
+ clearChangedIDs: function () {
this.modified = false;
},
_topics: ["pageshow", "TabOpen", "TabClose", "TabSelect"],
- _registerListenersForWindow: function registerListenersFW(window) {
+
+ _registerListenersForWindow: function (window) {
this._log.trace("Registering tab listeners in window");
for each (let topic in this._topics) {
window.addEventListener(topic, this.onTab, false);
@@ -271,11 +308,11 @@ TabTracker.prototype = {
window.addEventListener("unload", this._unregisterListeners, false);
},
- _unregisterListeners: function unregisterListeners(event) {
+ _unregisterListeners: function (event) {
this._unregisterListenersForWindow(event.target);
},
- _unregisterListenersForWindow: function unregisterListenersFW(window) {
+ _unregisterListenersForWindow: function (window) {
this._log.trace("Removing tab listeners in window");
window.removeEventListener("unload", this._unregisterListeners, false);
for each (let topic in this._topics) {
@@ -283,43 +320,43 @@ TabTracker.prototype = {
}
},
- _enabled: false,
- observe: function TabTracker_observe(aSubject, aTopic, aData) {
- switch (aTopic) {
- case "weave:engine:start-tracking":
- if (!this._enabled) {
- Svc.Obs.add("domwindowopened", this);
- let wins = Services.wm.getEnumerator("navigator:browser");
- while (wins.hasMoreElements())
- this._registerListenersForWindow(wins.getNext());
- this._enabled = true;
- }
- break;
- case "weave:engine:stop-tracking":
- if (this._enabled) {
- Svc.Obs.remove("domwindowopened", this);
- let wins = Services.wm.getEnumerator("navigator:browser");
- while (wins.hasMoreElements())
- this._unregisterListenersForWindow(wins.getNext());
- this._enabled = false;
- }
- return;
+ startTracking: function () {
+ Svc.Obs.add("domwindowopened", this);
+ let wins = Services.wm.getEnumerator("navigator:browser");
+ while (wins.hasMoreElements()) {
+ this._registerListenersForWindow(wins.getNext());
+ }
+ },
+
+ stopTracking: function () {
+ Svc.Obs.remove("domwindowopened", this);
+ let wins = Services.wm.getEnumerator("navigator:browser");
+ while (wins.hasMoreElements()) {
+ this._unregisterListenersForWindow(wins.getNext());
+ }
+ },
+
+ observe: function (subject, topic, data) {
+ Tracker.prototype.observe.call(this, subject, topic, data);
+
+ switch (topic) {
case "domwindowopened":
- // Add tab listeners now that a window has opened
- let self = this;
- aSubject.addEventListener("load", function onLoad(event) {
- aSubject.removeEventListener("load", onLoad, false);
- // Only register after the window is done loading to avoid unloads
- self._registerListenersForWindow(aSubject);
- }, false);
+ let onLoad = () => {
+ subject.removeEventListener("load", onLoad, false);
+ // Only register after the window is done loading to avoid unloads.
+ this._registerListenersForWindow(subject);
+ };
+
+ // Add tab listeners now that a window has opened.
+ subject.addEventListener("load", onLoad, false);
break;
}
},
- onTab: function onTab(event) {
+ onTab: function (event) {
if (event.originalTarget.linkedBrowser) {
- let win = event.originalTarget.linkedBrowser.contentWindow;
- if (PrivateBrowsingUtils.isWindowPrivate(win) &&
+ let browser = event.originalTarget.linkedBrowser;
+ if (PrivateBrowsingUtils.isBrowserPrivate(browser) &&
!PrivateBrowsingUtils.permanentPrivateBrowsing) {
this._log.trace("Ignoring tab event from private browsing.");
return;
@@ -329,20 +366,11 @@ TabTracker.prototype = {
this._log.trace("onTab event: " + event.type);
this.modified = true;
- // For pageshow events, only give a partial score bump (~.1)
- let chance = .1;
-
- // For regular Tab events, do a full score bump and remember when it changed
- if (event.type != "pageshow") {
- chance = 1;
-
- // Store a timestamp in the tab to track when it was last used
- Svc.Session.setTabValue(event.originalTarget, "weaveLastUsed",
- Math.floor(Date.now() / 1000));
- }
-
- // Only increase the score by whole numbers, so use random for partial score
- if (Math.random() < chance)
+ // For page shows, bump the score 10% of the time, emulating a partial
+ // score. We don't want to sync too frequently. For all other page
+ // events, always bump the score.
+ if (event.type != "pageshow" || Math.random() < .1) {
this.score += SCORE_INCREMENT_SMALL;
+ }
},
-}
+};
diff --git a/services/sync/modules/healthreport.jsm b/services/sync/modules/healthreport.jsm
new file mode 100644
index 000000000..47161c095
--- /dev/null
+++ b/services/sync/modules/healthreport.jsm
@@ -0,0 +1,262 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+this.EXPORTED_SYMBOLS = [
+ "SyncProvider",
+];
+
+const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
+
+Cu.import("resource://gre/modules/Metrics.jsm", this);
+Cu.import("resource://gre/modules/Promise.jsm", this);
+Cu.import("resource://gre/modules/Services.jsm", this);
+Cu.import("resource://gre/modules/XPCOMUtils.jsm", this);
+
+const DAILY_LAST_NUMERIC_FIELD = {type: Metrics.Storage.FIELD_DAILY_LAST_NUMERIC};
+const DAILY_LAST_TEXT_FIELD = {type: Metrics.Storage.FIELD_DAILY_LAST_TEXT};
+const DAILY_COUNTER_FIELD = {type: Metrics.Storage.FIELD_DAILY_COUNTER};
+
+XPCOMUtils.defineLazyModuleGetter(this, "Weave",
+ "resource://services-sync/main.js");
+
+function SyncMeasurement1() {
+ Metrics.Measurement.call(this);
+}
+
+SyncMeasurement1.prototype = Object.freeze({
+ __proto__: Metrics.Measurement.prototype,
+
+ name: "sync",
+ version: 1,
+
+ fields: {
+ enabled: DAILY_LAST_NUMERIC_FIELD,
+ preferredProtocol: DAILY_LAST_TEXT_FIELD,
+ activeProtocol: DAILY_LAST_TEXT_FIELD,
+ syncStart: DAILY_COUNTER_FIELD,
+ syncSuccess: DAILY_COUNTER_FIELD,
+ syncError: DAILY_COUNTER_FIELD,
+ },
+});
+
+function SyncDevicesMeasurement1() {
+ Metrics.Measurement.call(this);
+}
+
+SyncDevicesMeasurement1.prototype = Object.freeze({
+ __proto__: Metrics.Measurement.prototype,
+
+ name: "devices",
+ version: 1,
+
+ fields: {},
+
+ shouldIncludeField: function (name) {
+ return true;
+ },
+
+ fieldType: function (name) {
+ return Metrics.Storage.FIELD_DAILY_COUNTER;
+ },
+});
+
+function SyncMigrationMeasurement1() {
+ Metrics.Measurement.call(this);
+}
+
+SyncMigrationMeasurement1.prototype = Object.freeze({
+ __proto__: Metrics.Measurement.prototype,
+
+ name: "migration",
+ version: 1,
+
+ fields: {
+ state: DAILY_LAST_TEXT_FIELD, // last "user" or "internal" state we saw for the day
+ accepted: DAILY_COUNTER_FIELD, // number of times user tried to start migration
+ declined: DAILY_COUNTER_FIELD, // number of times user closed nagging infobar
+ unlinked: DAILY_LAST_NUMERIC_FIELD, // did the user decline and unlink
+ },
+});
+
+this.SyncProvider = function () {
+ Metrics.Provider.call(this);
+};
+SyncProvider.prototype = Object.freeze({
+ __proto__: Metrics.Provider.prototype,
+
+ name: "org.mozilla.sync",
+
+ measurementTypes: [
+ SyncDevicesMeasurement1,
+ SyncMeasurement1,
+ SyncMigrationMeasurement1,
+ ],
+
+ _OBSERVERS: [
+ "weave:service:sync:start",
+ "weave:service:sync:finish",
+ "weave:service:sync:error",
+ "fxa-migration:state-changed",
+ "fxa-migration:internal-state-changed",
+ "fxa-migration:internal-telemetry",
+ ],
+
+ postInit: function () {
+ for (let o of this._OBSERVERS) {
+ Services.obs.addObserver(this, o, false);
+ }
+
+ return Promise.resolve();
+ },
+
+ onShutdown: function () {
+ for (let o of this._OBSERVERS) {
+ Services.obs.removeObserver(this, o);
+ }
+
+ return Promise.resolve();
+ },
+
+ observe: function (subject, topic, data) {
+ switch (topic) {
+ case "weave:service:sync:start":
+ case "weave:service:sync:finish":
+ case "weave:service:sync:error":
+ return this._observeSync(subject, topic, data);
+
+ case "fxa-migration:state-changed":
+ case "fxa-migration:internal-state-changed":
+ case "fxa-migration:internal-telemetry":
+ return this._observeMigration(subject, topic, data);
+ }
+ Cu.reportError("unexpected topic in sync healthreport provider: " + topic);
+ },
+
+ _observeSync: function (subject, topic, data) {
+ let field;
+ switch (topic) {
+ case "weave:service:sync:start":
+ field = "syncStart";
+ break;
+
+ case "weave:service:sync:finish":
+ field = "syncSuccess";
+ break;
+
+ case "weave:service:sync:error":
+ field = "syncError";
+ break;
+
+ default:
+ Cu.reportError("unexpected sync topic in sync healthreport provider: " + topic);
+ return;
+ }
+
+ let m = this.getMeasurement(SyncMeasurement1.prototype.name,
+ SyncMeasurement1.prototype.version);
+ return this.enqueueStorageOperation(function recordSyncEvent() {
+ return m.incrementDailyCounter(field);
+ });
+ },
+
+ _observeMigration: function(subject, topic, data) {
+ switch (topic) {
+ case "fxa-migration:state-changed":
+ case "fxa-migration:internal-state-changed": {
+ // We record both "user" and "internal" states in the same field. This
+ // works for us as user state is always null when there is an internal
+ // state.
+ if (!data) {
+ return; // we don't count the |null| state
+ }
+ let m = this.getMeasurement(SyncMigrationMeasurement1.prototype.name,
+ SyncMigrationMeasurement1.prototype.version);
+ return this.enqueueStorageOperation(function() {
+ return m.setDailyLastText("state", data);
+ });
+ }
+
+ case "fxa-migration:internal-telemetry": {
+ // |data| is our field name.
+ let m = this.getMeasurement(SyncMigrationMeasurement1.prototype.name,
+ SyncMigrationMeasurement1.prototype.version);
+ return this.enqueueStorageOperation(function() {
+ switch (data) {
+ case "accepted":
+ case "declined":
+ return m.incrementDailyCounter(data);
+ case "unlinked":
+ return m.setDailyLastNumeric(data, 1);
+ default:
+ Cu.reportError("Unexpected migration field in sync healthreport provider: " + data);
+ return Promise.resolve();
+ }
+ });
+ }
+
+ default:
+ Cu.reportError("unexpected migration topic in sync healthreport provider: " + topic);
+ return;
+ }
+ },
+
+ collectDailyData: function () {
+ return this.storage.enqueueTransaction(this._populateDailyData.bind(this));
+ },
+
+ _populateDailyData: function* () {
+ let m = this.getMeasurement(SyncMeasurement1.prototype.name,
+ SyncMeasurement1.prototype.version);
+
+ let svc = Cc["@mozilla.org/weave/service;1"]
+ .getService(Ci.nsISupports)
+ .wrappedJSObject;
+
+ let enabled = svc.enabled;
+ yield m.setDailyLastNumeric("enabled", enabled ? 1 : 0);
+
+ // preferredProtocol is constant and only changes as the client
+ // evolves.
+ yield m.setDailyLastText("preferredProtocol", "1.5");
+
+ let protocol = svc.fxAccountsEnabled ? "1.5" : "1.1";
+ yield m.setDailyLastText("activeProtocol", protocol);
+
+ if (!enabled) {
+ return;
+ }
+
+ // Before grabbing more information, be sure the Sync service
+ // is fully initialized. This has the potential to initialize
+ // Sync on the spot. This may be undesired if Sync appears to
+ // be enabled but it really isn't. That responsibility should
+ // be up to svc.enabled to not return false positives, however.
+ yield svc.whenLoaded();
+
+ if (Weave.Status.service != Weave.STATUS_OK) {
+ return;
+ }
+
+ // Device types are dynamic. So we need to dynamically create fields if
+ // they don't exist.
+ let dm = this.getMeasurement(SyncDevicesMeasurement1.prototype.name,
+ SyncDevicesMeasurement1.prototype.version);
+ let devices = Weave.Service.clientsEngine.deviceTypes;
+ for (let [field, count] of devices) {
+ let hasField = this.storage.hasFieldFromMeasurement(dm.id, field,
+ this.storage.FIELD_DAILY_LAST_NUMERIC);
+ let fieldID;
+ if (hasField) {
+ fieldID = this.storage.fieldIDFromMeasurement(dm.id, field);
+ } else {
+ fieldID = yield this.storage.registerField(dm.id, field,
+ this.storage.FIELD_DAILY_LAST_NUMERIC);
+ }
+
+ yield this.storage.setDailyLastNumericFromFieldID(fieldID, count);
+ }
+ },
+});
diff --git a/services/sync/modules/identity.js b/services/sync/modules/identity.js
index e3ecd7635..2bee13b5b 100644
--- a/services/sync/modules/identity.js
+++ b/services/sync/modules/identity.js
@@ -9,8 +9,9 @@ this.EXPORTED_SYMBOLS = ["IdentityManager"];
const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Promise.jsm");
Cu.import("resource://services-sync/constants.js");
-Cu.import("resource://services-common/log4moz.js");
+Cu.import("resource://gre/modules/Log.jsm");
Cu.import("resource://services-sync/util.js");
// Lazy import to prevent unnecessary load on startup.
@@ -21,7 +22,8 @@ for (let symbol of ["BulkKeyBundle", "SyncKeyBundle"]) {
}
/**
- * Manages identity and authentication for Sync.
+ * Manages "legacy" identity and authentication for Sync.
+ * See browserid_identity for the Firefox Accounts based identity manager.
*
* The following entities are managed:
*
@@ -57,8 +59,8 @@ for (let symbol of ["BulkKeyBundle", "SyncKeyBundle"]) {
* and any other function that involves the built-in functionality.
*/
this.IdentityManager = function IdentityManager() {
- this._log = Log4Moz.repository.getLogger("Sync.Identity");
- this._log.Level = Log4Moz.Level[Svc.Prefs.get("log.logger.identity")];
+ this._log = Log.repository.getLogger("Sync.Identity");
+ this._log.Level = Log.Level[Svc.Prefs.get("log.logger.identity")];
this._basicPassword = null;
this._basicPasswordAllowLookup = true;
@@ -81,6 +83,45 @@ IdentityManager.prototype = {
_syncKeyBundle: null,
+ /**
+ * Initialize the identity provider. Returns a promise that is resolved
+ * when initialization is complete and the provider can be queried for
+ * its state
+ */
+ initialize: function() {
+ // Nothing to do for this identity provider.
+ return Promise.resolve();
+ },
+
+ finalize: function() {
+ // Nothing to do for this identity provider.
+ return Promise.resolve();
+ },
+
+ /**
+ * Called whenever Service.logout() is called.
+ */
+ logout: function() {
+ // nothing to do for this identity provider.
+ },
+
+ /**
+ * Ensure the user is logged in. Returns a promise that resolves when
+ * the user is logged in, or is rejected if the login attempt has failed.
+ */
+ ensureLoggedIn: function() {
+ // nothing to do for this identity provider
+ return Promise.resolve();
+ },
+
+ /**
+ * Indicates if the identity manager is still initializing
+ */
+ get readyToAuthenticate() {
+ // We initialize in a fully sync manner, so we are always finished.
+ return true;
+ },
+
get account() {
return Svc.Prefs.get("account", this.username);
},
@@ -133,7 +174,21 @@ IdentityManager.prototype = {
// If we change the username, we interpret this as a major change event
// and wipe out the credentials.
this._log.info("Username changed. Removing stored credentials.");
+ this.resetCredentials();
+ },
+
+ /**
+ * Resets/Drops all credentials we hold for the current user.
+ */
+ resetCredentials: function() {
this.basicPassword = null;
+ this.resetSyncKey();
+ },
+
+ /**
+ * Resets/Drops the sync key we hold for the current user.
+ */
+ resetSyncKey: function() {
this.syncKey = null;
// syncKeyBundle cleared as a result of setting syncKey.
},
@@ -323,6 +378,25 @@ IdentityManager.prototype = {
},
/**
+ * Verify the current auth state, unlocking the master-password if necessary.
+ *
+ * Returns a promise that resolves with the current auth state after
+ * attempting to unlock.
+ */
+ unlockAndVerifyAuthState: function() {
+ // Try to fetch the passphrase - this will prompt for MP unlock as a
+ // side-effect...
+ try {
+ this.syncKey;
+ } catch (ex) {
+ this._log.debug("Fetching passphrase threw " + ex +
+ "; assuming master password locked.");
+ return Promise.resolve(MASTER_PASSWORD_LOCKED);
+ }
+ return Promise.resolve(STATUS_OK);
+ },
+
+ /**
* Persist credentials to password store.
*
* When credentials are updated, they are changed in memory only. This will
@@ -373,6 +447,22 @@ IdentityManager.prototype = {
},
/**
+ * Pre-fetches any information that might help with migration away from this
+ * identity. Called after every sync and is really just an optimization that
+ * allows us to avoid a network request for when we actually need the
+ * migration info.
+ */
+ prefetchMigrationSentinel: function(service) {
+ // Try and fetch the migration sentinel - it will end up in the recordManager
+ // cache.
+ try {
+ service.recordManager.get(service.storageURL + "meta/fxa_credentials");
+ } catch (ex) {
+ this._log.warn("Failed to pre-fetch the migration sentinel", ex);
+ }
+ },
+
+ /**
* Obtains the array of basic logins from nsiPasswordManager.
*/
_getLogins: function _getLogins(realm) {
@@ -411,12 +501,21 @@ IdentityManager.prototype = {
},
/**
+ * Return credentials hosts for this identity only.
+ */
+ _getSyncCredentialsHosts: function() {
+ return Utils.getSyncCredentialsHostsLegacy();
+ },
+
+ /**
* Deletes Sync credentials from the password manager.
*/
deleteSyncCredentials: function deleteSyncCredentials() {
- let logins = Services.logins.findLogins({}, PWDMGR_HOST, "", "");
- for each (let login in logins) {
- Services.logins.removeLogin(login);
+ for (let host of this._getSyncCredentialsHosts()) {
+ let logins = Services.logins.findLogins({}, host, "", "");
+ for each (let login in logins) {
+ Services.logins.removeLogin(login);
+ }
}
// Wait until after store is updated in case it fails.
@@ -491,5 +590,15 @@ IdentityManager.prototype = {
onRESTRequestBasic: function onRESTRequestBasic(request) {
let up = this.username + ":" + this.basicPassword;
request.setHeader("authorization", "Basic " + btoa(up));
- }
+ },
+
+ createClusterManager: function(service) {
+ Cu.import("resource://services-sync/stages/cluster.js");
+ return new ClusterManager(service);
+ },
+
+ offerSyncOptions: function () {
+ // Do nothing for Sync 1.1.
+ return {accepted: true};
+ },
};
diff --git a/services/sync/modules/jpakeclient.js b/services/sync/modules/jpakeclient.js
index a8e343543..10f405371 100644
--- a/services/sync/modules/jpakeclient.js
+++ b/services/sync/modules/jpakeclient.js
@@ -6,7 +6,7 @@ this.EXPORTED_SYMBOLS = ["JPAKEClient", "SendCredentialsController"];
const {classes: Cc, interfaces: Ci, results: Cr, utils: Cu} = Components;
-Cu.import("resource://services-common/log4moz.js");
+Cu.import("resource://gre/modules/Log.jsm");
Cu.import("resource://services-common/rest.js");
Cu.import("resource://services-sync/constants.js");
Cu.import("resource://services-sync/util.js");
@@ -114,8 +114,8 @@ const JPAKE_VERIFY_VALUE = "0123456789ABCDEF";
this.JPAKEClient = function JPAKEClient(controller) {
this.controller = controller;
- this._log = Log4Moz.repository.getLogger("Sync.JPAKEClient");
- this._log.level = Log4Moz.Level[Svc.Prefs.get(
+ this._log = Log.repository.getLogger("Sync.JPAKEClient");
+ this._log.level = Log.Level[Svc.Prefs.get(
"log.logger.service.jpakeclient", "Debug")];
this._serverURL = Svc.Prefs.get("jpake.serverURL");
@@ -700,8 +700,8 @@ JPAKEClient.prototype = {
*/
this.SendCredentialsController =
function SendCredentialsController(jpakeclient, service) {
- this._log = Log4Moz.repository.getLogger("Sync.SendCredentialsController");
- this._log.level = Log4Moz.Level[Svc.Prefs.get("log.logger.service.main")];
+ this._log = Log.repository.getLogger("Sync.SendCredentialsController");
+ this._log.level = Log.Level[Svc.Prefs.get("log.logger.service.main")];
this._log.trace("Loading.");
this.jpakeclient = jpakeclient;
diff --git a/services/sync/modules/keys.js b/services/sync/modules/keys.js
index e228db31f..bf909bdc2 100644
--- a/services/sync/modules/keys.js
+++ b/services/sync/modules/keys.js
@@ -12,7 +12,7 @@ this.EXPORTED_SYMBOLS = [
const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
Cu.import("resource://services-sync/constants.js");
-Cu.import("resource://services-common/log4moz.js");
+Cu.import("resource://gre/modules/Log.jsm");
Cu.import("resource://services-sync/util.js");
/**
@@ -120,7 +120,7 @@ KeyBundle.prototype = {
* This is just a KeyBundle with a collection attached.
*/
this.BulkKeyBundle = function BulkKeyBundle(collection) {
- let log = Log4Moz.repository.getLogger("Sync.BulkKeyBundle");
+ let log = Log.repository.getLogger("Sync.BulkKeyBundle");
log.info("BulkKeyBundle being created for " + collection);
KeyBundle.call(this);
@@ -177,7 +177,7 @@ BulkKeyBundle.prototype = {
* If the username or Sync Key is invalid, an Error will be thrown.
*/
this.SyncKeyBundle = function SyncKeyBundle(username, syncKey) {
- let log = Log4Moz.repository.getLogger("Sync.SyncKeyBundle");
+ let log = Log.repository.getLogger("Sync.SyncKeyBundle");
log.info("SyncKeyBundle being created.");
KeyBundle.call(this);
diff --git a/services/sync/modules/main.js b/services/sync/modules/main.js
index df3868e20..488a2594b 100644
--- a/services/sync/modules/main.js
+++ b/services/sync/modules/main.js
@@ -21,7 +21,7 @@ function lazyImport(module, dest, props) {
delete dest[prop];
return dest[prop] = ns[prop];
};
- props.forEach(function(prop) dest.__defineGetter__(prop, getter(prop)));
+ props.forEach(function (prop) { dest.__defineGetter__(prop, getter(prop)); });
}
for (let mod in lazies) {
diff --git a/services/sync/modules/notifications.js b/services/sync/modules/notifications.js
index 18955aa45..1ee24f2cd 100644
--- a/services/sync/modules/notifications.js
+++ b/services/sync/modules/notifications.js
@@ -10,14 +10,14 @@ const Cr = Components.results;
const Cu = Components.utils;
Cu.import("resource://services-common/observers.js");
-Cu.import("resource://services-common/log4moz.js");
+Cu.import("resource://gre/modules/Log.jsm");
Cu.import("resource://services-sync/util.js");
this.Notifications = {
// Match the referenced values in toolkit/content/widgets/notification.xml.
- get PRIORITY_INFO() 1, // PRIORITY_INFO_LOW
- get PRIORITY_WARNING() 4, // PRIORITY_WARNING_LOW
- get PRIORITY_ERROR() 7, // PRIORITY_CRITICAL_LOW
+ get PRIORITY_INFO() { return 1; }, // PRIORITY_INFO_LOW
+ get PRIORITY_WARNING() { return 4; }, // PRIORITY_WARNING_LOW
+ get PRIORITY_ERROR() { return 7; }, // PRIORITY_CRITICAL_LOW
// FIXME: instead of making this public, dress the Notifications object
// to behave like an iterator (using generators?) and have callers access
@@ -68,8 +68,8 @@ this.Notifications = {
* Title of notifications to remove; falsy value means remove all
*/
removeAll: function Notifications_removeAll(title) {
- this.notifications.filter(function(old) old.title == title || !title).
- forEach(function(old) this.remove(old), this);
+ this.notifications.filter(old => (old.title == title || !title)).
+ forEach(old => { this.remove(old); }, this);
},
// replaces all existing notifications with the same title as the new one
@@ -84,7 +84,7 @@ this.Notifications = {
* A basic notification. Subclass this to create more complex notifications.
*/
this.Notification =
- function Notification(title, description, iconURL, priority, buttons) {
+function Notification(title, description, iconURL, priority, buttons, link) {
this.title = title;
this.description = description;
@@ -96,6 +96,9 @@ this.Notification =
if (buttons)
this.buttons = buttons;
+
+ if (link)
+ this.link = link;
}
// We set each prototype property individually instead of redefining
@@ -115,7 +118,7 @@ this.NotificationButton =
try {
callback.apply(this, arguments);
} catch (e) {
- let logger = Log4Moz.repository.getLogger("Sync.Notifications");
+ let logger = Log.repository.getLogger("Sync.Notifications");
logger.error("An exception occurred: " + Utils.exceptionStr(e));
logger.info(Utils.stackTrace(e));
throw e;
diff --git a/services/sync/modules/policies.js b/services/sync/modules/policies.js
index 8cd8ab46b..d799cb235 100644
--- a/services/sync/modules/policies.js
+++ b/services/sync/modules/policies.js
@@ -9,18 +9,21 @@ this.EXPORTED_SYMBOLS = [
const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
-Cu.import("resource://services-common/log4moz.js");
+Cu.import("resource://gre/modules/Log.jsm");
Cu.import("resource://services-sync/constants.js");
Cu.import("resource://services-sync/engines.js");
-Cu.import("resource://services-sync/status.js");
Cu.import("resource://services-sync/util.js");
+Cu.import("resource://services-common/logmanager.js");
+
+XPCOMUtils.defineLazyModuleGetter(this, "Status",
+ "resource://services-sync/status.js");
this.SyncScheduler = function SyncScheduler(service) {
this.service = service;
this.init();
}
SyncScheduler.prototype = {
- _log: Log4Moz.repository.getLogger("Sync.SyncScheduler"),
+ _log: Log.repository.getLogger("Sync.SyncScheduler"),
_fatalLoginStatus: [LOGIN_FAILED_NO_USERNAME,
LOGIN_FAILED_NO_PASSWORD,
@@ -36,10 +39,18 @@ SyncScheduler.prototype = {
setDefaults: function setDefaults() {
this._log.trace("Setting SyncScheduler policy values to defaults.");
- this.singleDeviceInterval = Svc.Prefs.get("scheduler.singleDeviceInterval") * 1000;
+ let service = Cc["@mozilla.org/weave/service;1"]
+ .getService(Ci.nsISupports)
+ .wrappedJSObject;
+
+ let part = service.fxAccountsEnabled ? "fxa" : "sync11";
+ let prefSDInterval = "scheduler." + part + ".singleDeviceInterval";
+ this.singleDeviceInterval = Svc.Prefs.get(prefSDInterval) * 1000;
+
this.idleInterval = Svc.Prefs.get("scheduler.idleInterval") * 1000;
this.activeInterval = Svc.Prefs.get("scheduler.activeInterval") * 1000;
this.immediateInterval = Svc.Prefs.get("scheduler.immediateInterval") * 1000;
+ this.eolInterval = Svc.Prefs.get("scheduler.eolInterval") * 1000;
// A user is non-idle on startup by default.
this.idle = false;
@@ -66,7 +77,7 @@ SyncScheduler.prototype = {
set numClients(value) Svc.Prefs.set("numClients", value),
init: function init() {
- this._log.level = Log4Moz.Level[Svc.Prefs.get("log.logger.service.main")];
+ this._log.level = Log.Level[Svc.Prefs.get("log.logger.service.main")];
this.setDefaults();
Svc.Obs.add("weave:engine:score:updated", this);
Svc.Obs.add("network:offline-status-changed", this);
@@ -82,8 +93,10 @@ SyncScheduler.prototype = {
Svc.Obs.add("weave:engine:sync:applied", this);
Svc.Obs.add("weave:service:setup-complete", this);
Svc.Obs.add("weave:service:start-over", this);
+ Svc.Obs.add("FxA:hawk:backoff:interval", this);
if (Status.checkSetup() == STATUS_OK) {
+ Svc.Obs.add("wake_notification", this);
Svc.Idle.addIdleObserver(this, Svc.Prefs.get("scheduler.idleTime"));
}
},
@@ -171,6 +184,7 @@ SyncScheduler.prototype = {
this.nextSync = 0;
this.handleSyncError();
break;
+ case "FxA:hawk:backoff:interval":
case "weave:service:backoff:interval":
let requested_interval = subject * 1000;
this._log.debug("Got backoff notification: " + requested_interval + "ms");
@@ -199,6 +213,7 @@ SyncScheduler.prototype = {
case "weave:service:setup-complete":
Services.prefs.savePrefFile(null);
Svc.Idle.addIdleObserver(this, Svc.Prefs.get("scheduler.idleTime"));
+ Svc.Obs.add("wake_notification", this);
break;
case "weave:service:start-over":
this.setDefaults();
@@ -217,7 +232,7 @@ SyncScheduler.prototype = {
// were just active.)
this.adjustSyncInterval();
break;
- case "back":
+ case "active":
this._log.trace("Received notification that we're back from idle.");
this.idle = false;
Utils.namedTimer(function onBack() {
@@ -234,15 +249,32 @@ SyncScheduler.prototype = {
}
}, IDLE_OBSERVER_BACK_DELAY, this, "idleDebouncerTimer");
break;
+ case "wake_notification":
+ this._log.debug("Woke from sleep.");
+ Utils.nextTick(() => {
+ // Trigger a sync if we have multiple clients.
+ if (this.numClients > 1) {
+ this._log.debug("More than 1 client. Syncing.");
+ this.scheduleNextSync(0);
+ }
+ });
+ break;
}
},
adjustSyncInterval: function adjustSyncInterval() {
+ if (Status.eol) {
+ this._log.debug("Server status is EOL; using eolInterval.");
+ this.syncInterval = this.eolInterval;
+ return;
+ }
+
if (this.numClients <= 1) {
this._log.trace("Adjusting syncInterval to singleDeviceInterval.");
this.syncInterval = this.singleDeviceInterval;
return;
}
+
// Only MULTI_DEVICE clients will enter this if statement
// since SINGLE_USER clients will be handled above.
if (this.idle) {
@@ -464,22 +496,67 @@ SyncScheduler.prototype = {
if (this.syncTimer)
this.syncTimer.clear();
},
-};
-const LOG_PREFIX_SUCCESS = "success-";
-const LOG_PREFIX_ERROR = "error-";
+ /**
+ * Prevent new syncs from starting. This is used by the FxA migration code
+ * where we can't afford to have a sync start partway through the migration.
+ * To handle the edge-case of a sync starting and not stopping, we store
+ * this state in a pref, so on the next startup we remain blocked (and thus
+ * sync will never start) so the migration can complete.
+ *
+ * As a safety measure, we only block for some period of time, and after
+ * that it will automatically unblock. This ensures that if things go
+ * really pear-shaped and we never end up calling unblockSync() we haven't
+ * completely broken the world.
+ */
+ blockSync: function(until = null) {
+ if (!until) {
+ until = Date.now() + DEFAULT_BLOCK_PERIOD;
+ }
+ // until is specified in ms, but Prefs can't hold that much
+ Svc.Prefs.set("scheduler.blocked-until", Math.floor(until / 1000));
+ },
+
+ unblockSync: function() {
+ Svc.Prefs.reset("scheduler.blocked-until");
+ // the migration code should be ready to roll, so resume normal operations.
+ this.checkSyncStatus();
+ },
+
+ get isBlocked() {
+ let until = Svc.Prefs.get("scheduler.blocked-until");
+ if (until === undefined) {
+ return false;
+ }
+ if (until <= Math.floor(Date.now() / 1000)) {
+ // we were previously blocked but the time has expired.
+ Svc.Prefs.reset("scheduler.blocked-until");
+ return false;
+ }
+ // we remain blocked.
+ return true;
+ },
+};
this.ErrorHandler = function ErrorHandler(service) {
this.service = service;
this.init();
}
ErrorHandler.prototype = {
+ MINIMUM_ALERT_INTERVAL_MSEC: 604800000, // One week.
/**
* Flag that turns on error reporting for all errors, incl. network errors.
*/
dontIgnoreErrors: false,
+ /**
+ * Flag that indicates if we have already reported a prolonged failure.
+ * Once set, we don't report it again, meaning this error is only reported
+ * one per run.
+ */
+ didReportProlongedError: false,
+
init: function init() {
Svc.Obs.add("weave:engine:sync:applied", this);
Svc.Obs.add("weave:engine:sync:error", this);
@@ -491,25 +568,16 @@ ErrorHandler.prototype = {
},
initLogs: function initLogs() {
- this._log = Log4Moz.repository.getLogger("Sync.ErrorHandler");
- this._log.level = Log4Moz.Level[Svc.Prefs.get("log.logger.service.main")];
- this._cleaningUpFileLogs = false;
+ this._log = Log.repository.getLogger("Sync.ErrorHandler");
+ this._log.level = Log.Level[Svc.Prefs.get("log.logger.service.main")];
- let root = Log4Moz.repository.getLogger("Sync");
- root.level = Log4Moz.Level[Svc.Prefs.get("log.rootLogger")];
+ let root = Log.repository.getLogger("Sync");
+ root.level = Log.Level[Svc.Prefs.get("log.rootLogger")];
- let formatter = new Log4Moz.BasicFormatter();
- let capp = new Log4Moz.ConsoleAppender(formatter);
- capp.level = Log4Moz.Level[Svc.Prefs.get("log.appender.console")];
- root.addAppender(capp);
+ let logs = ["Sync", "FirefoxAccounts", "Hawk", "Common.TokenServerClient",
+ "Sync.SyncMigration"];
- let dapp = new Log4Moz.DumpAppender(formatter);
- dapp.level = Log4Moz.Level[Svc.Prefs.get("log.appender.dump")];
- root.addAppender(dapp);
-
- let fapp = this._logAppender = new Log4Moz.StorageStreamAppender(formatter);
- fapp.level = Log4Moz.Level[Svc.Prefs.get("log.appender.file.level")];
- root.addAppender(fapp);
+ this._logManager = new LogManager(Svc.Prefs, logs, "sync");
},
observe: function observe(subject, topic, data) {
@@ -534,8 +602,7 @@ ErrorHandler.prototype = {
this._log.debug(engine_name + " failed: " + Utils.exceptionStr(exception));
break;
case "weave:service:login:error":
- this.resetFileLog(Svc.Prefs.get("log.appender.file.logOnError"),
- LOG_PREFIX_ERROR);
+ this.resetFileLog(this._logManager.REASON_ERROR);
if (this.shouldReportError()) {
this.notifyOnNextTick("weave:ui:login:error");
@@ -550,8 +617,7 @@ ErrorHandler.prototype = {
this.service.logout();
}
- this.resetFileLog(Svc.Prefs.get("log.appender.file.logOnError"),
- LOG_PREFIX_ERROR);
+ this.resetFileLog(this._logManager.REASON_ERROR);
if (this.shouldReportError()) {
this.notifyOnNextTick("weave:ui:sync:error");
@@ -577,8 +643,7 @@ ErrorHandler.prototype = {
if (Status.service == SYNC_FAILED_PARTIAL) {
this._log.debug("Some engines did not sync correctly.");
- this.resetFileLog(Svc.Prefs.get("log.appender.file.logOnError"),
- LOG_PREFIX_ERROR);
+ this.resetFileLog(this._logManager.REASON_ERROR);
if (this.shouldReportError()) {
this.dontIgnoreErrors = false;
@@ -586,8 +651,7 @@ ErrorHandler.prototype = {
break;
}
} else {
- this.resetFileLog(Svc.Prefs.get("log.appender.file.logOnSuccess"),
- LOG_PREFIX_SUCCESS);
+ this.resetFileLog(this._logManager.REASON_SUCCESS);
}
this.dontIgnoreErrors = false;
this.notifyOnNextTick("weave:ui:sync:finish");
@@ -615,94 +679,21 @@ ErrorHandler.prototype = {
},
/**
- * Finds all logs older than maxErrorAge and deletes them without tying up I/O.
- */
- cleanupLogs: function cleanupLogs() {
- let direntries = FileUtils.getDir("ProfD", ["weave", "logs"]).directoryEntries;
- let oldLogs = [];
- let index = 0;
- let threshold = Date.now() - 1000 * Svc.Prefs.get("log.appender.file.maxErrorAge");
-
- this._log.debug("Log cleanup threshold time: " + threshold);
- while (direntries.hasMoreElements()) {
- let logFile = direntries.getNext().QueryInterface(Ci.nsIFile);
- if (logFile.lastModifiedTime < threshold) {
- this._log.trace(" > Noting " + logFile.leafName +
- " for cleanup (" + logFile.lastModifiedTime + ")");
- oldLogs.push(logFile);
- }
- }
-
- // Deletes a file from oldLogs each tick until there are none left.
- let errorHandler = this;
- function deleteFile() {
- if (index >= oldLogs.length) {
- errorHandler._log.debug("Done deleting files.");
- errorHandler._cleaningUpFileLogs = false;
- Svc.Obs.notify("weave:service:cleanup-logs");
- return;
- }
- try {
- let file = oldLogs[index];
- file.remove(false);
- errorHandler._log.trace("Deleted " + file.leafName + ".");
- } catch (ex) {
- errorHandler._log._debug("Encountered error trying to clean up old log file '"
- + oldLogs[index].leafName + "':"
- + Utils.exceptionStr(ex));
- }
- index++;
- Utils.nextTick(deleteFile);
- }
-
- if (oldLogs.length > 0) {
- this._cleaningUpFileLogs = true;
- Utils.nextTick(deleteFile);
- } else {
- this._log.debug("No logs to clean up.");
- }
- },
-
- /**
* Generate a log file for the sync that just completed
* and refresh the input & output streams.
*
- * @param flushToFile
- * the log file to be flushed/reset
- *
- * @param filenamePrefix
- * a value of either LOG_PREFIX_SUCCESS or LOG_PREFIX_ERROR
- * to be used as the log filename prefix
+ * @param reason
+ * A constant from the LogManager that indicates the reason for the
+ * reset.
*/
- resetFileLog: function resetFileLog(flushToFile, filenamePrefix) {
- let inStream = this._logAppender.getInputStream();
- this._logAppender.reset();
- if (flushToFile && inStream) {
- this._log.debug("Flushing file log.");
- try {
- let filename = filenamePrefix + Date.now() + ".txt";
- let file = FileUtils.getFile("ProfD", ["weave", "logs", filename]);
- let outStream = FileUtils.openFileOutputStream(file);
-
- this._log.trace("Beginning stream copy to " + file.leafName + ": " +
- Date.now());
- NetUtil.asyncCopy(inStream, outStream, function onCopyComplete() {
- this._log.trace("onCopyComplete: " + Date.now());
- this._log.trace("Output file timestamp: " + file.lastModifiedTime);
- Svc.Obs.notify("weave:service:reset-file-log");
- this._log.trace("Notified: " + Date.now());
- if (filenamePrefix == LOG_PREFIX_ERROR &&
- !this._cleaningUpFileLogs) {
- this._log.trace("Scheduling cleanup.");
- Utils.nextTick(this.cleanupLogs, this);
- }
- }.bind(this));
- } catch (ex) {
- Svc.Obs.notify("weave:service:reset-file-log");
- }
- } else {
+ resetFileLog: function resetFileLog(reason) {
+ let onComplete = () => {
Svc.Obs.notify("weave:service:reset-file-log");
- }
+ this._log.trace("Notified: " + Date.now());
+ };
+ // Note we do not return the promise here - the caller doesn't need to wait
+ // for this to complete.
+ this._logManager.resetFileLog(reason).then(onComplete, onComplete);
},
/**
@@ -746,11 +737,23 @@ ErrorHandler.prototype = {
return true;
}
+ if (Status.login == LOGIN_FAILED_LOGIN_REJECTED) {
+ // An explicit LOGIN_REJECTED state is always reported (bug 1081158)
+ this._log.trace("shouldReportError: true (login was rejected)");
+ return true;
+ }
+
let lastSync = Svc.Prefs.get("lastSync");
if (lastSync && ((Date.now() - Date.parse(lastSync)) >
Svc.Prefs.get("errorhandler.networkFailureReportTimeout") * 1000)) {
Status.sync = PROLONGED_SYNC_FAILURE;
- this._log.trace("shouldReportError: true (prolonged sync failure).");
+ if (this.didReportProlongedError) {
+ this._log.trace("shouldReportError: false (prolonged sync failure, but" +
+ " we've already reported it).");
+ return false;
+ }
+ this._log.trace("shouldReportError: true (first prolonged sync failure).");
+ this.didReportProlongedError = true;
return true;
}
@@ -767,12 +770,97 @@ ErrorHandler.prototype = {
[Status.login, Status.sync].indexOf(LOGIN_FAILED_NETWORK_ERROR) == -1);
},
+ get currentAlertMode() {
+ return Svc.Prefs.get("errorhandler.alert.mode");
+ },
+
+ set currentAlertMode(str) {
+ return Svc.Prefs.set("errorhandler.alert.mode", str);
+ },
+
+ get earliestNextAlert() {
+ return Svc.Prefs.get("errorhandler.alert.earliestNext", 0) * 1000;
+ },
+
+ set earliestNextAlert(msec) {
+ return Svc.Prefs.set("errorhandler.alert.earliestNext", msec / 1000);
+ },
+
+ clearServerAlerts: function () {
+ // If we have any outstanding alerts, apparently they're no longer relevant.
+ Svc.Prefs.resetBranch("errorhandler.alert");
+ },
+
+ /**
+ * X-Weave-Alert headers can include a JSON object:
+ *
+ * {
+ * "code": // One of "hard-eol", "soft-eol".
+ * "url": // For "Learn more" link.
+ * "message": // Logged in Sync logs.
+ * }
+ */
+ handleServerAlert: function (xwa) {
+ if (!xwa.code) {
+ this._log.warn("Got structured X-Weave-Alert, but no alert code.");
+ return;
+ }
+
+ switch (xwa.code) {
+ // Gently and occasionally notify the user that this service will be
+ // shutting down.
+ case "soft-eol":
+ // Fall through.
+
+ // Tell the user that this service has shut down, and drop our syncing
+ // frequency dramatically.
+ case "hard-eol":
+ // Note that both of these alerts should be subservient to future "sign
+ // in with your Firefox Account" storage alerts.
+ if ((this.currentAlertMode != xwa.code) ||
+ (this.earliestNextAlert < Date.now())) {
+ Utils.nextTick(function() {
+ Svc.Obs.notify("weave:eol", xwa);
+ }, this);
+ this._log.error("X-Weave-Alert: " + xwa.code + ": " + xwa.message);
+ this.earliestNextAlert = Date.now() + this.MINIMUM_ALERT_INTERVAL_MSEC;
+ this.currentAlertMode = xwa.code;
+ }
+ break;
+ default:
+ this._log.debug("Got unexpected X-Weave-Alert code: " + xwa.code);
+ }
+ },
+
/**
* Handle HTTP response results or exceptions and set the appropriate
* Status.* bits.
+ *
+ * This method also looks for "side-channel" warnings.
*/
- checkServerError: function checkServerError(resp) {
+ checkServerError: function (resp) {
switch (resp.status) {
+ case 200:
+ case 404:
+ case 513:
+ let xwa = resp.headers['x-weave-alert'];
+
+ // Only process machine-readable alerts.
+ if (!xwa || !xwa.startsWith("{")) {
+ this.clearServerAlerts();
+ return;
+ }
+
+ try {
+ xwa = JSON.parse(xwa);
+ } catch (ex) {
+ this._log.warn("Malformed X-Weave-Alert from server: " + xwa);
+ return;
+ }
+
+ this.handleServerAlert(xwa);
+ break;
+
case 400:
if (resp == RESPONSE_OVER_QUOTA) {
Status.sync = OVER_QUOTA;
diff --git a/services/sync/modules/record.js b/services/sync/modules/record.js
index b1194bea6..4b3324d30 100644
--- a/services/sync/modules/record.js
+++ b/services/sync/modules/record.js
@@ -18,7 +18,7 @@ const Cu = Components.utils;
const CRYPTO_COLLECTION = "crypto";
const KEYS_WBO = "keys";
-Cu.import("resource://services-common/log4moz.js");
+Cu.import("resource://gre/modules/Log.jsm");
Cu.import("resource://services-sync/constants.js");
Cu.import("resource://services-sync/keys.js");
Cu.import("resource://services-sync/resource.js");
@@ -105,68 +105,6 @@ WBORecord.prototype = {
Utils.deferGetSet(WBORecord, "data", ["id", "modified", "sortindex", "payload"]);
-/**
- * An interface and caching layer for records.
- */
-this.RecordManager = function RecordManager(service) {
- this.service = service;
-
- this._log = Log4Moz.repository.getLogger(this._logName);
- this._records = {};
-}
-RecordManager.prototype = {
- _recordType: WBORecord,
- _logName: "Sync.RecordManager",
-
- import: function RecordMgr_import(url) {
- this._log.trace("Importing record: " + (url.spec ? url.spec : url));
- try {
- // Clear out the last response with empty object if GET fails
- this.response = {};
- this.response = this.service.resource(url).get();
-
- // Don't parse and save the record on failure
- if (!this.response.success)
- return null;
-
- let record = new this._recordType(url);
- record.deserialize(this.response);
-
- return this.set(url, record);
- } catch(ex) {
- this._log.debug("Failed to import record: " + Utils.exceptionStr(ex));
- return null;
- }
- },
-
- get: function RecordMgr_get(url) {
- // Use a url string as the key to the hash
- let spec = url.spec ? url.spec : url;
- if (spec in this._records)
- return this._records[spec];
- return this.import(url);
- },
-
- set: function RecordMgr_set(url, record) {
- let spec = url.spec ? url.spec : url;
- return this._records[spec] = record;
- },
-
- contains: function RecordMgr_contains(url) {
- if ((url.spec || url) in this._records)
- return true;
- return false;
- },
-
- clearCache: function recordMgr_clearCache() {
- this._records = {};
- },
-
- del: function RecordMgr_del(url) {
- delete this._records[url];
- }
-};
-
this.CryptoWrapper = function CryptoWrapper(collection, id) {
this.cleartext = {};
WBORecord.call(this, collection, id);
@@ -269,6 +207,67 @@ CryptoWrapper.prototype = {
Utils.deferGetSet(CryptoWrapper, "payload", ["ciphertext", "IV", "hmac"]);
Utils.deferGetSet(CryptoWrapper, "cleartext", "deleted");
+/**
+ * An interface and caching layer for records.
+ */
+this.RecordManager = function RecordManager(service) {
+ this.service = service;
+
+ this._log = Log.repository.getLogger(this._logName);
+ this._records = {};
+}
+RecordManager.prototype = {
+ _recordType: CryptoWrapper,
+ _logName: "Sync.RecordManager",
+
+ import: function RecordMgr_import(url) {
+ this._log.trace("Importing record: " + (url.spec ? url.spec : url));
+ try {
+ // Clear out the last response with empty object if GET fails
+ this.response = {};
+ this.response = this.service.resource(url).get();
+
+ // Don't parse and save the record on failure
+ if (!this.response.success)
+ return null;
+
+ let record = new this._recordType(url);
+ record.deserialize(this.response);
+
+ return this.set(url, record);
+ } catch(ex) {
+ this._log.debug("Failed to import record: " + Utils.exceptionStr(ex));
+ return null;
+ }
+ },
+
+ get: function RecordMgr_get(url) {
+ // Use a url string as the key to the hash
+ let spec = url.spec ? url.spec : url;
+ if (spec in this._records)
+ return this._records[spec];
+ return this.import(url);
+ },
+
+ set: function RecordMgr_set(url, record) {
+ let spec = url.spec ? url.spec : url;
+ return this._records[spec] = record;
+ },
+
+ contains: function RecordMgr_contains(url) {
+ if ((url.spec || url) in this._records)
+ return true;
+ return false;
+ },
+
+ clearCache: function recordMgr_clearCache() {
+ this._records = {};
+ },
+
+ del: function RecordMgr_del(url) {
+ delete this._records[url];
+ }
+};
/**
* Keeps track of mappings between collection names ('tabs') and KeyBundles.
@@ -281,7 +280,7 @@ this.CollectionKeyManager = function CollectionKeyManager() {
this._collections = {};
this._default = null;
- this._log = Log4Moz.repository.getLogger("Sync.CollectionKeyManager");
+ this._log = Log.repository.getLogger("Sync.CollectionKeyManager");
}
// TODO: persist this locally as an Identity. Bug 610913.
diff --git a/services/sync/modules/resource.js b/services/sync/modules/resource.js
index e6587cd43..1c2a67b90 100644
--- a/services/sync/modules/resource.js
+++ b/services/sync/modules/resource.js
@@ -14,7 +14,7 @@ const Cu = Components.utils;
Cu.import("resource://gre/modules/Preferences.jsm");
Cu.import("resource://services-common/async.js");
-Cu.import("resource://services-common/log4moz.js");
+Cu.import("resource://gre/modules/Log.jsm");
Cu.import("resource://services-common/observers.js");
Cu.import("resource://services-common/utils.js");
Cu.import("resource://services-sync/constants.js");
@@ -50,9 +50,9 @@ const DEFAULT_LOAD_FLAGS =
* the status of the HTTP response.
*/
this.AsyncResource = function AsyncResource(uri) {
- this._log = Log4Moz.repository.getLogger(this._logName);
+ this._log = Log.repository.getLogger(this._logName);
this._log.level =
- Log4Moz.Level[Svc.Prefs.get("log.logger.network.resources")];
+ Log.Level[Svc.Prefs.get("log.logger.network.resources")];
this.uri = uri;
this._headers = {};
this._onComplete = Utils.bind2(this, this._onComplete);
@@ -146,7 +146,14 @@ AsyncResource.prototype = {
// to obtain a request channel.
//
_createRequest: function Res__createRequest(method) {
- let channel = Services.io.newChannel(this.spec, null, null)
+ let channel = Services.io.newChannel2(this.spec,
+ null,
+ null,
+ null, // aLoadingNode
+ Services.scriptSecurityManager.getSystemPrincipal(),
+ null, // aTriggeringPrincipal
+ Ci.nsILoadInfo.SEC_NORMAL,
+ Ci.nsIContentPolicy.TYPE_OTHER)
.QueryInterface(Ci.nsIRequest)
.QueryInterface(Ci.nsIHttpChannel);
@@ -265,7 +272,7 @@ AsyncResource.prototype = {
this._log.debug(mesg);
// Additionally give the full response body when Trace logging.
- if (this._log.level <= Log4Moz.Level.Trace)
+ if (this._log.level <= Log.Level.Trace)
this._log.trace(action + " body: " + data);
} catch(ex) {
@@ -302,6 +309,14 @@ AsyncResource.prototype = {
Observers.notify("weave:service:quota:remaining",
parseInt(headers["x-weave-quota-remaining"], 10));
}
+
+ let contentLength = headers["content-length"];
+ if (success && contentLength && data &&
+ contentLength != data.length) {
+ this._log.warn("The response body's length of: " + data.length +
+ " doesn't match the header's content-length of: " +
+ contentLength + ".");
+ }
} catch (ex) {
this._log.debug("Caught exception " + CommonUtils.exceptionStr(ex) +
" visiting headers in _onComplete.");
@@ -380,7 +395,8 @@ Resource.prototype = {
function callback(error, ret) {
if (error)
cb.throw(error);
- cb(ret);
+ else
+ cb(ret);
}
// The channel listener might get a failure code
@@ -588,8 +604,8 @@ ChannelListener.prototype = {
function ChannelNotificationListener(headersToCopy) {
this._headersToCopy = headersToCopy;
- this._log = Log4Moz.repository.getLogger(this._logName);
- this._log.level = Log4Moz.Level[Svc.Prefs.get("log.logger.network.resources")];
+ this._log = Log.repository.getLogger(this._logName);
+ this._log.level = Log.Level[Svc.Prefs.get("log.logger.network.resources")];
}
ChannelNotificationListener.prototype = {
_logName: "Sync.Resource",
@@ -609,7 +625,7 @@ ChannelNotificationListener.prototype = {
},
notifyCertProblem: function certProblem(socketInfo, sslStatus, targetHost) {
- let log = Log4Moz.repository.getLogger("Sync.CertListener");
+ let log = Log.repository.getLogger("Sync.CertListener");
log.warn("Invalid HTTPS certificate encountered!");
// This suppresses the UI warning only. The request is still cancelled.
diff --git a/services/sync/modules/rest.js b/services/sync/modules/rest.js
index 15e83a24f..34382eed5 100644
--- a/services/sync/modules/rest.js
+++ b/services/sync/modules/rest.js
@@ -4,7 +4,7 @@
const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
-Cu.import("resource://services-common/log4moz.js");
+Cu.import("resource://gre/modules/Log.jsm");
Cu.import("resource://services-common/rest.js");
Cu.import("resource://services-sync/util.js");
Cu.import("resource://services-sync/constants.js");
@@ -86,5 +86,21 @@ SyncStorageRequest.prototype = {
Svc.Obs.notify("weave:service:quota:remaining",
parseInt(headers["x-weave-quota-remaining"], 10));
}
+ },
+
+ onStopRequest: function onStopRequest(channel, context, statusCode) {
+ if (this.status != this.ABORTED) {
+ let resp = this.response;
+ let contentLength = resp.headers ? resp.headers["content-length"] : "";
+
+ if (resp.success && contentLength &&
+ contentLength != resp.body.length) {
+ this._log.warn("The response body's length of: " + resp.body.length +
+ " doesn't match the header's content-length of: " +
+ contentLength + ".");
+ }
+ }
+
+ RESTRequest.prototype.onStopRequest.apply(this, arguments);
}
};
diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js
index 85829a00f..4b792adf8 100644
--- a/services/sync/modules/service.js
+++ b/services/sync/modules/service.js
@@ -20,7 +20,7 @@ const KEYS_WBO = "keys";
Cu.import("resource://gre/modules/Preferences.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
-Cu.import("resource://services-common/log4moz.js");
+Cu.import("resource://gre/modules/Log.jsm");
Cu.import("resource://services-common/utils.js");
Cu.import("resource://services-sync/constants.js");
Cu.import("resource://services-sync/engines.js");
@@ -30,15 +30,14 @@ Cu.import("resource://services-sync/policies.js");
Cu.import("resource://services-sync/record.js");
Cu.import("resource://services-sync/resource.js");
Cu.import("resource://services-sync/rest.js");
-Cu.import("resource://services-sync/stages/cluster.js");
Cu.import("resource://services-sync/stages/enginesync.js");
+Cu.import("resource://services-sync/stages/declined.js");
Cu.import("resource://services-sync/status.js");
Cu.import("resource://services-sync/userapi.js");
Cu.import("resource://services-sync/util.js");
const ENGINE_MODULES = {
Addons: "addons.js",
- Apps: "apps.js",
Bookmarks: "bookmarks.js",
Form: "forms.js",
History: "history.js",
@@ -52,6 +51,14 @@ const STORAGE_INFO_TYPES = [INFO_COLLECTIONS,
INFO_COLLECTION_COUNTS,
INFO_QUOTA];
+// A structure mapping a (boolean) telemetry probe name to a preference name.
+// The probe will record true if the pref is modified, false otherwise.
+const TELEMETRY_CUSTOM_SERVER_PREFS = {
+ WEAVE_CUSTOM_LEGACY_SERVER_CONFIGURATION: "services.sync.serverURL",
+ WEAVE_CUSTOM_FXA_SERVER_CONFIGURATION: "identity.fxaccounts.auth.uri",
+ WEAVE_CUSTOM_TOKEN_SERVER_CONFIGURATION: "services.sync.tokenServerURI",
+};
+
function Sync11Service() {
this._notify = Utils.notify("weave:service:");
@@ -62,16 +69,11 @@ Sync11Service.prototype = {
_locked: false,
_loggedIn: false,
- userBaseURL: null,
infoURL: null,
storageURL: null,
metaURL: null,
cryptoKeyURL: null,
- get enabledEngineNames() {
- return [e.name for each (e in this.engineManager.getEnabled())];
- },
-
get serverURL() Svc.Prefs.get("serverURL"),
set serverURL(value) {
if (!value.endsWith("/")) {
@@ -111,7 +113,7 @@ Sync11Service.prototype = {
get userAPIURI() {
// Append to the serverURL if it's a relative fragment.
let url = Svc.Prefs.get("userURL");
- if (!url.contains(":")) {
+ if (!url.includes(":")) {
url = this.serverURL + url;
}
@@ -158,13 +160,18 @@ Sync11Service.prototype = {
return Utils.catch.call(this, func, lockExceptions);
},
+ get userBaseURL() {
+ if (!this._clusterManager) {
+ return null;
+ }
+ return this._clusterManager.getUserBaseURL();
+ },
+
_updateCachedURLs: function _updateCachedURLs() {
// Nothing to cache yet if we don't have the building blocks
- if (this.clusterURL == "" || this.identity.username == "")
+ if (!this.clusterURL || !this.identity.username)
return;
- let storageAPI = this.clusterURL + SYNC_API_VERSION + "/";
- this.userBaseURL = storageAPI + this.identity.username + "/";
this._log.debug("Caching URLs under storage user base: " + this.userBaseURL);
// Generate and cache various URLs under the storage API for this user
@@ -298,6 +305,21 @@ Sync11Service.prototype = {
return false;
},
+ // The global "enabled" state comes from prefs, and will be set to false
+ // whenever the UI that exposes what to sync finds all Sync engines disabled.
+ get enabled() {
+ return Svc.Prefs.get("enabled");
+ },
+ set enabled(val) {
+ // There's no real reason to impose this other than to catch someone doing
+ // something we don't expect with bad consequences - all setting of this
+ // pref are in the UI code and external to this module.
+ if (val) {
+ throw new Error("Only disabling via this setter is supported");
+ }
+ Svc.Prefs.set("enabled", val);
+ },
+
/**
* Prepare to initialize the rest of Weave after waiting a little bit
*/
@@ -318,17 +340,15 @@ Sync11Service.prototype = {
this.errorHandler = new ErrorHandler(this);
- this._log = Log4Moz.repository.getLogger("Sync.Service");
+ this._log = Log.repository.getLogger("Sync.Service");
this._log.level =
- Log4Moz.Level[Svc.Prefs.get("log.logger.service.main")];
+ Log.Level[Svc.Prefs.get("log.logger.service.main")];
this._log.info("Loading Weave " + WEAVE_VERSION);
- this._clusterManager = new ClusterManager(this);
+ this._clusterManager = this.identity.createClusterManager(this);
this.recordManager = new RecordManager(this);
- this.enabled = true;
-
this._registerEngines();
let ua = Cc["@mozilla.org/network/protocol;1?name=http"].
@@ -357,6 +377,12 @@ Sync11Service.prototype = {
Svc.Obs.notify("weave:engine:start-tracking");
}
+ // Telemetry probes to indicate if the user is using custom servers.
+ for (let [probeName, prefName] of Iterator(TELEMETRY_CUSTOM_SERVER_PREFS)) {
+ let isCustomized = Services.prefs.prefHasUserValue(prefName);
+ Services.telemetry.getHistogramById(probeName).add(isCustomized);
+ }
+
// Send an event now that Weave service is ready. We don't do this
// synchronously so that observers can import this module before
// registering an observer.
@@ -428,6 +454,12 @@ Sync11Service.prototype = {
engines = pref.split(",");
}
+ let declined = [];
+ pref = Svc.Prefs.get("declinedEngines");
+ if (pref) {
+ declined = pref.split(",");
+ }
+
this.clientsEngine = new ClientEngine(this);
for (let name of engines) {
@@ -446,12 +478,14 @@ Sync11Service.prototype = {
continue;
}
- this.engineManager.register(ns[engineName], this);
+ this.engineManager.register(ns[engineName]);
} catch (ex) {
this._log.warn("Could not register engine " + name + ": " +
CommonUtils.exceptionStr(ex));
}
}
+
+ this.engineManager.setDeclined(declined);
},
QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver,
@@ -507,9 +541,10 @@ Sync11Service.prototype = {
},
/**
- * Perform the info fetch as part of a login or key fetch.
+ * Perform the info fetch as part of a login or key fetch, or
+ * inside engine sync.
*/
- _fetchInfo: function _fetchInfo(url) {
+ _fetchInfo: function (url) {
let infoURL = url || this.infoURL;
this._log.trace("In _fetchInfo: " + infoURL);
@@ -520,9 +555,11 @@ Sync11Service.prototype = {
this.errorHandler.checkServerError(ex);
throw ex;
}
+
+ // Always check for errors; this is also where we look for X-Weave-Alert.
+ this.errorHandler.checkServerError(info);
if (!info.success) {
- this.errorHandler.checkServerError(info);
- throw "aborting sync, failed to get collections";
+ throw "Aborting sync: failed to get collections.";
}
return info;
},
@@ -646,24 +683,35 @@ Sync11Service.prototype = {
}
},
- verifyLogin: function verifyLogin() {
+ verifyLogin: function verifyLogin(allow40XRecovery = true) {
+ // If the identity isn't ready it might not know the username...
+ if (!this.identity.readyToAuthenticate) {
+ this._log.info("Not ready to authenticate in verifyLogin.");
+ this.status.login = LOGIN_FAILED_NOT_READY;
+ return false;
+ }
+
if (!this.identity.username) {
this._log.warn("No username in verifyLogin.");
this.status.login = LOGIN_FAILED_NO_USERNAME;
return false;
}
- // Unlock master password, or return.
// Attaching auth credentials to a request requires access to
// passwords, which means that Resource.get can throw MP-related
// exceptions!
- // Try to fetch the passphrase first, while we still have control.
- try {
- this.identity.syncKey;
- } catch (ex) {
- this._log.debug("Fetching passphrase threw " + ex +
- "; assuming master password locked.");
- this.status.login = MASTER_PASSWORD_LOCKED;
+ // So we ask the identity to verify the login state after unlocking the
+ // master password (ie, this call is expected to prompt for MP unlock
+ // if necessary) while we still have control.
+ let cb = Async.makeSpinningCallback();
+ this.identity.unlockAndVerifyAuthState().then(
+ result => cb(null, result),
+ cb
+ );
+ let unlockedState = cb.wait();
+ this._log.debug("Fetching unlocked auth state returned " + unlockedState);
+ if (unlockedState != STATUS_OK) {
+ this.status.login = unlockedState;
return false;
}
@@ -673,7 +721,6 @@ Sync11Service.prototype = {
// to succeed, since that probably means we just don't have storage.
if (this.clusterURL == "" && !this._clusterManager.setCluster()) {
this.status.sync = NO_SYNC_NODE_FOUND;
- Svc.Obs.notify("weave:service:sync:delayed");
return true;
}
@@ -695,7 +742,7 @@ Sync11Service.prototype = {
// Go ahead and do remote setup, so that we can determine
// conclusively that our passphrase is correct.
- if (this._remoteSetup()) {
+ if (this._remoteSetup(test)) {
// Username/password verified.
this.status.login = LOGIN_SUCCEEDED;
return true;
@@ -711,8 +758,8 @@ Sync11Service.prototype = {
case 404:
// Check that we're verifying with the correct cluster
- if (this._clusterManager.setCluster()) {
- return this.verifyLogin();
+ if (allow40XRecovery && this._clusterManager.setCluster()) {
+ return this.verifyLogin(false);
}
// We must have the right cluster, but the server doesn't expect us
@@ -763,20 +810,20 @@ Sync11Service.prototype = {
info = info.obj;
if (!(CRYPTO_COLLECTION in info)) {
- this._log.error("Consistency failure: info/collections excludes " +
+ this._log.error("Consistency failure: info/collections excludes " +
"crypto after successful upload.");
throw new Error("Symmetric key upload failed.");
}
// Can't check against local modified: clock drift.
if (info[CRYPTO_COLLECTION] < serverModified) {
- this._log.error("Consistency failure: info/collections crypto entry " +
+ this._log.error("Consistency failure: info/collections crypto entry " +
"is stale after successful upload.");
throw new Error("Symmetric key upload failed.");
}
-
+
// Doesn't matter if the timestamp is ahead.
-
+
// Download and install them.
let cryptoKeys = new CryptoWrapper(CRYPTO_COLLECTION, KEYS_WBO);
let cryptoResp = cryptoKeys.fetch(this.resource(this.cryptoKeysURL)).response;
@@ -840,14 +887,6 @@ Sync11Service.prototype = {
Svc.Obs.notify("weave:engine:stop-tracking");
this.status.resetSync();
- // We want let UI consumers of the following notification know as soon as
- // possible, so let's fake for the CLIENT_NOT_CONFIGURED status for now
- // by emptying the passphrase (we still need the password).
- this.identity.syncKey = null;
- this.status.login = LOGIN_FAILED_NO_PASSPHRASE;
- this.logout();
- Svc.Obs.notify("weave:service:start-over");
-
// Deletion doesn't make sense if we aren't set up yet!
if (this.clusterURL != "") {
// Clear client-specific data from the server, including disabled engines.
@@ -859,10 +898,20 @@ Sync11Service.prototype = {
+ Utils.exceptionStr(ex));
}
}
+ this._log.debug("Finished deleting client data.");
} else {
this._log.debug("Skipping client data removal: no cluster URL.");
}
+ // We want let UI consumers of the following notification know as soon as
+ // possible, so let's fake for the CLIENT_NOT_CONFIGURED status for now
+ // by emptying the passphrase (we still need the password).
+ this._log.info("Service.startOver dropping sync key and logging out.");
+ this.identity.resetSyncKey();
+ this.status.login = LOGIN_FAILED_NO_PASSPHRASE;
+ this.logout();
+ Svc.Obs.notify("weave:service:start-over");
+
// Reset all engines and clear keys.
this.resetClient();
this.collectionKeys.clear();
@@ -877,7 +926,36 @@ Sync11Service.prototype = {
this.identity.deleteSyncCredentials();
- Svc.Obs.notify("weave:service:start-over:finish");
+ // If necessary, reset the identity manager, then re-initialize it so the
+ // FxA manager is used. This is configurable via a pref - mainly for tests.
+ let keepIdentity = false;
+ try {
+ keepIdentity = Services.prefs.getBoolPref("services.sync-testing.startOverKeepIdentity");
+ } catch (_) { /* no such pref */ }
+ if (keepIdentity) {
+ Svc.Obs.notify("weave:service:start-over:finish");
+ return;
+ }
+
+ this.identity.finalize().then(
+ () => {
+ // an observer so the FxA migration code can take some action before
+ // the new identity is created.
+ Svc.Obs.notify("weave:service:start-over:init-identity");
+ this.identity.username = "";
+ this.status.__authManager = null;
+ this.identity = Status._authManager;
+ this._clusterManager = this.identity.createClusterManager(this);
+ Svc.Obs.notify("weave:service:start-over:finish");
+ }
+ ).then(null,
+ err => {
+ this._log.error("startOver failed to re-initialize the identity manager: " + err);
+ // Still send the observer notification so the current state is
+ // reflected in the UI.
+ Svc.Obs.notify("weave:service:start-over:finish");
+ }
+ );
},
persistLogin: function persistLogin() {
@@ -911,14 +989,20 @@ Sync11Service.prototype = {
throw "Aborting login, client not configured.";
}
+ // Ask the identity manager to explicitly login now.
+ let cb = Async.makeSpinningCallback();
+ this.identity.ensureLoggedIn().then(cb, cb);
+
+ // Just let any errors bubble up - they've more context than we do!
+ cb.wait();
+
// Calling login() with parameters when the client was
// previously not configured means setup was completed.
if (initialStatus == CLIENT_NOT_CONFIGURED
&& (username || password || passphrase)) {
Svc.Obs.notify("weave:service:setup-complete");
}
-
- this._log.info("Logging in user " + this.identity.username);
+ this._log.info("Logging in the user.");
this._updateCachedURLs();
if (!this.verifyLogin()) {
@@ -936,11 +1020,11 @@ Sync11Service.prototype = {
},
logout: function logout() {
- // No need to do anything if we're already logged out.
- if (!this._loggedIn)
- return;
-
+ // If we failed during login, we aren't going to have this._loggedIn set,
+ // but we still want to ask the identity to logout, so it doesn't try and
+ // reuse any old credentials next time we sync.
this._log.info("Logging out");
+ this.identity.logout();
this._loggedIn = false;
Svc.Obs.notify("weave:service:logout:finish");
@@ -1008,11 +1092,20 @@ Sync11Service.prototype = {
// ... fetch the current record from the server, and COPY THE FLAGS.
let newMeta = this.recordManager.get(this.metaURL);
+ // If we got a 401, we do not want to create a new meta/global - we
+ // should be able to get the existing meta after we get a new node.
+ if (this.recordManager.response.status == 401) {
+ this._log.debug("Fetching meta/global record on the server returned 401.");
+ this.errorHandler.checkServerError(this.recordManager.response);
+ return false;
+ }
+
if (!this.recordManager.response.success || !newMeta) {
this._log.debug("No meta/global record on the server. Creating one.");
newMeta = new WBORecord("meta", "global");
newMeta.payload.syncID = this.syncID;
newMeta.payload.storageVersion = STORAGE_VERSION;
+ newMeta.payload.declined = this.engineManager.getDeclined();
newMeta.isNew = true;
@@ -1162,6 +1255,10 @@ Sync11Service.prototype = {
},
sync: function sync() {
+ if (!this.enabled) {
+ this._log.debug("Not syncing as Sync is disabled.");
+ return;
+ }
let dateStr = new Date().toLocaleFormat(LOG_DATE_FORMAT);
this._log.debug("User-Agent: " + SyncStorageRequest.prototype.userAgent);
this._log.info("Starting sync at " + dateStr);
@@ -1188,6 +1285,9 @@ Sync11Service.prototype = {
return this._lock("service.js: sync",
this._notify("sync", "", function onNotify() {
+ let histogram = Services.telemetry.getHistogramById("WEAVE_START_COUNT");
+ histogram.add(1);
+
let synchronizer = new EngineSynchronizer(this);
let cb = Async.makeSpinningCallback();
synchronizer.onComplete = cb;
@@ -1196,10 +1296,142 @@ Sync11Service.prototype = {
// wait() throws if the first argument is truthy, which is exactly what
// we want.
let result = cb.wait();
+
+ histogram = Services.telemetry.getHistogramById("WEAVE_COMPLETE_SUCCESS_COUNT");
+ histogram.add(1);
+
+ // We successfully synchronized.
+ // Check if the identity wants to pre-fetch a migration sentinel from
+ // the server.
+ // If we have no clusterURL, we are probably doing a node reassignment
+ // so don't attempt to get it in that case.
+ if (this.clusterURL) {
+ this.identity.prefetchMigrationSentinel(this);
+ }
+
+ // Now let's update our declined engines.
+ let meta = this.recordManager.get(this.metaURL);
+ if (!meta) {
+ this._log.warn("No meta/global; can't update declined state.");
+ return;
+ }
+
+ let declinedEngines = new DeclinedEngines(this);
+ let didChange = declinedEngines.updateDeclined(meta, this.engineManager);
+ if (!didChange) {
+ this._log.info("No change to declined engines. Not reuploading meta/global.");
+ return;
+ }
+
+ this.uploadMetaGlobal(meta);
}))();
},
/**
+ * Upload meta/global, throwing the response on failure.
+ */
+ uploadMetaGlobal: function (meta) {
+ this._log.debug("Uploading meta/global: " + JSON.stringify(meta));
+
+ // It would be good to set the X-If-Unmodified-Since header to `timestamp`
+ // for this PUT to ensure at least some level of transactionality.
+ // Unfortunately, the servers don't support it after a wipe right now
+ // (bug 693893), so we're going to defer this until bug 692700.
+ let res = this.resource(this.metaURL);
+ let response = res.put(meta);
+ if (!response.success) {
+ throw response;
+ }
+ this.recordManager.set(this.metaURL, meta);
+ },
+
+ /**
+ * Get a migration sentinel for the Firefox Accounts migration.
+ * Returns a JSON blob - it is up to callers of this to make sense of the
+ * data.
+ *
+ * Returns a promise that resolves with the sentinel, or null.
+ */
+ getFxAMigrationSentinel: function() {
+ if (this._shouldLogin()) {
+ this._log.debug("In getFxAMigrationSentinel: should login.");
+ if (!this.login()) {
+ this._log.debug("Can't get migration sentinel: login returned false.");
+ return Promise.resolve(null);
+ }
+ }
+ if (!this.identity.syncKeyBundle) {
+ this._log.error("Can't get migration sentinel: no syncKeyBundle.");
+ return Promise.resolve(null);
+ }
+ try {
+ let collectionURL = this.storageURL + "meta/fxa_credentials";
+ let cryptoWrapper = this.recordManager.get(collectionURL);
+ if (!cryptoWrapper || !cryptoWrapper.payload) {
+ // nothing to decrypt - .decrypt is noisy in that case, so just bail
+ // now.
+ return Promise.resolve(null);
+ }
+ // If the payload has a sentinel it means we must have put back the
+ // decrypted version last time we were called.
+ if (cryptoWrapper.payload.sentinel) {
+ return Promise.resolve(cryptoWrapper.payload.sentinel);
+ }
+ // If decryption fails it almost certainly means the key is wrong - but
+ // it's not clear if we need to take special action for that case?
+ let payload = cryptoWrapper.decrypt(this.identity.syncKeyBundle);
+ // After decrypting the ciphertext is lost, so we just stash the
+ // decrypted payload back into the wrapper.
+ cryptoWrapper.payload = payload;
+ return Promise.resolve(payload.sentinel);
+ } catch (ex) {
+ this._log.error("Failed to fetch the migration sentinel: ${}", ex);
+ return Promise.resolve(null);
+ }
+ },
+
+ /**
+ * Set a migration sentinel for the Firefox Accounts migration.
+ * Accepts a JSON blob - it is up to callers of this to make sense of the
+ * data.
+ *
+ * Returns a promise that resolves with a boolean which indicates if the
+ * sentinel was successfully written.
+ */
+ setFxAMigrationSentinel: function(sentinel) {
+ if (this._shouldLogin()) {
+ this._log.debug("In setFxAMigrationSentinel: should login.");
+ if (!this.login()) {
+ this._log.debug("Can't set migration sentinel: login returned false.");
+ return Promise.resolve(false);
+ }
+ }
+ if (!this.identity.syncKeyBundle) {
+ this._log.error("Can't set migration sentinel: no syncKeyBundle.");
+ return Promise.resolve(false);
+ }
+ try {
+ let collectionURL = this.storageURL + "meta/fxa_credentials";
+ let cryptoWrapper = new CryptoWrapper("meta", "fxa_credentials");
+ cryptoWrapper.cleartext.sentinel = sentinel;
+
+ cryptoWrapper.encrypt(this.identity.syncKeyBundle);
+
+ let res = this.resource(collectionURL);
+ let response = res.put(cryptoWrapper.toJSON());
+
+ if (!response.success) {
+ throw response;
+ }
+ this.recordManager.set(collectionURL, cryptoWrapper);
+ } catch (ex) {
+ this._log.error("Failed to set the migration sentinel: ${}", ex);
+ return Promise.resolve(false);
+ }
+ return Promise.resolve(true);
+ },
+
+ /**
* If we have a passphrase, rather than a 25-alphadigit sync key,
* use the provided sync ID to bootstrap it using PBKDF2.
*
@@ -1254,26 +1486,19 @@ Sync11Service.prototype = {
let meta = new WBORecord("meta", "global");
meta.payload.syncID = this.syncID;
meta.payload.storageVersion = STORAGE_VERSION;
+ meta.payload.declined = this.engineManager.getDeclined();
meta.isNew = true;
- this._log.debug("New metadata record: " + JSON.stringify(meta.payload));
- let res = this.resource(this.metaURL);
- // It would be good to set the X-If-Unmodified-Since header to `timestamp`
- // for this PUT to ensure at least some level of transactionality.
- // Unfortunately, the servers don't support it after a wipe right now
- // (bug 693893), so we're going to defer this until bug 692700.
- let resp = res.put(meta);
- if (!resp.success) {
- // If we got into a race condition, we'll abort the sync this way, too.
- // That's fine. We'll just wait till the next sync. The client that we're
- // racing is probably busy uploading stuff right now anyway.
- throw resp;
- }
- this.recordManager.set(this.metaURL, meta);
+ // uploadMetaGlobal throws on failure -- including race conditions.
+ // If we got into a race condition, we'll abort the sync this way, too.
+ // That's fine. We'll just wait till the next sync. The client that we're
+ // racing is probably busy uploading stuff right now anyway.
+ this.uploadMetaGlobal(meta);
// Wipe everything we know about except meta because we just uploaded it
let engines = [this.clientsEngine].concat(this.engineManager.getAll());
let collections = [engine.name for each (engine in engines)];
+ // TODO: there's a bug here. We should be calling resetClient, no?
// Generate, upload, and download new keys. Do this last so we don't wipe
// them...
@@ -1381,7 +1606,9 @@ Sync11Service.prototype = {
// Only wipe the engines provided.
if (engines) {
- engines.forEach(function(e) this.clientsEngine.sendCommand("wipeEngine", [e]), this);
+ engines.forEach(function(e) {
+ this.clientsEngine.sendCommand("wipeEngine", [e]);
+ }, this);
}
// Tell the remote machines to wipe themselves.
else {
diff --git a/services/sync/modules/stages/cluster.js b/services/sync/modules/stages/cluster.js
index dd7717201..dd358bf98 100644
--- a/services/sync/modules/stages/cluster.js
+++ b/services/sync/modules/stages/cluster.js
@@ -1,12 +1,12 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this file,
- * You can obtain one at http://mozilla.org/MPL/2.0/. */
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
this.EXPORTED_SYMBOLS = ["ClusterManager"];
const {utils: Cu} = Components;
-Cu.import("resource://services-common/log4moz.js");
+Cu.import("resource://gre/modules/Log.jsm");
Cu.import("resource://services-sync/constants.js");
Cu.import("resource://services-sync/policies.js");
Cu.import("resource://services-sync/util.js");
@@ -15,8 +15,8 @@ Cu.import("resource://services-sync/util.js");
* Contains code for managing the Sync cluster we are in.
*/
this.ClusterManager = function ClusterManager(service) {
- this._log = Log4Moz.repository.getLogger("Sync.Service");
- this._log.level = Log4Moz.Level[Svc.Prefs.get("log.logger.service.main")];
+ this._log = Log.repository.getLogger("Sync.Service");
+ this._log.level = Log.Level[Svc.Prefs.get("log.logger.service.main")];
this.service = service;
}
@@ -91,5 +91,21 @@ ClusterManager.prototype = {
return true;
},
+
+ getUserBaseURL: function getUserBaseURL() {
+ // Legacy Sync and FxA Sync construct the userBaseURL differently. Legacy
+ // Sync appends path components onto an empty path, and in FxA Sync, the
+ // token server constructs this for us in an opaque manner. Since the
+ // cluster manager already sets the clusterURL on Service and also has
+ // access to the current identity, we added this functionality here.
+
+ // If the clusterURL hasn't been set, the userBaseURL shouldn't be set
+ // either. Some tests expect "undefined" to be returned here.
+ if (!this.service.clusterURL) {
+ return undefined;
+ }
+ let storageAPI = this.service.clusterURL + SYNC_API_VERSION + "/";
+ return storageAPI + this.identity.username + "/";
+ }
};
Object.freeze(ClusterManager.prototype);
diff --git a/services/sync/modules/stages/declined.js b/services/sync/modules/stages/declined.js
new file mode 100644
index 000000000..b0877e929
--- /dev/null
+++ b/services/sync/modules/stages/declined.js
@@ -0,0 +1,76 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * This file contains code for maintaining the set of declined engines,
+ * in conjunction with EngineManager.
+ */
+
+"use strict";
+
+this.EXPORTED_SYMBOLS = ["DeclinedEngines"];
+
+const {utils: Cu} = Components;
+
+Cu.import("resource://services-sync/constants.js");
+Cu.import("resource://gre/modules/Log.jsm");
+Cu.import("resource://services-common/utils.js");
+Cu.import("resource://services-common/observers.js");
+Cu.import("resource://gre/modules/Preferences.jsm");
+
+
+
+this.DeclinedEngines = function (service) {
+ this._log = Log.repository.getLogger("Sync.Declined");
+ this._log.level = Log.Level[new Preferences(PREFS_BRANCH).get("log.logger.declined")];
+
+ this.service = service;
+}
+this.DeclinedEngines.prototype = {
+ updateDeclined: function (meta, engineManager=this.service.engineManager) {
+ let enabled = new Set([e.name for each (e in engineManager.getEnabled())]);
+ let known = new Set([e.name for each (e in engineManager.getAll())]);
+ let remoteDeclined = new Set(meta.payload.declined || []);
+ let localDeclined = new Set(engineManager.getDeclined());
+
+ this._log.debug("Handling remote declined: " + JSON.stringify([...remoteDeclined]));
+ this._log.debug("Handling local declined: " + JSON.stringify([...localDeclined]));
+
+ // Any engines that are locally enabled should be removed from the remote
+ // declined list.
+ //
+ // Any engines that are locally declined should be added to the remote
+ // declined list.
+ let newDeclined = CommonUtils.union(localDeclined, CommonUtils.difference(remoteDeclined, enabled));
+
+ // If our declined set has changed, put it into the meta object and mark
+ // it as changed.
+ let declinedChanged = !CommonUtils.setEqual(newDeclined, remoteDeclined);
+ this._log.debug("Declined changed? " + declinedChanged);
+ if (declinedChanged) {
+ meta.changed = true;
+ meta.payload.declined = [...newDeclined];
+ }
+
+ // Update the engine manager regardless.
+ engineManager.setDeclined(newDeclined);
+
+ // Any engines that are locally known, locally disabled, and not remotely
+ // or locally declined, are candidates for enablement.
+ let undecided = CommonUtils.difference(CommonUtils.difference(known, enabled), newDeclined);
+ if (undecided.size) {
+ let subject = {
+ declined: newDeclined,
+ enabled: enabled,
+ known: known,
+ undecided: undecided,
+ };
+ CommonUtils.nextTick(() => {
+ Observers.notify("weave:engines:notdeclined", subject);
+ });
+ }
+
+ return declinedChanged;
+ },
+};
diff --git a/services/sync/modules/stages/enginesync.js b/services/sync/modules/stages/enginesync.js
index 823c71ffc..ed91adddb 100644
--- a/services/sync/modules/stages/enginesync.js
+++ b/services/sync/modules/stages/enginesync.js
@@ -1,6 +1,6 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this file,
- * You can obtain one at http://mozilla.org/MPL/2.0/. */
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
/**
* This file contains code for synchronizing engines.
@@ -10,7 +10,7 @@ this.EXPORTED_SYMBOLS = ["EngineSynchronizer"];
const {utils: Cu} = Components;
-Cu.import("resource://services-common/log4moz.js");
+Cu.import("resource://gre/modules/Log.jsm");
Cu.import("resource://services-sync/constants.js");
Cu.import("resource://services-sync/engines.js");
Cu.import("resource://services-sync/policies.js");
@@ -22,8 +22,8 @@ Cu.import("resource://services-sync/util.js");
* This was originally split out of service.js. The API needs lots of love.
*/
this.EngineSynchronizer = function EngineSynchronizer(service) {
- this._log = Log4Moz.repository.getLogger("Sync.Synchronizer");
- this._log.level = Log4Moz.Level[Svc.Prefs.get("log.logger.synchronizer")];
+ this._log = Log.repository.getLogger("Sync.Synchronizer");
+ this._log.level = Log.Level[Svc.Prefs.get("log.logger.synchronizer")];
this.service = service;
@@ -71,11 +71,13 @@ EngineSynchronizer.prototype = {
Svc.Prefs.set("lastPing", now);
}
+ let engineManager = this.service.engineManager;
+
// Figure out what the last modified time is for each collection
let info = this.service._fetchInfo(infoURL);
// Convert the response to an object and read out the modified times
- for (let engine of [this.service.clientsEngine].concat(this.service.engineManager.getAll())) {
+ for (let engine of [this.service.clientsEngine].concat(engineManager.getAll())) {
engine.lastModified = info.obj[engine.name] || 0;
}
@@ -97,13 +99,13 @@ EngineSynchronizer.prototype = {
// Wipe data in the desired direction if necessary
switch (Svc.Prefs.get("firstSync")) {
case "resetClient":
- this.service.resetClient(this.service.enabledEngineNames);
+ this.service.resetClient(engineManager.enabledEngineNames);
break;
case "wipeClient":
- this.service.wipeClient(this.service.enabledEngineNames);
+ this.service.wipeClient(engineManager.enabledEngineNames);
break;
case "wipeRemote":
- this.service.wipeRemote(this.service.enabledEngineNames);
+ this.service.wipeRemote(engineManager.enabledEngineNames);
break;
}
@@ -142,7 +144,7 @@ EngineSynchronizer.prototype = {
}
try {
- for each (let engine in this.service.engineManager.getEnabled()) {
+ for (let engine of engineManager.getEnabled()) {
// If there's any problems with syncing the engine, report the failure
if (!(this._syncEngine(engine)) || this.service.status.enforceBackoff) {
this._log.info("Aborting sync for failure in " + engine.name);
@@ -160,12 +162,17 @@ EngineSynchronizer.prototype = {
return;
}
- // Upload meta/global if any engines changed anything
+ // Upload meta/global if any engines changed anything.
let meta = this.service.recordManager.get(this.service.metaURL);
if (meta.isNew || meta.changed) {
- this.service.resource(this.service.metaURL).put(meta);
- delete meta.isNew;
- delete meta.changed;
+ this._log.info("meta/global changed locally: reuploading.");
+ try {
+ this.service.uploadMetaGlobal(meta);
+ delete meta.isNew;
+ delete meta.changed;
+ } catch (error) {
+ this._log.error("Unable to upload meta/global. Leaving marked as new.");
+ }
}
// If there were no sync engine failures
@@ -205,17 +212,19 @@ EngineSynchronizer.prototype = {
return true;
},
- _updateEnabledEngines: function _updateEnabledEngines() {
+ _updateEnabledFromMeta: function (meta, numClients, engineManager=this.service.engineManager) {
this._log.info("Updating enabled engines: " +
- this.service.scheduler.numClients + " clients.");
- let meta = this.service.recordManager.get(this.service.metaURL);
- if (meta.isNew || !meta.payload.engines)
+ numClients + " clients.");
+
+ if (meta.isNew || !meta.payload.engines) {
+ this._log.debug("meta/global isn't new, or is missing engines. Not updating enabled state.");
return;
+ }
// If we're the only client, and no engines are marked as enabled,
// thumb our noses at the server data: it can't be right.
// Belt-and-suspenders approach to Bug 615926.
- if ((this.service.scheduler.numClients <= 1) &&
+ if ((numClients <= 1) &&
([e for (e in meta.payload.engines) if (e != "clients")].length == 0)) {
this._log.info("One client and no enabled engines: not touching local engine status.");
return;
@@ -223,7 +232,11 @@ EngineSynchronizer.prototype = {
this.service._ignorePrefObserver = true;
- let enabled = this.service.enabledEngineNames;
+ let enabled = engineManager.enabledEngineNames;
+
+ let toDecline = new Set();
+ let toUndecline = new Set();
+
for (let engineName in meta.payload.engines) {
if (engineName == "clients") {
// Clients is special.
@@ -235,40 +248,73 @@ EngineSynchronizer.prototype = {
enabled.splice(index, 1);
continue;
}
- let engine = this.service.engineManager.get(engineName);
+ let engine = engineManager.get(engineName);
if (!engine) {
// The engine doesn't exist locally. Nothing to do.
continue;
}
- if (Svc.Prefs.get("engineStatusChanged." + engine.prefName, false)) {
- // The engine was disabled locally. Wipe server data and
- // disable it everywhere.
+ let attemptedEnable = false;
+ // If the engine was enabled remotely, enable it locally.
+ if (!Svc.Prefs.get("engineStatusChanged." + engine.prefName, false)) {
+ this._log.trace("Engine " + engineName + " was enabled. Marking as non-declined.");
+ toUndecline.add(engineName);
+ this._log.trace(engineName + " engine was enabled remotely.");
+ engine.enabled = true;
+ // Note that setting engine.enabled to true might not have worked for
+ // the password engine if a master-password is enabled. However, it's
+ // still OK that we added it to undeclined - the user *tried* to enable
+ // it remotely - so it still winds up as not being flagged as declined
+ // even though it's disabled remotely.
+ attemptedEnable = true;
+ }
+
+ // If either the engine was disabled locally or enabling the engine
+ // failed (see above re master-password) then wipe server data and
+ // disable it everywhere.
+ if (!engine.enabled) {
this._log.trace("Wiping data for " + engineName + " engine.");
engine.wipeServer();
delete meta.payload.engines[engineName];
- meta.changed = true;
- } else {
- // The engine was enabled remotely. Enable it locally.
- this._log.trace(engineName + " engine was enabled remotely.");
- engine.enabled = true;
+ meta.changed = true; // the new enabled state must propagate
+ // We also here mark the engine as declined, because the pref
+ // was explicitly changed to false - unless we tried, and failed,
+ // to enable it - in which case we leave the declined state alone.
+ if (!attemptedEnable) {
+ // This will be reflected in meta/global in the next stage.
+ this._log.trace("Engine " + engineName + " was disabled locally. Marking as declined.");
+ toDecline.add(engineName);
+ }
}
}
// Any remaining engines were either enabled locally or disabled remotely.
for each (let engineName in enabled) {
- let engine = this.service.engineManager.get(engineName);
+ let engine = engineManager.get(engineName);
if (Svc.Prefs.get("engineStatusChanged." + engine.prefName, false)) {
this._log.trace("The " + engineName + " engine was enabled locally.");
+ toUndecline.add(engineName);
} else {
this._log.trace("The " + engineName + " engine was disabled remotely.");
+
+ // Don't automatically mark it as declined!
engine.enabled = false;
}
}
+ engineManager.decline(toDecline);
+ engineManager.undecline(toUndecline);
+
Svc.Prefs.resetBranch("engineStatusChanged.");
this.service._ignorePrefObserver = false;
},
+ _updateEnabledEngines: function () {
+ let meta = this.service.recordManager.get(this.service.metaURL);
+ let numClients = this.service.scheduler.numClients;
+ let engineManager = this.service.engineManager;
+
+ this._updateEnabledFromMeta(meta, numClients, engineManager);
+ },
};
Object.freeze(EngineSynchronizer.prototype);
diff --git a/services/sync/modules/status.js b/services/sync/modules/status.js
index f17736a94..19dff9712 100644
--- a/services/sync/modules/status.js
+++ b/services/sync/modules/status.js
@@ -10,21 +10,39 @@ const Cr = Components.results;
const Cu = Components.utils;
Cu.import("resource://services-sync/constants.js");
-Cu.import("resource://services-common/log4moz.js");
+Cu.import("resource://gre/modules/Log.jsm");
Cu.import("resource://services-sync/identity.js");
+Cu.import("resource://services-sync/browserid_identity.js");
Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://services-common/async.js");
this.Status = {
- _log: Log4Moz.repository.getLogger("Sync.Status"),
- _authManager: new IdentityManager(),
+ _log: Log.repository.getLogger("Sync.Status"),
+ __authManager: null,
ready: false,
+ get _authManager() {
+ if (this.__authManager) {
+ return this.__authManager;
+ }
+ let service = Components.classes["@mozilla.org/weave/service;1"]
+ .getService(Components.interfaces.nsISupports)
+ .wrappedJSObject;
+ let idClass = service.fxAccountsEnabled ? BrowserIDManager : IdentityManager;
+ this.__authManager = new idClass();
+ // .initialize returns a promise, so we need to spin until it resolves.
+ let cb = Async.makeSpinningCallback();
+ this.__authManager.initialize().then(cb, cb);
+ cb.wait();
+ return this.__authManager;
+ },
+
get service() {
return this._service;
},
set service(code) {
- this._log.debug("Status.service: " + this._service + " => " + code);
+ this._log.debug("Status.service: " + (this._service || undefined) + " => " + code);
this._service = code;
},
@@ -57,6 +75,15 @@ this.Status = {
this.service = code == SYNC_SUCCEEDED ? STATUS_OK : SYNC_FAILED;
},
+ get eol() {
+ let modePref = PREFS_BRANCH + "errorhandler.alert.mode";
+ try {
+ return Services.prefs.getCharPref(modePref) == "hard-eol";
+ } catch (ex) {
+ return false;
+ }
+ },
+
get engines() {
return this._engines;
},
@@ -105,7 +132,7 @@ this.Status = {
} catch (ex) {
// Use default.
}
- this._log.level = Log4Moz.Level[logLevel];
+ this._log.level = Log.Level[logLevel];
this._log.info("Resetting Status.");
this.service = STATUS_OK;
diff --git a/services/sync/modules/userapi.js b/services/sync/modules/userapi.js
index d09a98d5d..ec77d63e2 100644
--- a/services/sync/modules/userapi.js
+++ b/services/sync/modules/userapi.js
@@ -10,7 +10,7 @@ this.EXPORTED_SYMBOLS = [
const {utils: Cu} = Components;
-Cu.import("resource://services-common/log4moz.js");
+Cu.import("resource://gre/modules/Log.jsm");
Cu.import("resource://services-common/rest.js");
Cu.import("resource://services-common/utils.js");
Cu.import("resource://services-sync/identity.js");
@@ -24,8 +24,8 @@ Cu.import("resource://services-sync/util.js");
* Instances are constructed with the base URI of the service.
*/
this.UserAPI10Client = function UserAPI10Client(baseURI) {
- this._log = Log4Moz.repository.getLogger("Sync.UserAPI");
- this._log.level = Log4Moz.Level[Svc.Prefs.get("log.logger.userapi")];
+ this._log = Log.repository.getLogger("Sync.UserAPI");
+ this._log.level = Log.Level[Svc.Prefs.get("log.logger.userapi")];
this.baseURI = baseURI;
}
@@ -165,7 +165,7 @@ UserAPI10Client.prototype = {
return;
}
- let error = new Error("Sync node retrieval failed.");
+ error = new Error("Sync node retrieval failed.");
switch (response.status) {
case 400:
error.denied = true;
@@ -214,7 +214,7 @@ UserAPI10Client.prototype = {
return;
}
- let error = new Error("Could not create user.");
+ error = new Error("Could not create user.");
error.body = response.body;
cb(error, null);
diff --git a/services/sync/modules/util.js b/services/sync/modules/util.js
index e0aae8486..67cc3f063 100644
--- a/services/sync/modules/util.js
+++ b/services/sync/modules/util.js
@@ -2,24 +2,29 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-this.EXPORTED_SYMBOLS = ["XPCOMUtils", "Services", "NetUtil", "PlacesUtils",
- "FileUtils", "Utils", "Async", "Svc", "Str"];
+this.EXPORTED_SYMBOLS = ["XPCOMUtils", "Services", "Utils", "Async", "Svc", "Str"];
const {classes: Cc, interfaces: Ci, results: Cr, utils: Cu} = Components;
-Cu.import("resource://services-common/log4moz.js");
+Cu.import("resource://gre/modules/Log.jsm");
Cu.import("resource://services-common/observers.js");
Cu.import("resource://services-common/stringbundle.js");
Cu.import("resource://services-common/utils.js");
Cu.import("resource://services-common/async.js", this);
Cu.import("resource://services-crypto/utils.js");
Cu.import("resource://services-sync/constants.js");
-Cu.import("resource://gre/modules/FileUtils.jsm", this);
-Cu.import("resource://gre/modules/NetUtil.jsm", this);
-Cu.import("resource://gre/modules/PlacesUtils.jsm", this);
Cu.import("resource://gre/modules/Preferences.jsm");
Cu.import("resource://gre/modules/Services.jsm", this);
Cu.import("resource://gre/modules/XPCOMUtils.jsm", this);
+Cu.import("resource://gre/modules/osfile.jsm", this);
+Cu.import("resource://gre/modules/Task.jsm", this);
+
+// FxAccountsCommon.js doesn't use a "namespace", so create one here.
+XPCOMUtils.defineLazyGetter(this, "FxAccountsCommon", function() {
+ let FxAccountsCommon = {};
+ Cu.import("resource://gre/modules/FxAccountsCommon.js", FxAccountsCommon);
+ return FxAccountsCommon;
+});
/*
* Utility functions
@@ -38,6 +43,7 @@ this.Utils = {
safeAtoB: CommonUtils.safeAtoB,
byteArrayToString: CommonUtils.byteArrayToString,
bytesAsHex: CommonUtils.bytesAsHex,
+ hexToBytes: CommonUtils.hexToBytes,
encodeBase32: CommonUtils.encodeBase32,
decodeBase32: CommonUtils.decodeBase32,
@@ -60,7 +66,7 @@ this.Utils = {
*
* @usage MyObj._catch = Utils.catch;
* MyObj.foo = function() { this._catch(func)(); }
- *
+ *
* Optionally pass a function which will be called if an
* exception occurs.
*/
@@ -101,7 +107,7 @@ this.Utils = {
}
};
},
-
+
isLockException: function isLockException(ex) {
return ex && ex.indexOf && ex.indexOf("Could not acquire lock.") == 0;
},
@@ -109,14 +115,14 @@ this.Utils = {
/**
* Wrap functions to notify when it starts and finishes executing or if it
* threw an error.
- *
+ *
* The message is a combination of a provided prefix, the local name, and
* the event. Possible events are: "start", "finish", "error". The subject
* is the function's return value on "finish" or the caught exception on
* "error". The data argument is the predefined data value.
- *
+ *
* Example:
- *
+ *
* @usage function MyObj(name) {
* this.name = name;
* this._notify = Utils.notify("obj:");
@@ -151,22 +157,6 @@ this.Utils = {
};
},
- runInTransaction: function(db, callback, thisObj) {
- let hasTransaction = false;
- try {
- db.beginTransaction();
- hasTransaction = true;
- } catch(e) { /* om nom nom exceptions */ }
-
- try {
- return callback.call(thisObj);
- } finally {
- if (hasTransaction) {
- db.commitTransaction();
- }
- }
- },
-
/**
* GUIDs are 9 random bytes encoded with base64url (RFC 4648).
* That makes them 12 characters long with 72 bits of entropy.
@@ -187,57 +177,34 @@ this.Utils = {
* @param obj
* Object to add properties to defer in its prototype
* @param defer
- * Hash property of obj to defer to (dot split each level)
+ * Property of obj to defer to
* @param prop
* Property name to defer (or an array of property names)
*/
deferGetSet: function Utils_deferGetSet(obj, defer, prop) {
if (Array.isArray(prop))
- return prop.map(function(prop) Utils.deferGetSet(obj, defer, prop));
-
- // Split the defer into each dot part for each level to dereference
- let parts = defer.split(".");
- let deref = function(base) Utils.deref(base, parts);
+ return prop.map(prop => Utils.deferGetSet(obj, defer, prop));
let prot = obj.prototype;
// Create a getter if it doesn't exist yet
if (!prot.__lookupGetter__(prop)) {
- // Yes, this should be a one-liner, but there are errors if it's not
- // broken out. *sigh*
- // Errors are these:
- // JavaScript strict warning: resource://services-sync/util.js, line 304: reference to undefined property deref(this)[prop]
- // JavaScript strict warning: resource://services-sync/util.js, line 304: reference to undefined property deref(this)[prop]
- let f = function() {
- let d = deref(this);
- if (!d)
- return undefined;
- let out = d[prop];
- return out;
- }
- prot.__defineGetter__(prop, f);
+ prot.__defineGetter__(prop, function () {
+ return this[defer][prop];
+ });
}
// Create a setter if it doesn't exist yet
- if (!prot.__lookupSetter__(prop))
- prot.__defineSetter__(prop, function(val) deref(this)[prop] = val);
+ if (!prot.__lookupSetter__(prop)) {
+ prot.__defineSetter__(prop, function (val) {
+ this[defer][prop] = val;
+ });
+ }
},
-
- /**
- * Dereference an array of properties starting from a base object
- *
- * @param base
- * Base object to start dereferencing
- * @param props
- * Array of properties to dereference (one for each level)
- */
- deref: function Utils_deref(base, props) props.reduce(function(curr, prop)
- curr[prop], base),
-
lazyStrings: function Weave_lazyStrings(name) {
let bundle = "chrome://weave/locale/services/" + name + ".properties";
- return function() new StringBundle(bundle);
+ return () => new StringBundle(bundle);
},
deepEquals: function eq(a, b) {
@@ -360,41 +327,28 @@ this.Utils = {
* Function to process json object as its first argument. If the file
* could not be loaded, the first argument will be undefined.
*/
- jsonLoad: function jsonLoad(filePath, that, callback) {
- let path = "weave/" + filePath + ".json";
+ jsonLoad: Task.async(function*(filePath, that, callback) {
+ let path = OS.Path.join(OS.Constants.Path.profileDir, "weave", filePath + ".json");
if (that._log) {
that._log.trace("Loading json from disk: " + filePath);
}
- let file = FileUtils.getFile("ProfD", path.split("/"), true);
- if (!file.exists()) {
- callback.call(that);
- return;
- }
+ let json;
- let channel = NetUtil.newChannel(file);
- channel.contentType = "application/json";
-
- NetUtil.asyncFetch(channel, function (is, result) {
- if (!Components.isSuccessCode(result)) {
- callback.call(that);
- return;
- }
- let string = NetUtil.readInputStreamToString(is, is.available());
- is.close();
- let json;
- try {
- json = JSON.parse(string);
- } catch (ex) {
- if (that._log) {
- that._log.debug("Failed to load json: " +
- CommonUtils.exceptionStr(ex));
- }
+ try {
+ json = yield CommonUtils.readJSON(path);
+ } catch (e if e instanceof OS.File.Error && e.becauseNoSuchFile) {
+ // Ignore non-existent files.
+ } catch (e) {
+ if (that._log) {
+ that._log.debug("Failed to load json: " +
+ CommonUtils.exceptionStr(e));
}
- callback.call(that, json);
- });
- },
+ }
+
+ callback.call(that, json);
+ }),
/**
* Save a json-able object to disk in the profile directory.
@@ -412,36 +366,30 @@ this.Utils = {
* constant on error or null if no error was encountered (and
* the file saved successfully).
*/
- jsonSave: function jsonSave(filePath, that, obj, callback) {
- let path = "weave/" + filePath + ".json";
- if (that._log) {
- that._log.trace("Saving json to disk: " + path);
- }
+ jsonSave: Task.async(function*(filePath, that, obj, callback) {
+ let path = OS.Path.join(OS.Constants.Path.profileDir, "weave",
+ ...(filePath + ".json").split("/"));
+ let dir = OS.Path.dirname(path);
+ let error = null;
- let file = FileUtils.getFile("ProfD", path.split("/"), true);
- let json = typeof obj == "function" ? obj.call(that) : obj;
- let out = JSON.stringify(json);
+ try {
+ yield OS.File.makeDir(dir, { from: OS.Constants.Path.profileDir });
- let fos = FileUtils.openSafeFileOutputStream(file);
- let is = this._utf8Converter.convertToInputStream(out);
- NetUtil.asyncCopy(is, fos, function (result) {
- if (typeof callback == "function") {
- let error = (result == Cr.NS_OK) ? null : result;
- callback.call(that, error);
+ if (that._log) {
+ that._log.trace("Saving json to disk: " + path);
}
- });
- },
- getIcon: function(iconUri, defaultIcon) {
- try {
- let iconURI = Utils.makeURI(iconUri);
- return PlacesUtils.favicons.getFaviconLinkForIcon(iconURI).spec;
+ let json = typeof obj == "function" ? obj.call(that) : obj;
+
+ yield CommonUtils.writeJSON(json, path);
+ } catch (e) {
+ error = e
}
- catch(ex) {}
- // Just give the provided default icon or the system's default
- return defaultIcon || PlacesUtils.favicons.defaultFavicon.spec;
- },
+ if (typeof callback == "function") {
+ callback.call(that, error);
+ }
+ }),
getErrorString: function Utils_getErrorString(error, args) {
try {
@@ -547,7 +495,7 @@ this.Utils = {
// Something else -- just return.
return pp;
},
-
+
normalizeAccount: function normalizeAccount(acc) {
return acc.trim();
},
@@ -559,7 +507,7 @@ this.Utils = {
arraySub: function arraySub(minuend, subtrahend) {
if (!minuend.length || !subtrahend.length)
return minuend;
- return minuend.filter(function(i) subtrahend.indexOf(i) == -1);
+ return minuend.filter(i => subtrahend.indexOf(i) == -1);
},
/**
@@ -577,6 +525,22 @@ this.Utils = {
return function innerBind() { return method.apply(object, arguments); };
},
+ /**
+ * Is there a master password configured, regardless of current lock state?
+ */
+ mpEnabled: function mpEnabled() {
+ let modules = Cc["@mozilla.org/security/pkcs11moduledb;1"]
+ .getService(Ci.nsIPKCS11ModuleDB);
+ let sdrSlot = modules.findSlotByName("");
+ let status = sdrSlot.status;
+ let slots = Ci.nsIPKCS11Slot;
+
+ return status != slots.SLOT_UNINITIALIZED && status != slots.SLOT_READY;
+ },
+
+ /**
+ * Is there a master password configured and currently locked?
+ */
mpLocked: function mpLocked() {
let modules = Cc["@mozilla.org/security/pkcs11moduledb;1"]
.getService(Ci.nsIPKCS11ModuleDB);
@@ -590,7 +554,7 @@ this.Utils = {
if (status == slots.SLOT_NOT_LOGGED_IN)
return true;
-
+
// something wacky happened, pretend MP is locked
return true;
},
@@ -609,7 +573,7 @@ this.Utils = {
} catch(e) {}
return false;
},
-
+
/**
* Return a value for a backoff interval. Maximum is eight hours, unless
* Status.backoffInterval is higher.
@@ -623,6 +587,92 @@ this.Utils = {
return Math.max(Math.min(backoffInterval, MAXIMUM_BACKOFF_INTERVAL),
statusInterval);
},
+
+ /**
+ * Return a set of hostnames (including the protocol) which may have
+ * credentials for sync itself stored in the login manager.
+ *
+ * In general, these hosts will not have their passwords synced, will be
+ * reset when we drop sync credentials, etc.
+ */
+ getSyncCredentialsHosts: function() {
+ let result = new Set(this.getSyncCredentialsHostsLegacy());
+ for (let host of this.getSyncCredentialsHostsFxA()) {
+ result.add(host);
+ }
+ return result;
+ },
+
+ /*
+ * Get the "legacy" identity hosts.
+ */
+ getSyncCredentialsHostsLegacy: function() {
+ // the legacy sync host
+ return new Set([PWDMGR_HOST]);
+ },
+
+ /*
+ * Get the FxA identity hosts.
+ */
+ getSyncCredentialsHostsFxA: function() {
+ // This is somewhat expensive and the result static, so we cache the result.
+ if (this._syncCredentialsHostsFxA) {
+ return this._syncCredentialsHostsFxA;
+ }
+ let result = new Set();
+ // the FxA host
+ result.add(FxAccountsCommon.FXA_PWDMGR_HOST);
+ //
+ // The FxA hosts - these almost certainly all have the same hostname, but
+ // better safe than sorry...
+ for (let prefName of ["identity.fxaccounts.remote.force_auth.uri",
+ "identity.fxaccounts.remote.signup.uri",
+ "identity.fxaccounts.remote.signin.uri",
+ "identity.fxaccounts.settings.uri"]) {
+ let prefVal;
+ try {
+ prefVal = Services.prefs.getCharPref(prefName);
+ } catch (_) {
+ continue;
+ }
+ let uri = Services.io.newURI(prefVal, null, null);
+ result.add(uri.prePath);
+ }
+ return this._syncCredentialsHostsFxA = result;
+ },
+
+ getDefaultDeviceName() {
+ // Generate a client name if we don't have a useful one yet
+ let env = Cc["@mozilla.org/process/environment;1"]
+ .getService(Ci.nsIEnvironment);
+ let user = env.get("USER") || env.get("USERNAME") ||
+ Svc.Prefs.get("account") || Svc.Prefs.get("username");
+ // A little hack for people using the the moz-build environment on Windows
+ // which sets USER to the literal "%USERNAME%" (yes, really)
+ if (user == "%USERNAME%" && env.get("USERNAME")) {
+ user = env.get("USERNAME");
+ }
+
+ let brand = new StringBundle("chrome://branding/locale/brand.properties");
+ let brandName = brand.get("brandShortName");
+
+ let appName;
+ try {
+ let syncStrings = new StringBundle("chrome://browser/locale/sync.properties");
+ appName = syncStrings.getFormattedString("sync.defaultAccountApplication", [brandName]);
+ } catch (ex) {}
+ appName = appName || brandName;
+
+ let system =
+ // 'device' is defined on unix systems
+ Cc["@mozilla.org/system-info;1"].getService(Ci.nsIPropertyBag2).get("device") ||
+ // hostname of the system, usually assigned by the user or admin
+ Cc["@mozilla.org/system-info;1"].getService(Ci.nsIPropertyBag2).get("host") ||
+ // fall back on ua info string
+ Cc["@mozilla.org/network/protocol;1?name=http"].getService(Ci.nsIHttpProtocolHandler).oscpu;
+
+ return Str.sync.get("client.name2", [user, appName, system]);
+ }
};
XPCOMUtils.defineLazyGetter(Utils, "_utf8Converter", function() {
diff --git a/services/sync/moz.build b/services/sync/moz.build
index df680a7fe..cedeb0529 100644
--- a/services/sync/moz.build
+++ b/services/sync/moz.build
@@ -5,4 +5,64 @@
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
DIRS += ['locales']
-TEST_DIRS += ['tests']
+
+XPCSHELL_TESTS_MANIFESTS += ['tests/unit/xpcshell.ini']
+
+EXTRA_COMPONENTS += [
+ 'Weave.js',
+]
+
+EXTRA_PP_COMPONENTS += [
+ 'SyncComponents.manifest',
+]
+
+EXTRA_JS_MODULES['services-sync'] += [
+ 'modules/addonsreconciler.js',
+ 'modules/addonutils.js',
+ 'modules/browserid_identity.js',
+ 'modules/engines.js',
+ 'modules/FxaMigrator.jsm',
+ 'modules/healthreport.jsm',
+ 'modules/identity.js',
+ 'modules/jpakeclient.js',
+ 'modules/keys.js',
+ 'modules/main.js',
+ 'modules/notifications.js',
+ 'modules/policies.js',
+ 'modules/record.js',
+ 'modules/resource.js',
+ 'modules/rest.js',
+ 'modules/service.js',
+ 'modules/status.js',
+ 'modules/userapi.js',
+ 'modules/util.js',
+]
+
+EXTRA_JS_MODULES['services-sync'].engines += [
+ 'modules/engines/addons.js',
+ 'modules/engines/bookmarks.js',
+ 'modules/engines/clients.js',
+ 'modules/engines/forms.js',
+ 'modules/engines/history.js',
+ 'modules/engines/passwords.js',
+ 'modules/engines/prefs.js',
+ 'modules/engines/tabs.js',
+]
+
+EXTRA_JS_MODULES['services-sync'].stages += [
+ 'modules/stages/cluster.js',
+ 'modules/stages/declined.js',
+ 'modules/stages/enginesync.js',
+]
+
+TESTING_JS_MODULES.services.sync += [
+ 'modules-testing/fakeservices.js',
+ 'modules-testing/fxa_utils.js',
+ 'modules-testing/rotaryengine.js',
+ 'modules-testing/utils.js',
+]
+
+JS_PREFERENCE_FILES += [
+ 'services-sync.js',
+]
+
diff --git a/services/sync/services-sync.js b/services/sync/services-sync.js
index 9f1797078..02b95b026 100644
--- a/services/sync/services-sync.js
+++ b/services/sync/services-sync.js
@@ -13,21 +13,28 @@ pref("services.sync.syncKeyHelpURL", "http://www.palemoon.org/sync/keyhelp.shtml
pref("services.sync.lastversion", "firstrun");
pref("services.sync.sendVersionInfo", true);
-pref("services.sync.scheduler.singleDeviceInterval", 86400); // 1 day
+pref("services.sync.scheduler.eolInterval", 604800); // 1 week
pref("services.sync.scheduler.idleInterval", 3600); // 1 hour
pref("services.sync.scheduler.activeInterval", 600); // 10 minutes
pref("services.sync.scheduler.immediateInterval", 90); // 1.5 minutes
pref("services.sync.scheduler.idleTime", 300); // 5 minutes
+pref("services.sync.scheduler.fxa.singleDeviceInterval", 3600); // 1 hour
+pref("services.sync.scheduler.sync11.singleDeviceInterval", 86400); // 1 day
+
pref("services.sync.errorhandler.networkFailureReportTimeout", 1209600); // 2 weeks
+// A "master" pref for Sync being enabled. Will be set to false if the sync
+// customization UI finds all our builtin engines disabled (and addons are
+// free to force this to true if they have their own engine)
+pref("services.sync.enabled", true);
+// Our engines.
pref("services.sync.engine.addons", false);
pref("services.sync.engine.bookmarks", true);
-pref("services.sync.engine.history", false);
+pref("services.sync.engine.history", true);
pref("services.sync.engine.passwords", true);
pref("services.sync.engine.prefs", true);
pref("services.sync.engine.tabs", true);
-pref("services.sync.engine.apps", false);
pref("services.sync.engine.tabs.filteredUrls", "^(about:.*|chrome://weave/.*|wyciwyg:.*|file:.*)$");
pref("services.sync.jpake.serverURL", "https://keyserver.palemoon.net/");
@@ -55,6 +62,7 @@ pref("services.sync.log.appender.file.logOnSuccess", false);
pref("services.sync.log.appender.file.maxErrorAge", 864000); // 10 days
pref("services.sync.log.rootLogger", "Debug");
pref("services.sync.log.logger.addonutils", "Debug");
+pref("services.sync.log.logger.declined", "Debug");
pref("services.sync.log.logger.service.main", "Debug");
pref("services.sync.log.logger.status", "Debug");
pref("services.sync.log.logger.authenticator", "Debug");
@@ -69,5 +77,11 @@ pref("services.sync.log.logger.engine.prefs", "Debug");
pref("services.sync.log.logger.engine.tabs", "Debug");
pref("services.sync.log.logger.engine.addons", "Debug");
pref("services.sync.log.logger.engine.apps", "Debug");
+pref("services.sync.log.logger.identity", "Debug");
pref("services.sync.log.logger.userapi", "Debug");
pref("services.sync.log.cryptoDebug", false);
+
+pref("services.sync.tokenServerURI", "https://token.services.mozilla.com/1.0/sync/1.5");
+
+pref("services.sync.fxa.termsURL", "https://accounts.firefox.com/legal/terms");
+pref("services.sync.fxa.privacyURL", "https://accounts.firefox.com/legal/privacy");
diff --git a/services/sync/tests/moz.build b/services/sync/tests/moz.build
deleted file mode 100644
index a1067b49f..000000000
--- a/services/sync/tests/moz.build
+++ /dev/null
@@ -1,9 +0,0 @@
-# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
-# vim: set filetype=python:
-# This Source Code Form is subject to the terms of the Mozilla Public
-# License, v. 2.0. If a copy of the MPL was not distributed with this
-# file, You can obtain one at http://mozilla.org/MPL/2.0/.
-
-MODULE = 'test_services_sync'
-
-XPCSHELL_TESTS_MANIFESTS += ['unit/xpcshell.ini']
diff --git a/services/sync/tests/tps/restartless-xpi.xml b/services/sync/tests/tps/addons/api/restartless-xpi@tests.mozilla.org.xml
index a6be2ecb4..9a5f6d52b 100644
--- a/services/sync/tests/tps/restartless-xpi.xml
+++ b/services/sync/tests/tps/addons/api/restartless-xpi@tests.mozilla.org.xml
@@ -12,11 +12,11 @@
<application_id>1</application_id>
<min_version>3.6</min_version>
<max_version>*</max_version>
- <appID>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</appID>
+ <appID>{8de7fcbb-c55c-4fbe-bfc5-fc555c87dbc4}</appID>
</application></compatible_applications>
<all_compatible_os><os>ALL</os></all_compatible_os>
- <install os="ALL" size="485">http://127.0.0.1:4567/restartless.xpi</install>
+ <install os="ALL" size="485">http://127.0.0.1:4567/addons/restartless.xpi</install>
<created epoch="1252903662">
2009-09-14T04:47:42Z
</created>
diff --git a/services/sync/tests/tps/unsigned-xpi.xml b/services/sync/tests/tps/addons/api/unsigned-xpi@tests.mozilla.org.xml
index 614927e77..d7a577b31 100644
--- a/services/sync/tests/tps/unsigned-xpi.xml
+++ b/services/sync/tests/tps/addons/api/unsigned-xpi@tests.mozilla.org.xml
@@ -12,11 +12,11 @@
<application_id>1</application_id>
<min_version>3.6</min_version>
<max_version>*</max_version>
- <appID>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</appID>
+ <appID>{8de7fcbb-c55c-4fbe-bfc5-fc555c87dbc4}</appID>
</application></compatible_applications>
<all_compatible_os><os>ALL</os></all_compatible_os>
- <install os="ALL" size="452">http://127.0.0.1:4567/unsigned.xpi</install>
+ <install os="ALL" size="452">http://127.0.0.1:4567/addons/unsigned.xpi</install>
<created epoch="1252903662">
2009-09-14T04:47:42Z
</created>
diff --git a/services/sync/tests/tps/restartless.xpi b/services/sync/tests/tps/addons/restartless.xpi
index 973bc00cb..973bc00cb 100644
--- a/services/sync/tests/tps/restartless.xpi
+++ b/services/sync/tests/tps/addons/restartless.xpi
Binary files differ
diff --git a/services/sync/tests/tps/unsigned.xpi b/services/sync/tests/tps/addons/unsigned.xpi
index 51b00475a..51b00475a 100644
--- a/services/sync/tests/tps/unsigned.xpi
+++ b/services/sync/tests/tps/addons/unsigned.xpi
Binary files differ
diff --git a/services/sync/tests/tps/mozmill_sanity.js b/services/sync/tests/tps/mozmill_sanity.js
index 99addd1b2..fbaed8f25 100644
--- a/services/sync/tests/tps/mozmill_sanity.js
+++ b/services/sync/tests/tps/mozmill_sanity.js
@@ -2,10 +2,10 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-Components.utils.import('resource://tps/sync.jsm');
+Components.utils.import('resource://tps/tps.jsm');
var setupModule = function(module) {
- controller = mozmill.getBrowserController();
+ module.controller = mozmill.getBrowserController();
assert.ok(true, "SetupModule passes");
}
@@ -16,8 +16,9 @@ var setupTest = function(module) {
var testTestStep = function() {
assert.ok(true, "test Passes");
controller.open("http://www.mozilla.org");
- TPS.SetupSyncAccount();
- assert.equal(TPS.Sync(SYNC_WIPE_SERVER), 0, "sync succeeded");
+
+ TPS.Login();
+ TPS.Sync(ACTIONS.ACTION_SYNC_WIPE_CLIENT);
}
var teardownTest = function () {
diff --git a/services/sync/tests/tps/mozmill_sanity2.js b/services/sync/tests/tps/mozmill_sanity2.js
index 027e87f1b..f0fd0e3d5 100644
--- a/services/sync/tests/tps/mozmill_sanity2.js
+++ b/services/sync/tests/tps/mozmill_sanity2.js
@@ -1,7 +1,6 @@
-/* Any copyright is dedicated to the Public Domain.
- http://creativecommons.org/publicdomain/zero/1.0/ */
-
-var jum = {}; Components.utils.import('resource://mozmill/modules/jum.js', jum);
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
var setupModule = function(module) {
module.controller = mozmill.getBrowserController();
@@ -11,43 +10,6 @@ var testGetNode = function() {
controller.open("about:support");
controller.waitForPageLoad();
- var appbox = new elementslib.ID(controller.tabs.activeTab, "application-box");
- jum.assert(appbox.getNode().innerHTML == 'Firefox', 'correct app name');
-};
-
-const NAV_BAR = '/id("main-window")/id("tab-view-deck")/{"flex":"1"}' +
- '/id("navigator-toolbox")/id("nav-bar")';
-const SEARCH_BAR = NAV_BAR + '/id("search-container")/id("searchbar")';
-const SEARCH_TEXTBOX = SEARCH_BAR + '/anon({"anonid":"searchbar-textbox"})';
-const SEARCH_DROPDOWN = SEARCH_TEXTBOX + '/[0]/anon({"anonid":"searchbar-engine-button"})';
-const SEARCH_POPUP = SEARCH_DROPDOWN + '/anon({"anonid":"searchbar-popup"})';
-const SEARCH_INPUT = SEARCH_TEXTBOX + '/anon({"class":"autocomplete-textbox-container"})' +
- '/anon({"anonid":"textbox-input-box"})' +
- '/anon({"anonid":"input"})';
-const SEARCH_CONTEXT = SEARCH_TEXTBOX + '/anon({"anonid":"textbox-input-box"})' +
- '/anon({"anonid":"input-box-contextmenu"})';
-const SEARCH_GO_BUTTON = SEARCH_TEXTBOX + '/anon({"class":"search-go-container"})' +
- '/anon({"class":"search-go-button"})';
-const SEARCH_AUTOCOMPLETE = '/id("main-window")/id("mainPopupSet")/id("PopupAutoComplete")';
-
-var testLookupExpressions = function() {
- var item;
- item = new elementslib.Lookup(controller.window.document, NAV_BAR);
- controller.click(item);
- item = new elementslib.Lookup(controller.window.document, SEARCH_BAR);
- controller.click(item);
- item = new elementslib.Lookup(controller.window.document, SEARCH_TEXTBOX);
- controller.click(item);
- item = new elementslib.Lookup(controller.window.document, SEARCH_DROPDOWN);
- controller.click(item);
- item = new elementslib.Lookup(controller.window.document, SEARCH_POPUP);
- controller.click(item);
- item = new elementslib.Lookup(controller.window.document, SEARCH_INPUT);
- controller.click(item);
- item = new elementslib.Lookup(controller.window.document, SEARCH_CONTEXT);
- controller.click(item);
- item = new elementslib.Lookup(controller.window.document, SEARCH_GO_BUTTON);
- controller.click(item);
- item = new elementslib.Lookup(controller.window.document, SEARCH_AUTOCOMPLETE);
- controller.click(item);
+ var appbox = findElement.ID(controller.tabs.activeTab, "application-box");
+ assert.waitFor(() => appbox.getNode().textContent == 'Firefox', 'correct app name');
};
diff --git a/services/sync/tests/tps/test_addon_reconciling.js b/services/sync/tests/tps/test_addon_reconciling.js
index ab70d97fb..14dda8ade 100644
--- a/services/sync/tests/tps/test_addon_reconciling.js
+++ b/services/sync/tests/tps/test_addon_reconciling.js
@@ -19,10 +19,13 @@ const id = "restartless-xpi@tests.mozilla.org";
// Install the add-on in 2 profiles.
Phase("phase01", [
+ [Addons.verifyNot, [id]],
[Addons.install, [id]],
+ [Addons.verify, [id], STATE_ENABLED],
[Sync]
]);
Phase("phase02", [
+ [Addons.verifyNot, [id]],
[Sync],
[Addons.verify, [id], STATE_ENABLED]
]);
@@ -33,7 +36,9 @@ Phase("phase03", [
[Addons.setEnabled, [id], STATE_DISABLED],
]);
Phase("phase04", [
+ [EnsureTracking],
[Addons.uninstall, [id]],
+ [Sync]
]);
// When we sync, the uninstall should take precedence because it was newer.
diff --git a/services/sync/tests/tps/test_tabs.js b/services/sync/tests/tps/test_tabs.js
index 41cf82001..03f277709 100644
--- a/services/sync/tests/tps/test_tabs.js
+++ b/services/sync/tests/tps/test_tabs.js
@@ -17,8 +17,8 @@ var phases = { "phase1": "profile1",
*/
var tabs1 = [
- { uri: "http://hg.mozilla.org/automation/crossweave/raw-file/2d9aca9585b6/pages/page1.html",
- title: "Crossweave Test Page 1",
+ { uri: "http://mozqa.com/data/firefox/layout/mozilla.html",
+ title: "Mozilla",
profile: "profile1"
},
{ uri: "data:text/html,<html><head><title>Hello</title></head><body>Hello</body></html>",
@@ -28,8 +28,8 @@ var tabs1 = [
];
var tabs2 = [
- { uri: "http://hg.mozilla.org/automation/crossweave/raw-file/2d9aca9585b6/pages/page3.html",
- title: "Crossweave Test Page 3",
+ { uri: "http://mozqa.com/data/firefox/layout/mozilla_community.html",
+ title: "Mozilla Community",
profile: "profile2"
},
{ uri: "data:text/html,<html><head><title>Bye</title></head><body>Bye</body></html>",
diff --git a/services/sync/tests/unit/head_helpers.js b/services/sync/tests/unit/head_helpers.js
index 12a18c11e..04534dc8e 100644
--- a/services/sync/tests/unit/head_helpers.js
+++ b/services/sync/tests/unit/head_helpers.js
@@ -2,6 +2,8 @@
http://creativecommons.org/publicdomain/zero/1.0/ */
Cu.import("resource://services-common/async.js");
+Cu.import("resource://testing-common/services/common/utils.js");
+Cu.import("resource://testing-common/PlacesTestUtils.jsm");
let provider = {
getFile: function(prop, persistent) {
@@ -124,3 +126,83 @@ function generateNewKeys(collectionKeys, collections=null) {
collectionKeys.setContents(wbo.cleartext, modified);
}
+// Helpers for testing open tabs.
+// These reflect part of the internal structure of TabEngine,
+// and stub part of Service.wm.
+
+function mockShouldSkipWindow (win) {
+ return win.closed ||
+ win.mockIsPrivate;
+}
+
+function mockGetTabState (tab) {
+ return tab;
+}
+
+function mockGetWindowEnumerator(url, numWindows, numTabs, indexes, moreURLs) {
+ let elements = [];
+
+ function url2entry(url) {
+ return {
+ url: ((typeof url == "function") ? url() : url),
+ title: "title"
+ };
+ }
+
+ for (let w = 0; w < numWindows; ++w) {
+ let tabs = [];
+ let win = {
+ closed: false,
+ mockIsPrivate: false,
+ gBrowser: {
+ tabs: tabs,
+ },
+ };
+ elements.push(win);
+
+ for (let t = 0; t < numTabs; ++t) {
+ tabs.push(TestingUtils.deepCopy({
+ index: indexes ? indexes() : 1,
+ entries: (moreURLs ? [url].concat(moreURLs()) : [url]).map(url2entry),
+ attributes: {
+ image: "image"
+ },
+ lastAccessed: 1499
+ }));
+ }
+ }
+
+ // Always include a closed window and a private window.
+ elements.push({
+ closed: true,
+ mockIsPrivate: false,
+ gBrowser: {
+ tabs: [],
+ },
+ });
+
+ elements.push({
+ closed: false,
+ mockIsPrivate: true,
+ gBrowser: {
+ tabs: [],
+ },
+ });
+
+ return {
+ hasMoreElements: function () {
+ return elements.length;
+ },
+ getNext: function () {
+ return elements.shift();
+ },
+ };
+}
+
+// Helper that allows checking array equality.
+function do_check_array_eq(a1, a2) {
+ do_check_eq(a1.length, a2.length);
+ for (let i = 0; i < a1.length; ++i) {
+ do_check_eq(a1[i], a2[i]);
+ }
+}
diff --git a/services/sync/tests/unit/head_http_server.js b/services/sync/tests/unit/head_http_server.js
index 92a83b3bd..c917c4988 100644
--- a/services/sync/tests/unit/head_http_server.js
+++ b/services/sync/tests/unit/head_http_server.js
@@ -1,7 +1,7 @@
const Cm = Components.manager;
// Shared logging for all HTTP server functions.
-Cu.import("resource://services-common/log4moz.js");
+Cu.import("resource://gre/modules/Log.jsm");
const SYNC_HTTP_LOGGER = "Sync.Test.Server";
const SYNC_API_VERSION = "1.1";
@@ -163,7 +163,7 @@ function ServerCollection(wbos, acceptNew, timestamp) {
* has a modified time.
*/
this.timestamp = timestamp || new_timestamp();
- this._log = Log4Moz.repository.getLogger(SYNC_HTTP_LOGGER);
+ this._log = Log.repository.getLogger(SYNC_HTTP_LOGGER);
}
ServerCollection.prototype = {
@@ -514,8 +514,11 @@ let SyncServerCallback = {
*
* Allows the test to inspect the request. Hooks should be careful not to
* modify or change state of the request or they may impact future processing.
+ * The response is also passed so the callback can set headers etc - but care
+ * must be taken to not screw with the response body or headers that may
+ * conflict with normal operation of this server.
*/
- onRequest: function onRequest(request) {},
+ onRequest: function onRequest(request, response) {},
};
/**
@@ -527,7 +530,7 @@ function SyncServer(callback) {
this.server = new HttpServer();
this.started = false;
this.users = {};
- this._log = Log4Moz.repository.getLogger(SYNC_HTTP_LOGGER);
+ this._log = Log.repository.getLogger(SYNC_HTTP_LOGGER);
// Install our own default handler. This allows us to mess around with the
// whole URL space.
@@ -535,7 +538,6 @@ function SyncServer(callback) {
handler._handleDefault = this.handleDefault.bind(this, handler);
}
SyncServer.prototype = {
- port: 8080,
server: null, // HttpServer.
users: null, // Map of username => {collections, password}.
@@ -544,7 +546,7 @@ SyncServer.prototype = {
*
* @param port
* The numeric port on which to start. A falsy value implies the
- * default (8080).
+ * default, a randomly chosen port.
* @param cb
* A callback function (of no arguments) which is invoked after
* startup.
@@ -554,23 +556,25 @@ SyncServer.prototype = {
this._log.warn("Warning: server already started on " + this.port);
return;
}
- if (port) {
- this.port = port;
- }
try {
- this.server.start(this.port);
+ this.server.start(port);
+ let i = this.server.identity;
+ this.port = i.primaryPort;
+ this.baseURI = i.primaryScheme + "://" + i.primaryHost + ":" +
+ i.primaryPort + "/";
this.started = true;
if (cb) {
cb();
}
} catch (ex) {
_("==========================================");
- _("Got exception starting Sync HTTP server on port " + this.port);
+ _("Got exception starting Sync HTTP server.");
_("Error: " + Utils.exceptionStr(ex));
- _("Is there a process already listening on port " + this.port + "?");
+ _("Is there a process already listening on port " + port + "?");
_("==========================================");
do_throw(ex);
}
+
},
/**
@@ -795,7 +799,7 @@ SyncServer.prototype = {
this._log.debug("SyncServer: Handling request: " + req.method + " " + req.path);
if (this.callback.onRequest) {
- this.callback.onRequest(req);
+ this.callback.onRequest(req, resp);
}
let parts = this.pathRE.exec(req.path);
@@ -805,7 +809,12 @@ SyncServer.prototype = {
}
let [all, version, username, first, rest] = parts;
- if (version != SYNC_API_VERSION) {
+ // Doing a float compare of the version allows for us to pretend there was
+ // a node-reassignment - eg, we could re-assign from "1.1/user/" to
+ // "1.10/user" - this server will then still accept requests with the new
+ // URL while any code in sync itself which compares URLs will see a
+ // different URL.
+ if (parseFloat(version) != parseFloat(SYNC_API_VERSION)) {
this._log.debug("SyncServer: Unknown version.");
throw HTTP_404;
}
@@ -851,7 +860,7 @@ SyncServer.prototype = {
// TODO: verify if this is spec-compliant.
if (req.method != "DELETE") {
respond(405, "Method Not Allowed", "[]", {"Allow": "DELETE"});
- return;
+ return undefined;
}
// Delete all collections and track the timestamp for the response.
@@ -859,7 +868,7 @@ SyncServer.prototype = {
// Return timestamp and OK for deletion.
respond(200, "OK", JSON.stringify(timestamp));
- return;
+ return undefined;
}
let match = this.storageRE.exec(rest);
@@ -874,11 +883,11 @@ SyncServer.prototype = {
if (!coll) {
if (wboID) {
respond(404, "Not found", "Not found");
- return;
+ return undefined;
}
// *cries inside*: Bug 687299.
respond(200, "OK", "[]");
- return;
+ return undefined;
}
if (!wboID) {
return coll.collectionHandler(req, resp);
@@ -886,7 +895,7 @@ SyncServer.prototype = {
let wbo = coll.wbo(wboID);
if (!wbo) {
respond(404, "Not found", "Not found");
- return;
+ return undefined;
}
return wbo.handler()(req, resp);
@@ -894,7 +903,7 @@ SyncServer.prototype = {
case "DELETE":
if (!coll) {
respond(200, "OK", "{}");
- return;
+ return undefined;
}
if (wboID) {
let wbo = coll.wbo(wboID);
@@ -903,7 +912,7 @@ SyncServer.prototype = {
this.callback.onItemDeleted(username, collection, wboID);
}
respond(200, "OK", "{}");
- return;
+ return undefined;
}
coll.collectionHandler(req, resp);
@@ -934,7 +943,7 @@ SyncServer.prototype = {
for (let i = 0; i < deleted.length; ++i) {
this.callback.onItemDeleted(username, collection, deleted[i]);
}
- return;
+ return undefined;
case "POST":
case "PUT":
if (!coll) {
diff --git a/services/sync/tests/unit/test_addons_engine.js b/services/sync/tests/unit/test_addons_engine.js
index 4646e8215..ca2e4bd96 100644
--- a/services/sync/tests/unit/test_addons_engine.js
+++ b/services/sync/tests/unit/test_addons_engine.js
@@ -111,7 +111,8 @@ add_test(function test_get_changed_ids() {
do_check_eq("object", typeof(changes));
do_check_eq(1, Object.keys(changes).length);
do_check_true(addon.syncGUID in changes);
- do_check_true(changes[addon.syncGUID] > changeTime);
+ _("Change time: " + changeTime + ", addon change: " + changes[addon.syncGUID]);
+ do_check_true(changes[addon.syncGUID] >= changeTime);
let oldTime = changes[addon.syncGUID];
let guid2 = addon.syncGUID;
@@ -158,7 +159,9 @@ add_test(function test_disabled_install_semantics() {
const PASSPHRASE = "abcdeabcdeabcdeabcdeabcdea";
const ADDON_ID = "addon1@tests.mozilla.org";
- new SyncTestingInfrastructure(USER, PASSWORD, PASSPHRASE);
+ let server = new SyncServer();
+ server.start();
+ new SyncTestingInfrastructure(server.server, USER, PASSWORD, PASSPHRASE);
generateNewKeys(Service.collectionKeys);
@@ -169,10 +172,8 @@ add_test(function test_disabled_install_semantics() {
addons: {}
};
- let server = new SyncServer();
server.registerUser(USER, "password");
server.createContents(USER, contents);
- server.start();
let amoServer = new HttpServer();
amoServer.registerFile("/search/guid:addon1%40tests.mozilla.org",
@@ -239,15 +240,13 @@ add_test(function cleanup() {
function run_test() {
initTestLogging("Trace");
- Log4Moz.repository.getLogger("Sync.Engine.Addons").level =
- Log4Moz.Level.Trace;
- Log4Moz.repository.getLogger("Sync.Store.Addons").level = Log4Moz.Level.Trace;
- Log4Moz.repository.getLogger("Sync.Tracker.Addons").level =
- Log4Moz.Level.Trace;
- Log4Moz.repository.getLogger("Sync.AddonsRepository").level =
- Log4Moz.Level.Trace;
-
- new SyncTestingInfrastructure();
+ Log.repository.getLogger("Sync.Engine.Addons").level =
+ Log.Level.Trace;
+ Log.repository.getLogger("Sync.Store.Addons").level = Log.Level.Trace;
+ Log.repository.getLogger("Sync.Tracker.Addons").level =
+ Log.Level.Trace;
+ Log.repository.getLogger("Sync.AddonsRepository").level =
+ Log.Level.Trace;
reconciler.startListening();
diff --git a/services/sync/tests/unit/test_addons_reconciler.js b/services/sync/tests/unit/test_addons_reconciler.js
index be99ab276..8cfa37d78 100644
--- a/services/sync/tests/unit/test_addons_reconciler.js
+++ b/services/sync/tests/unit/test_addons_reconciler.js
@@ -14,9 +14,9 @@ startupManager();
function run_test() {
initTestLogging("Trace");
- Log4Moz.repository.getLogger("Sync.AddonsReconciler").level = Log4Moz.Level.Trace;
- Log4Moz.repository.getLogger("Sync.AddonsReconciler").level =
- Log4Moz.Level.Trace;
+ Log.repository.getLogger("Sync.AddonsReconciler").level = Log.Level.Trace;
+ Log.repository.getLogger("Sync.AddonsReconciler").level =
+ Log.Level.Trace;
Svc.Prefs.set("engine.addons", true);
Service.engineManager.register(AddonsEngine);
@@ -179,7 +179,7 @@ add_test(function test_prune_changes_before_date() {
do_check_eq(2, reconciler._changes.length);
_("Ensure pruning a single item works.");
- let threshold = new Date(young.getTime() - 1000);
+ threshold = new Date(young.getTime() - 1000);
reconciler.pruneChangesBeforeDate(threshold);
do_check_eq(1, reconciler._changes.length);
do_check_neq(undefined, reconciler._changes[0]);
diff --git a/services/sync/tests/unit/test_addons_store.js b/services/sync/tests/unit/test_addons_store.js
index 5d7f6c19a..b21f6afe1 100644
--- a/services/sync/tests/unit/test_addons_store.js
+++ b/services/sync/tests/unit/test_addons_store.js
@@ -67,9 +67,9 @@ function createAndStartHTTPServer(port) {
function run_test() {
initTestLogging("Trace");
- Log4Moz.repository.getLogger("Sync.Engine.Addons").level = Log4Moz.Level.Trace;
- Log4Moz.repository.getLogger("Sync.AddonsRepository").level =
- Log4Moz.Level.Trace;
+ Log.repository.getLogger("Sync.Engine.Addons").level = Log.Level.Trace;
+ Log.repository.getLogger("Sync.AddonsRepository").level =
+ Log.Level.Trace;
reconciler.startListening();
diff --git a/services/sync/tests/unit/test_addons_tracker.js b/services/sync/tests/unit/test_addons_tracker.js
index 65950e355..690a57d03 100644
--- a/services/sync/tests/unit/test_addons_tracker.js
+++ b/services/sync/tests/unit/test_addons_tracker.js
@@ -27,7 +27,7 @@ const addon1ID = "addon1@tests.mozilla.org";
function cleanup_and_advance() {
Svc.Obs.notify("weave:engine:stop-tracking");
- tracker.observe(null, "weave:engine:stop-tracking");
+ tracker.stopTracking();
tracker.resetScore();
tracker.clearChangedIDs();
@@ -43,9 +43,9 @@ function cleanup_and_advance() {
function run_test() {
initTestLogging("Trace");
- Log4Moz.repository.getLogger("Sync.Engine.Addons").level = Log4Moz.Level.Trace;
- Log4Moz.repository.getLogger("Sync.AddonsReconciler").level =
- Log4Moz.Level.Trace;
+ Log.repository.getLogger("Sync.Engine.Addons").level = Log.Level.Trace;
+ Log.repository.getLogger("Sync.AddonsReconciler").level =
+ Log.Level.Trace;
cleanup_and_advance();
}
diff --git a/services/sync/tests/unit/test_block_sync.js b/services/sync/tests/unit/test_block_sync.js
new file mode 100644
index 000000000..f83b7b740
--- /dev/null
+++ b/services/sync/tests/unit/test_block_sync.js
@@ -0,0 +1,37 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+Cu.import("resource://services-sync/main.js");
+Cu.import("resource://services-sync/util.js");
+
+// Simple test for block/unblock.
+add_task(function *() {
+ Assert.ok(!Weave.Service.scheduler.isBlocked, "sync is not blocked.")
+ Assert.ok(!Svc.Prefs.has("scheduler.blocked-until"), "have no blocked pref");
+ Weave.Service.scheduler.blockSync();
+
+ Assert.ok(Weave.Service.scheduler.isBlocked, "sync is blocked.")
+ Assert.ok(Svc.Prefs.has("scheduler.blocked-until"), "have the blocked pref");
+
+ Weave.Service.scheduler.unblockSync();
+ Assert.ok(!Weave.Service.scheduler.isBlocked, "sync is not blocked.")
+ Assert.ok(!Svc.Prefs.has("scheduler.blocked-until"), "have no blocked pref");
+
+ // now check the "until" functionality.
+ let until = Date.now() + 1000;
+ Weave.Service.scheduler.blockSync(until);
+ Assert.ok(Weave.Service.scheduler.isBlocked, "sync is blocked.")
+ Assert.ok(Svc.Prefs.has("scheduler.blocked-until"), "have the blocked pref");
+
+ // wait for 'until' to pass.
+ yield new Promise((resolve, reject) => {
+ CommonUtils.namedTimer(resolve, 1000, {}, "timer");
+ });
+
+ // should have automagically unblocked and removed the pref.
+ Assert.ok(!Weave.Service.scheduler.isBlocked, "sync is not blocked.")
+ Assert.ok(!Svc.Prefs.has("scheduler.blocked-until"), "have no blocked pref");
+});
+
+function run_test() {
+ run_next_test();
+}
diff --git a/services/sync/tests/unit/test_bookmark_engine.js b/services/sync/tests/unit/test_bookmark_engine.js
index fda9260e0..bd4c740cb 100644
--- a/services/sync/tests/unit/test_bookmark_engine.js
+++ b/services/sync/tests/unit/test_bookmark_engine.js
@@ -4,7 +4,7 @@
Cu.import("resource://gre/modules/PlacesUtils.jsm");
Cu.import("resource://gre/modules/BookmarkJSONUtils.jsm");
Cu.import("resource://services-common/async.js");
-Cu.import("resource://services-common/log4moz.js");
+Cu.import("resource://gre/modules/Log.jsm");
Cu.import("resource://services-sync/engines.js");
Cu.import("resource://services-sync/engines/bookmarks.js");
Cu.import("resource://services-sync/service.js");
@@ -13,10 +13,11 @@ Cu.import("resource://testing-common/services/sync/utils.js");
Cu.import("resource://gre/modules/Promise.jsm");
Service.engineManager.register(BookmarksEngine);
-var syncTesting = new SyncTestingInfrastructure();
add_test(function bad_record_allIDs() {
- let syncTesting = new SyncTestingInfrastructure();
+ let server = new SyncServer();
+ server.start();
+ let syncTesting = new SyncTestingInfrastructure(server.server);
_("Ensure that bad Places queries don't cause an error in getAllIDs.");
let engine = new BookmarksEngine(Service);
@@ -43,11 +44,13 @@ add_test(function bad_record_allIDs() {
_("Clean up.");
PlacesUtils.bookmarks.removeItem(badRecordID);
- run_next_test();
+ server.stop(run_next_test);
});
add_test(function test_ID_caching() {
- let syncTesting = new SyncTestingInfrastructure();
+ let server = new SyncServer();
+ server.start();
+ let syncTesting = new SyncTestingInfrastructure(server.server);
_("Ensure that Places IDs are not cached.");
let engine = new BookmarksEngine(Service);
@@ -83,7 +86,7 @@ add_test(function test_ID_caching() {
do_check_eq(newMobileID, store.idForGUID("mobile", false));
do_check_eq(store.GUIDForId(mobileID), "abcdefghijkl");
- run_next_test();
+ server.stop(run_next_test);
});
function serverForFoo(engine) {
@@ -96,11 +99,11 @@ function serverForFoo(engine) {
add_test(function test_processIncoming_error_orderChildren() {
_("Ensure that _orderChildren() is called even when _processIncoming() throws an error.");
- new SyncTestingInfrastructure();
let engine = new BookmarksEngine(Service);
let store = engine._store;
let server = serverForFoo(engine);
+ new SyncTestingInfrastructure(server.server);
let collection = server.user("foo").collection("bookmarks");
@@ -166,11 +169,10 @@ add_test(function test_processIncoming_error_orderChildren() {
add_task(function test_restorePromptsReupload() {
_("Ensure that restoring from a backup will reupload all records.");
- new SyncTestingInfrastructure();
-
let engine = new BookmarksEngine(Service);
let store = engine._store;
let server = serverForFoo(engine);
+ new SyncTestingInfrastructure(server.server);
let collection = server.user("foo").collection("bookmarks");
@@ -333,11 +335,10 @@ add_test(function test_mismatched_types() {
"parentid": "toolbar"
};
- new SyncTestingInfrastructure();
-
let engine = new BookmarksEngine(Service);
let store = engine._store;
let server = serverForFoo(engine);
+ new SyncTestingInfrastructure(server.server);
_("GUID: " + store.GUIDForId(6, true));
@@ -376,14 +377,12 @@ add_test(function test_mismatched_types() {
add_test(function test_bookmark_guidMap_fail() {
_("Ensure that failures building the GUID map cause early death.");
- new SyncTestingInfrastructure();
-
let engine = new BookmarksEngine(Service);
let store = engine._store;
- let store = engine._store;
let server = serverForFoo(engine);
let coll = server.user("foo").collection("bookmarks");
+ new SyncTestingInfrastructure(server.server);
// Add one item to the server.
let itemID = PlacesUtils.bookmarks.createFolder(
@@ -475,10 +474,9 @@ add_test(function test_bookmark_tag_but_no_uri() {
add_test(function test_misreconciled_root() {
_("Ensure that we don't reconcile an arbitrary record with a root.");
- new SyncTestingInfrastructure();
-
let engine = new BookmarksEngine(Service);
let store = engine._store;
+ let server = serverForFoo(engine);
// Log real hard for this test.
store._log.trace = store._log.debug;
@@ -534,7 +532,7 @@ add_test(function test_misreconciled_root() {
do_check_eq(parentGUIDBefore, parentGUIDAfter);
do_check_eq(parentIDBefore, parentIDAfter);
- run_next_test();
+ server.stop(run_next_test);
});
function run_test() {
diff --git a/services/sync/tests/unit/test_bookmark_legacy_microsummaries_support.js b/services/sync/tests/unit/test_bookmark_legacy_microsummaries_support.js
index b0821e3d5..a7e3a4647 100644
--- a/services/sync/tests/unit/test_bookmark_legacy_microsummaries_support.js
+++ b/services/sync/tests/unit/test_bookmark_legacy_microsummaries_support.js
@@ -6,7 +6,7 @@ Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/NetUtil.jsm");
Cu.import("resource://gre/modules/PlacesUtils.jsm");
-Cu.import("resource://services-common/log4moz.js");
+Cu.import("resource://gre/modules/Log.jsm");
Cu.import("resource://services-sync/engines.js");
Cu.import("resource://services-sync/engines/bookmarks.js");
Cu.import("resource://services-sync/record.js");
@@ -45,7 +45,7 @@ function run_test() {
store.wipe();
initTestLogging("Trace");
- Log4Moz.repository.getLogger("Sync.Engine.Bookmarks").level = Log4Moz.Level.Trace;
+ Log.repository.getLogger("Sync.Engine.Bookmarks").level = Log.Level.Trace;
_("Create a microsummarized bookmark.");
let id = newMicrosummary(TEST_URL, TEST_TITLE);
diff --git a/services/sync/tests/unit/test_bookmark_livemarks.js b/services/sync/tests/unit/test_bookmark_livemarks.js
index e0347708d..d7cda091b 100644
--- a/services/sync/tests/unit/test_bookmark_livemarks.js
+++ b/services/sync/tests/unit/test_bookmark_livemarks.js
@@ -1,14 +1,14 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
-Cu.import("resource://services-common/log4moz.js");
+Cu.import("resource://gre/modules/Log.jsm");
Cu.import("resource://services-sync/record.js");
Cu.import("resource://services-sync/engines.js");
Cu.import("resource://services-sync/engines/bookmarks.js");
Cu.import("resource://services-sync/util.js");
Cu.import("resource://services-sync/service.js");
Cu.import("resource://gre/modules/PlacesUtils.jsm");
-Cu.import("resource://testing-common/services-common/utils.js");
+Cu.import("resource://testing-common/services/common/utils.js");
const DESCRIPTION_ANNO = "bookmarkProperties/description";
@@ -70,8 +70,8 @@ function makeLivemark(p, mintGUID) {
function run_test() {
initTestLogging("Trace");
- Log4Moz.repository.getLogger("Sync.Engine.Bookmarks").level = Log4Moz.Level.Trace;
- Log4Moz.repository.getLogger("Sync.Store.Bookmarks").level = Log4Moz.Level.Trace;
+ Log.repository.getLogger("Sync.Engine.Bookmarks").level = Log.Level.Trace;
+ Log.repository.getLogger("Sync.Store.Bookmarks").level = Log.Level.Trace;
run_next_test();
}
diff --git a/services/sync/tests/unit/test_bookmark_order.js b/services/sync/tests/unit/test_bookmark_order.js
index c93be2543..56806dba0 100644
--- a/services/sync/tests/unit/test_bookmark_order.js
+++ b/services/sync/tests/unit/test_bookmark_order.js
@@ -2,6 +2,7 @@
http://creativecommons.org/publicdomain/zero/1.0/ */
_("Making sure after processing incoming bookmarks, they show up in the right order");
+Cu.import("resource://gre/modules/PlacesUtils.jsm", this);
Cu.import("resource://services-sync/engines/bookmarks.js");
Cu.import("resource://services-sync/service.js");
Cu.import("resource://services-sync/util.js");
diff --git a/services/sync/tests/unit/test_bookmark_places_query_rewriting.js b/services/sync/tests/unit/test_bookmark_places_query_rewriting.js
index 94edf794f..8b764d675 100644
--- a/services/sync/tests/unit/test_bookmark_places_query_rewriting.js
+++ b/services/sync/tests/unit/test_bookmark_places_query_rewriting.js
@@ -2,6 +2,7 @@
http://creativecommons.org/publicdomain/zero/1.0/ */
_("Rewrite place: URIs.");
+Cu.import("resource://gre/modules/PlacesUtils.jsm");
Cu.import("resource://services-sync/engines/bookmarks.js");
Cu.import("resource://services-sync/service.js");
Cu.import("resource://services-sync/util.js");
@@ -11,8 +12,8 @@ let store = engine._store;
function run_test() {
initTestLogging("Trace");
- Log4Moz.repository.getLogger("Sync.Engine.Bookmarks").level = Log4Moz.Level.Trace;
- Log4Moz.repository.getLogger("Sync.Store.Bookmarks").level = Log4Moz.Level.Trace;
+ Log.repository.getLogger("Sync.Engine.Bookmarks").level = Log.Level.Trace;
+ Log.repository.getLogger("Sync.Store.Bookmarks").level = Log.Level.Trace;
let tagRecord = new BookmarkQuery("bookmarks", "abcdefabcdef");
let uri = "place:folder=499&type=7&queryType=1";
diff --git a/services/sync/tests/unit/test_bookmark_record.js b/services/sync/tests/unit/test_bookmark_record.js
index bf56835ee..194fef5e2 100644
--- a/services/sync/tests/unit/test_bookmark_record.js
+++ b/services/sync/tests/unit/test_bookmark_record.js
@@ -1,12 +1,13 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
-Cu.import("resource://services-common/log4moz.js");
+Cu.import("resource://gre/modules/Log.jsm");
Cu.import("resource://services-sync/engines/bookmarks.js");
Cu.import("resource://services-sync/keys.js");
Cu.import("resource://services-sync/record.js");
Cu.import("resource://services-sync/service.js");
Cu.import("resource://services-sync/util.js");
+Cu.import("resource://testing-common/services/sync/utils.js");
function prepareBookmarkItem(collection, id) {
let b = new Bookmark(collection, id);
@@ -15,13 +16,14 @@ function prepareBookmarkItem(collection, id) {
}
function run_test() {
+ ensureLegacyIdentityManager();
Service.identity.username = "john@example.com";
Service.identity.syncKey = "abcdeabcdeabcdeabcdeabcdea";
generateNewKeys(Service.collectionKeys);
let keyBundle = Service.identity.syncKeyBundle;
- let log = Log4Moz.repository.getLogger("Test");
- Log4Moz.repository.rootLogger.addAppender(new Log4Moz.DumpAppender());
+ let log = Log.repository.getLogger("Test");
+ Log.repository.rootLogger.addAppender(new Log.DumpAppender());
log.info("Creating a record");
diff --git a/services/sync/tests/unit/test_bookmark_smart_bookmarks.js b/services/sync/tests/unit/test_bookmark_smart_bookmarks.js
index 185bda3fa..4e9b2834d 100644
--- a/services/sync/tests/unit/test_bookmark_smart_bookmarks.js
+++ b/services/sync/tests/unit/test_bookmark_smart_bookmarks.js
@@ -2,7 +2,7 @@
http://creativecommons.org/publicdomain/zero/1.0/ */
Cu.import("resource://gre/modules/PlacesUtils.jsm");
-Cu.import("resource://services-common/log4moz.js");
+Cu.import("resource://gre/modules/Log.jsm");
Cu.import("resource://services-sync/engines.js");
Cu.import("resource://services-sync/engines/bookmarks.js");
Cu.import("resource://services-sync/service.js");
@@ -22,8 +22,6 @@ let store = engine._store;
// Clean up after other tests. Only necessary in XULRunner.
store.wipe();
-var syncTesting = new SyncTestingInfrastructure();
-
function newSmartBookmark(parent, uri, position, title, queryID) {
let id = PlacesUtils.bookmarks.insertBookmark(parent, uri, position, title);
PlacesUtils.annotations.setItemAnnotation(id, SMART_BOOKMARKS_ANNO,
@@ -60,7 +58,8 @@ function serverForFoo(engine) {
// Verify that Places smart bookmarks have their annotation uploaded and
// handled locally.
add_test(function test_annotation_uploaded() {
- new SyncTestingInfrastructure();
+ let server = serverForFoo(engine);
+ new SyncTestingInfrastructure(server.server);
let startCount = smartBookmarkCount();
@@ -108,7 +107,6 @@ add_test(function test_annotation_uploaded() {
do_check_eq(smartBookmarkCount(), startCount + 1);
_("Sync record to the server.");
- let server = serverForFoo(engine);
let collection = server.user("foo").collection("bookmarks");
try {
@@ -175,7 +173,8 @@ add_test(function test_annotation_uploaded() {
});
add_test(function test_smart_bookmarks_duped() {
- new SyncTestingInfrastructure();
+ let server = serverForFoo(engine);
+ new SyncTestingInfrastructure(server.server);
let parent = PlacesUtils.toolbarFolderId;
let uri =
@@ -189,7 +188,6 @@ add_test(function test_smart_bookmarks_duped() {
let record = store.createRecord(mostVisitedGUID);
_("Prepare sync.");
- let server = serverForFoo(engine);
let collection = server.user("foo").collection("bookmarks");
try {
@@ -229,7 +227,7 @@ add_test(function test_smart_bookmarks_duped() {
function run_test() {
initTestLogging("Trace");
- Log4Moz.repository.getLogger("Sync.Engine.Bookmarks").level = Log4Moz.Level.Trace;
+ Log.repository.getLogger("Sync.Engine.Bookmarks").level = Log.Level.Trace;
generateNewKeys(Service.collectionKeys);
diff --git a/services/sync/tests/unit/test_bookmark_store.js b/services/sync/tests/unit/test_bookmark_store.js
index 578eb142a..53ea433e6 100644
--- a/services/sync/tests/unit/test_bookmark_store.js
+++ b/services/sync/tests/unit/test_bookmark_store.js
@@ -1,6 +1,7 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
+Cu.import("resource://gre/modules/PlacesUtils.jsm");
Cu.import("resource://services-sync/engines.js");
Cu.import("resource://services-sync/engines/bookmarks.js");
Cu.import("resource://services-sync/service.js");
diff --git a/services/sync/tests/unit/test_bookmark_tracker.js b/services/sync/tests/unit/test_bookmark_tracker.js
index 1ab43a47f..6060fbae4 100644
--- a/services/sync/tests/unit/test_bookmark_tracker.js
+++ b/services/sync/tests/unit/test_bookmark_tracker.js
@@ -167,9 +167,9 @@ function test_onItemMoved() {
function run_test() {
initTestLogging("Trace");
- Log4Moz.repository.getLogger("Sync.Engine.Bookmarks").level = Log4Moz.Level.Trace;
- Log4Moz.repository.getLogger("Sync.Store.Bookmarks").level = Log4Moz.Level.Trace;
- Log4Moz.repository.getLogger("Sync.Tracker.Bookmarks").level = Log4Moz.Level.Trace;
+ Log.repository.getLogger("Sync.Engine.Bookmarks").level = Log.Level.Trace;
+ Log.repository.getLogger("Sync.Store.Bookmarks").level = Log.Level.Trace;
+ Log.repository.getLogger("Sync.Tracker.Bookmarks").level = Log.Level.Trace;
test_tracking();
test_onItemChanged();
diff --git a/services/sync/tests/unit/test_browserid_identity.js b/services/sync/tests/unit/test_browserid_identity.js
new file mode 100644
index 000000000..f3cde9f8f
--- /dev/null
+++ b/services/sync/tests/unit/test_browserid_identity.js
@@ -0,0 +1,682 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+Cu.import("resource://gre/modules/FxAccounts.jsm");
+Cu.import("resource://services-sync/browserid_identity.js");
+Cu.import("resource://services-sync/rest.js");
+Cu.import("resource://services-sync/util.js");
+Cu.import("resource://services-common/utils.js");
+Cu.import("resource://services-crypto/utils.js");
+Cu.import("resource://testing-common/services/sync/utils.js");
+Cu.import("resource://testing-common/services/sync/fxa_utils.js");
+Cu.import("resource://services-common/hawkclient.js");
+Cu.import("resource://gre/modules/FxAccounts.jsm");
+Cu.import("resource://gre/modules/FxAccountsClient.jsm");
+Cu.import("resource://gre/modules/FxAccountsCommon.js");
+Cu.import("resource://services-sync/service.js");
+Cu.import("resource://services-sync/status.js");
+Cu.import("resource://services-sync/constants.js");
+
+const SECOND_MS = 1000;
+const MINUTE_MS = SECOND_MS * 60;
+const HOUR_MS = MINUTE_MS * 60;
+
+let identityConfig = makeIdentityConfig();
+let browseridManager = new BrowserIDManager();
+configureFxAccountIdentity(browseridManager, identityConfig);
+
+/**
+ * Mock client clock and skew vs server in FxAccounts signed-in user module and
+ * API client. browserid_identity.js queries these values to construct HAWK
+ * headers. We will use this to test clock skew compensation in these headers
+ * below.
+ */
+let MockFxAccountsClient = function() {
+ FxAccountsClient.apply(this);
+};
+MockFxAccountsClient.prototype = {
+ __proto__: FxAccountsClient.prototype
+};
+
+function MockFxAccounts() {
+ let fxa = new FxAccounts({
+ _now_is: Date.now(),
+
+ now: function () {
+ return this._now_is;
+ },
+
+ fxAccountsClient: new MockFxAccountsClient()
+ });
+ fxa.internal.currentAccountState.getCertificate = function(data, keyPair, mustBeValidUntil) {
+ this.cert = {
+ validUntil: fxa.internal.now() + CERT_LIFETIME,
+ cert: "certificate",
+ };
+ return Promise.resolve(this.cert.cert);
+ };
+ return fxa;
+}
+
+function run_test() {
+ initTestLogging("Trace");
+ Log.repository.getLogger("Sync.Identity").level = Log.Level.Trace;
+ Log.repository.getLogger("Sync.BrowserIDManager").level = Log.Level.Trace;
+ run_next_test();
+};
+
+add_test(function test_initial_state() {
+ _("Verify initial state");
+ do_check_false(!!browseridManager._token);
+ do_check_false(browseridManager.hasValidToken());
+ run_next_test();
+ }
+);
+
+add_task(function test_initialializeWithCurrentIdentity() {
+ _("Verify start after initializeWithCurrentIdentity");
+ browseridManager.initializeWithCurrentIdentity();
+ yield browseridManager.whenReadyToAuthenticate.promise;
+ do_check_true(!!browseridManager._token);
+ do_check_true(browseridManager.hasValidToken());
+ do_check_eq(browseridManager.account, identityConfig.fxaccount.user.email);
+ }
+);
+
+add_task(function test_initialializeWithNoKeys() {
+ _("Verify start after initializeWithCurrentIdentity without kA, kB or keyFetchToken");
+ let identityConfig = makeIdentityConfig();
+ delete identityConfig.fxaccount.user.kA;
+ delete identityConfig.fxaccount.user.kB;
+ // there's no keyFetchToken by default, so the initialize should fail.
+ configureFxAccountIdentity(browseridManager, identityConfig);
+
+ yield browseridManager.initializeWithCurrentIdentity();
+ yield browseridManager.whenReadyToAuthenticate.promise;
+ do_check_eq(Status.login, LOGIN_SUCCEEDED, "login succeeded even without keys");
+ do_check_false(browseridManager._canFetchKeys(), "_canFetchKeys reflects lack of keys");
+ do_check_eq(browseridManager._token, null, "we don't have a token");
+});
+
+add_test(function test_getResourceAuthenticator() {
+ _("BrowserIDManager supplies a Resource Authenticator callback which returns a Hawk header.");
+ configureFxAccountIdentity(browseridManager);
+ let authenticator = browseridManager.getResourceAuthenticator();
+ do_check_true(!!authenticator);
+ let req = {uri: CommonUtils.makeURI(
+ "https://example.net/somewhere/over/the/rainbow"),
+ method: 'GET'};
+ let output = authenticator(req, 'GET');
+ do_check_true('headers' in output);
+ do_check_true('authorization' in output.headers);
+ do_check_true(output.headers.authorization.startsWith('Hawk'));
+ _("Expected internal state after successful call.");
+ do_check_eq(browseridManager._token.uid, identityConfig.fxaccount.token.uid);
+ run_next_test();
+ }
+);
+
+add_test(function test_getRESTRequestAuthenticator() {
+ _("BrowserIDManager supplies a REST Request Authenticator callback which sets a Hawk header on a request object.");
+ let request = new SyncStorageRequest(
+ "https://example.net/somewhere/over/the/rainbow");
+ let authenticator = browseridManager.getRESTRequestAuthenticator();
+ do_check_true(!!authenticator);
+ let output = authenticator(request, 'GET');
+ do_check_eq(request.uri, output.uri);
+ do_check_true(output._headers.authorization.startsWith('Hawk'));
+ do_check_true(output._headers.authorization.includes('nonce'));
+ do_check_true(browseridManager.hasValidToken());
+ run_next_test();
+ }
+);
+
+add_test(function test_resourceAuthenticatorSkew() {
+ _("BrowserIDManager Resource Authenticator compensates for clock skew in Hawk header.");
+
+ // Clock is skewed 12 hours into the future
+ // We pick a date in the past so we don't risk concealing bugs in code that
+ // uses new Date() instead of our given date.
+ let now = new Date("Fri Apr 09 2004 00:00:00 GMT-0700").valueOf() + 12 * HOUR_MS;
+ let browseridManager = new BrowserIDManager();
+ let hawkClient = new HawkClient("https://example.net/v1", "/foo");
+
+ // mock fxa hawk client skew
+ hawkClient.now = function() {
+ dump("mocked client now: " + now + '\n');
+ return now;
+ }
+ // Imagine there's already been one fxa request and the hawk client has
+ // already detected skew vs the fxa auth server.
+ let localtimeOffsetMsec = -1 * 12 * HOUR_MS;
+ hawkClient._localtimeOffsetMsec = localtimeOffsetMsec;
+
+ let fxaClient = new MockFxAccountsClient();
+ fxaClient.hawk = hawkClient;
+
+ // Sanity check
+ do_check_eq(hawkClient.now(), now);
+ do_check_eq(hawkClient.localtimeOffsetMsec, localtimeOffsetMsec);
+
+ // Properly picked up by the client
+ do_check_eq(fxaClient.now(), now);
+ do_check_eq(fxaClient.localtimeOffsetMsec, localtimeOffsetMsec);
+
+ let fxa = new MockFxAccounts();
+ fxa.internal._now_is = now;
+ fxa.internal.fxAccountsClient = fxaClient;
+
+ // Picked up by the signed-in user module
+ do_check_eq(fxa.internal.now(), now);
+ do_check_eq(fxa.internal.localtimeOffsetMsec, localtimeOffsetMsec);
+
+ do_check_eq(fxa.now(), now);
+ do_check_eq(fxa.localtimeOffsetMsec, localtimeOffsetMsec);
+
+ // Mocks within mocks...
+ configureFxAccountIdentity(browseridManager, identityConfig);
+
+ // Ensure the new FxAccounts mock has a signed-in user.
+ fxa.internal.currentAccountState.signedInUser = browseridManager._fxaService.internal.currentAccountState.signedInUser;
+
+ browseridManager._fxaService = fxa;
+
+ do_check_eq(browseridManager._fxaService.internal.now(), now);
+ do_check_eq(browseridManager._fxaService.internal.localtimeOffsetMsec,
+ localtimeOffsetMsec);
+
+ do_check_eq(browseridManager._fxaService.now(), now);
+ do_check_eq(browseridManager._fxaService.localtimeOffsetMsec,
+ localtimeOffsetMsec);
+
+ let request = new SyncStorageRequest("https://example.net/i/like/pie/");
+ let authenticator = browseridManager.getResourceAuthenticator();
+ let output = authenticator(request, 'GET');
+ dump("output" + JSON.stringify(output));
+ let authHeader = output.headers.authorization;
+ do_check_true(authHeader.startsWith('Hawk'));
+
+ // Skew correction is applied in the header and we're within the two-minute
+ // window.
+ do_check_eq(getTimestamp(authHeader), now - 12 * HOUR_MS);
+ do_check_true(
+ (getTimestampDelta(authHeader, now) - 12 * HOUR_MS) < 2 * MINUTE_MS);
+
+ run_next_test();
+});
+
+add_test(function test_RESTResourceAuthenticatorSkew() {
+ _("BrowserIDManager REST Resource Authenticator compensates for clock skew in Hawk header.");
+
+ // Clock is skewed 12 hours into the future from our arbitary date
+ let now = new Date("Fri Apr 09 2004 00:00:00 GMT-0700").valueOf() + 12 * HOUR_MS;
+ let browseridManager = new BrowserIDManager();
+ let hawkClient = new HawkClient("https://example.net/v1", "/foo");
+
+ // mock fxa hawk client skew
+ hawkClient.now = function() {
+ return now;
+ }
+ // Imagine there's already been one fxa request and the hawk client has
+ // already detected skew vs the fxa auth server.
+ hawkClient._localtimeOffsetMsec = -1 * 12 * HOUR_MS;
+
+ let fxaClient = new MockFxAccountsClient();
+ fxaClient.hawk = hawkClient;
+ let fxa = new MockFxAccounts();
+ fxa.internal._now_is = now;
+ fxa.internal.fxAccountsClient = fxaClient;
+
+ configureFxAccountIdentity(browseridManager, identityConfig);
+
+ // Ensure the new FxAccounts mock has a signed-in user.
+ fxa.internal.currentAccountState.signedInUser = browseridManager._fxaService.internal.currentAccountState.signedInUser;
+
+ browseridManager._fxaService = fxa;
+
+ do_check_eq(browseridManager._fxaService.internal.now(), now);
+
+ let request = new SyncStorageRequest("https://example.net/i/like/pie/");
+ let authenticator = browseridManager.getResourceAuthenticator();
+ let output = authenticator(request, 'GET');
+ dump("output" + JSON.stringify(output));
+ let authHeader = output.headers.authorization;
+ do_check_true(authHeader.startsWith('Hawk'));
+
+ // Skew correction is applied in the header and we're within the two-minute
+ // window.
+ do_check_eq(getTimestamp(authHeader), now - 12 * HOUR_MS);
+ do_check_true(
+ (getTimestampDelta(authHeader, now) - 12 * HOUR_MS) < 2 * MINUTE_MS);
+
+ run_next_test();
+});
+
+add_task(function test_ensureLoggedIn() {
+ configureFxAccountIdentity(browseridManager);
+ yield browseridManager.initializeWithCurrentIdentity();
+ yield browseridManager.whenReadyToAuthenticate.promise;
+ Assert.equal(Status.login, LOGIN_SUCCEEDED, "original initialize worked");
+ yield browseridManager.ensureLoggedIn();
+ Assert.equal(Status.login, LOGIN_SUCCEEDED, "original ensureLoggedIn worked");
+ Assert.ok(browseridManager._shouldHaveSyncKeyBundle,
+ "_shouldHaveSyncKeyBundle should always be true after ensureLogin completes.");
+
+ // arrange for no logged in user.
+ let fxa = browseridManager._fxaService
+ let signedInUser = fxa.internal.currentAccountState.signedInUser;
+ fxa.internal.currentAccountState.signedInUser = null;
+ browseridManager.initializeWithCurrentIdentity();
+ Assert.ok(!browseridManager._shouldHaveSyncKeyBundle,
+ "_shouldHaveSyncKeyBundle should be false so we know we are testing what we think we are.");
+ Status.login = LOGIN_FAILED_NO_USERNAME;
+ yield Assert.rejects(browseridManager.ensureLoggedIn(), "expecting rejection due to no user");
+ Assert.ok(browseridManager._shouldHaveSyncKeyBundle,
+ "_shouldHaveSyncKeyBundle should always be true after ensureLogin completes.");
+ fxa.internal.currentAccountState.signedInUser = signedInUser;
+ Status.login = LOGIN_FAILED_LOGIN_REJECTED;
+ yield Assert.rejects(browseridManager.ensureLoggedIn(),
+ "LOGIN_FAILED_LOGIN_REJECTED should have caused immediate rejection");
+ Assert.equal(Status.login, LOGIN_FAILED_LOGIN_REJECTED,
+ "status should remain LOGIN_FAILED_LOGIN_REJECTED");
+ Status.login = LOGIN_FAILED_NETWORK_ERROR;
+ yield browseridManager.ensureLoggedIn();
+ Assert.equal(Status.login, LOGIN_SUCCEEDED, "final ensureLoggedIn worked");
+});
+
+add_test(function test_tokenExpiration() {
+ _("BrowserIDManager notices token expiration:");
+ let bimExp = new BrowserIDManager();
+ configureFxAccountIdentity(bimExp, identityConfig);
+
+ let authenticator = bimExp.getResourceAuthenticator();
+ do_check_true(!!authenticator);
+ let req = {uri: CommonUtils.makeURI(
+ "https://example.net/somewhere/over/the/rainbow"),
+ method: 'GET'};
+ authenticator(req, 'GET');
+
+ // Mock the clock.
+ _("Forcing the token to expire ...");
+ Object.defineProperty(bimExp, "_now", {
+ value: function customNow() {
+ return (Date.now() + 3000001);
+ },
+ writable: true,
+ });
+ do_check_true(bimExp._token.expiration < bimExp._now());
+ _("... means BrowserIDManager knows to re-fetch it on the next call.");
+ do_check_false(bimExp.hasValidToken());
+ run_next_test();
+ }
+);
+
+add_test(function test_sha256() {
+ // Test vectors from http://www.bichlmeier.info/sha256test.html
+ let vectors = [
+ ["",
+ "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"],
+ ["abc",
+ "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"],
+ ["message digest",
+ "f7846f55cf23e14eebeab5b4e1550cad5b509e3348fbc4efa3a1413d393cb650"],
+ ["secure hash algorithm",
+ "f30ceb2bb2829e79e4ca9753d35a8ecc00262d164cc077080295381cbd643f0d"],
+ ["SHA256 is considered to be safe",
+ "6819d915c73f4d1e77e4e1b52d1fa0f9cf9beaead3939f15874bd988e2a23630"],
+ ["abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq",
+ "248d6a61d20638b8e5c026930c3e6039a33ce45964ff2167f6ecedd419db06c1"],
+ ["For this sample, this 63-byte string will be used as input data",
+ "f08a78cbbaee082b052ae0708f32fa1e50c5c421aa772ba5dbb406a2ea6be342"],
+ ["This is exactly 64 bytes long, not counting the terminating byte",
+ "ab64eff7e88e2e46165e29f2bce41826bd4c7b3552f6b382a9e7d3af47c245f8"]
+ ];
+ let bidUser = new BrowserIDManager();
+ for (let [input,output] of vectors) {
+ do_check_eq(CommonUtils.bytesAsHex(bidUser._sha256(input)), output);
+ }
+ run_next_test();
+});
+
+add_test(function test_computeXClientStateHeader() {
+ let kBhex = "fd5c747806c07ce0b9d69dcfea144663e630b65ec4963596a22f24910d7dd15d";
+ let kB = CommonUtils.hexToBytes(kBhex);
+
+ let bidUser = new BrowserIDManager();
+ let header = bidUser._computeXClientState(kB);
+
+ do_check_eq(header, "6ae94683571c7a7c54dab4700aa3995f");
+ run_next_test();
+});
+
+add_task(function test_getTokenErrors() {
+ _("BrowserIDManager correctly handles various failures to get a token.");
+
+ _("Arrange for a 401 - Sync should reflect an auth error.");
+ initializeIdentityWithTokenServerResponse({
+ status: 401,
+ headers: {"content-type": "application/json"},
+ body: JSON.stringify({}),
+ });
+ let browseridManager = Service.identity;
+
+ yield browseridManager.initializeWithCurrentIdentity();
+ yield Assert.rejects(browseridManager.whenReadyToAuthenticate.promise,
+ "should reject due to 401");
+ Assert.equal(Status.login, LOGIN_FAILED_LOGIN_REJECTED, "login was rejected");
+
+ // XXX - other interesting responses to return?
+
+ // And for good measure, some totally "unexpected" errors - we generally
+ // assume these problems are going to magically go away at some point.
+ _("Arrange for an empty body with a 200 response - should reflect a network error.");
+ initializeIdentityWithTokenServerResponse({
+ status: 200,
+ headers: [],
+ body: "",
+ });
+ browseridManager = Service.identity;
+ yield browseridManager.initializeWithCurrentIdentity();
+ yield Assert.rejects(browseridManager.whenReadyToAuthenticate.promise,
+ "should reject due to non-JSON response");
+ Assert.equal(Status.login, LOGIN_FAILED_NETWORK_ERROR, "login state is LOGIN_FAILED_NETWORK_ERROR");
+});
+
+add_task(function test_getTokenErrorWithRetry() {
+ _("tokenserver sends an observer notification on various backoff headers.");
+
+ // Set Sync's backoffInterval to zero - after we simulated the backoff header
+ // it should reflect the value we sent.
+ Status.backoffInterval = 0;
+ _("Arrange for a 503 with a Retry-After header.");
+ initializeIdentityWithTokenServerResponse({
+ status: 503,
+ headers: {"content-type": "application/json",
+ "retry-after": "100"},
+ body: JSON.stringify({}),
+ });
+ let browseridManager = Service.identity;
+
+ yield browseridManager.initializeWithCurrentIdentity();
+ yield Assert.rejects(browseridManager.whenReadyToAuthenticate.promise,
+ "should reject due to 503");
+
+ // The observer should have fired - check it got the value in the response.
+ Assert.equal(Status.login, LOGIN_FAILED_NETWORK_ERROR, "login was rejected");
+ // Sync will have the value in ms with some slop - so check it is at least that.
+ Assert.ok(Status.backoffInterval >= 100000);
+
+ _("Arrange for a 200 with an X-Backoff header.");
+ Status.backoffInterval = 0;
+ initializeIdentityWithTokenServerResponse({
+ status: 503,
+ headers: {"content-type": "application/json",
+ "x-backoff": "200"},
+ body: JSON.stringify({}),
+ });
+ browseridManager = Service.identity;
+
+ yield browseridManager.initializeWithCurrentIdentity();
+ yield Assert.rejects(browseridManager.whenReadyToAuthenticate.promise,
+ "should reject due to no token in response");
+
+ // The observer should have fired - check it got the value in the response.
+ Assert.ok(Status.backoffInterval >= 200000);
+});
+
+add_task(function test_getKeysErrorWithBackoff() {
+ _("Auth server (via hawk) sends an observer notification on backoff headers.");
+
+ // Set Sync's backoffInterval to zero - after we simulated the backoff header
+ // it should reflect the value we sent.
+ Status.backoffInterval = 0;
+ _("Arrange for a 503 with a X-Backoff header.");
+
+ let config = makeIdentityConfig();
+ // We want no kA or kB so we attempt to fetch them.
+ delete config.fxaccount.user.kA;
+ delete config.fxaccount.user.kB;
+ config.fxaccount.user.keyFetchToken = "keyfetchtoken";
+ yield initializeIdentityWithHAWKResponseFactory(config, function(method, data, uri) {
+ Assert.equal(method, "get");
+ Assert.equal(uri, "http://mockedserver:9999/account/keys")
+ return {
+ status: 503,
+ headers: {"content-type": "application/json",
+ "x-backoff": "100"},
+ body: "{}",
+ }
+ });
+
+ let browseridManager = Service.identity;
+ yield Assert.rejects(browseridManager.whenReadyToAuthenticate.promise,
+ "should reject due to 503");
+
+ // The observer should have fired - check it got the value in the response.
+ Assert.equal(Status.login, LOGIN_FAILED_NETWORK_ERROR, "login was rejected");
+ // Sync will have the value in ms with some slop - so check it is at least that.
+ Assert.ok(Status.backoffInterval >= 100000);
+});
+
+add_task(function test_getKeysErrorWithRetry() {
+ _("Auth server (via hawk) sends an observer notification on retry headers.");
+
+ // Set Sync's backoffInterval to zero - after we simulated the backoff header
+ // it should reflect the value we sent.
+ Status.backoffInterval = 0;
+ _("Arrange for a 503 with a Retry-After header.");
+
+ let config = makeIdentityConfig();
+ // We want no kA or kB so we attempt to fetch them.
+ delete config.fxaccount.user.kA;
+ delete config.fxaccount.user.kB;
+ config.fxaccount.user.keyFetchToken = "keyfetchtoken";
+ yield initializeIdentityWithHAWKResponseFactory(config, function(method, data, uri) {
+ Assert.equal(method, "get");
+ Assert.equal(uri, "http://mockedserver:9999/account/keys")
+ return {
+ status: 503,
+ headers: {"content-type": "application/json",
+ "retry-after": "100"},
+ body: "{}",
+ }
+ });
+
+ let browseridManager = Service.identity;
+ yield Assert.rejects(browseridManager.whenReadyToAuthenticate.promise,
+ "should reject due to 503");
+
+ // The observer should have fired - check it got the value in the response.
+ Assert.equal(Status.login, LOGIN_FAILED_NETWORK_ERROR, "login was rejected");
+ // Sync will have the value in ms with some slop - so check it is at least that.
+ Assert.ok(Status.backoffInterval >= 100000);
+});
+
+add_task(function test_getHAWKErrors() {
+ _("BrowserIDManager correctly handles various HAWK failures.");
+
+ _("Arrange for a 401 - Sync should reflect an auth error.");
+ let config = makeIdentityConfig();
+ yield initializeIdentityWithHAWKResponseFactory(config, function(method, data, uri) {
+ Assert.equal(method, "post");
+ Assert.equal(uri, "http://mockedserver:9999/certificate/sign")
+ return {
+ status: 401,
+ headers: {"content-type": "application/json"},
+ body: JSON.stringify({}),
+ }
+ });
+ Assert.equal(Status.login, LOGIN_FAILED_LOGIN_REJECTED, "login was rejected");
+
+ // XXX - other interesting responses to return?
+
+ // And for good measure, some totally "unexpected" errors - we generally
+ // assume these problems are going to magically go away at some point.
+ _("Arrange for an empty body with a 200 response - should reflect a network error.");
+ yield initializeIdentityWithHAWKResponseFactory(config, function(method, data, uri) {
+ Assert.equal(method, "post");
+ Assert.equal(uri, "http://mockedserver:9999/certificate/sign")
+ return {
+ status: 200,
+ headers: [],
+ body: "",
+ }
+ });
+ Assert.equal(Status.login, LOGIN_FAILED_NETWORK_ERROR, "login state is LOGIN_FAILED_NETWORK_ERROR");
+});
+
+add_task(function test_getGetKeysFailing401() {
+ _("BrowserIDManager correctly handles 401 responses fetching keys.");
+
+ _("Arrange for a 401 - Sync should reflect an auth error.");
+ let config = makeIdentityConfig();
+ // We want no kA or kB so we attempt to fetch them.
+ delete config.fxaccount.user.kA;
+ delete config.fxaccount.user.kB;
+ config.fxaccount.user.keyFetchToken = "keyfetchtoken";
+ yield initializeIdentityWithHAWKResponseFactory(config, function(method, data, uri) {
+ Assert.equal(method, "get");
+ Assert.equal(uri, "http://mockedserver:9999/account/keys")
+ return {
+ status: 401,
+ headers: {"content-type": "application/json"},
+ body: "{}",
+ }
+ });
+ Assert.equal(Status.login, LOGIN_FAILED_LOGIN_REJECTED, "login was rejected");
+});
+
+add_task(function test_getGetKeysFailing503() {
+ _("BrowserIDManager correctly handles 5XX responses fetching keys.");
+
+ _("Arrange for a 503 - Sync should reflect a network error.");
+ let config = makeIdentityConfig();
+ // We want no kA or kB so we attempt to fetch them.
+ delete config.fxaccount.user.kA;
+ delete config.fxaccount.user.kB;
+ config.fxaccount.user.keyFetchToken = "keyfetchtoken";
+ yield initializeIdentityWithHAWKResponseFactory(config, function(method, data, uri) {
+ Assert.equal(method, "get");
+ Assert.equal(uri, "http://mockedserver:9999/account/keys")
+ return {
+ status: 503,
+ headers: {"content-type": "application/json"},
+ body: "{}",
+ }
+ });
+ Assert.equal(Status.login, LOGIN_FAILED_NETWORK_ERROR, "state reflects network error");
+});
+
+add_task(function test_getKeysMissing() {
+ _("BrowserIDManager correctly handles getKeys succeeding but not returning keys.");
+
+ let browseridManager = new BrowserIDManager();
+ let identityConfig = makeIdentityConfig();
+ // our mock identity config already has kA and kB - remove them or we never
+ // try and fetch them.
+ delete identityConfig.fxaccount.user.kA;
+ delete identityConfig.fxaccount.user.kB;
+ identityConfig.fxaccount.user.keyFetchToken = 'keyFetchToken';
+
+ configureFxAccountIdentity(browseridManager, identityConfig);
+
+ // Mock a fxAccounts object that returns no keys
+ let fxa = new FxAccounts({
+ fetchAndUnwrapKeys: function () {
+ return Promise.resolve({});
+ },
+ fxAccountsClient: new MockFxAccountsClient()
+ });
+
+ // Add a mock to the currentAccountState object.
+ fxa.internal.currentAccountState.getCertificate = function(data, keyPair, mustBeValidUntil) {
+ this.cert = {
+ validUntil: fxa.internal.now() + CERT_LIFETIME,
+ cert: "certificate",
+ };
+ return Promise.resolve(this.cert.cert);
+ };
+
+ // Ensure the new FxAccounts mock has a signed-in user.
+ fxa.internal.currentAccountState.signedInUser = browseridManager._fxaService.internal.currentAccountState.signedInUser;
+
+ browseridManager._fxaService = fxa;
+
+ yield browseridManager.initializeWithCurrentIdentity();
+
+ let ex;
+ try {
+ yield browseridManager.whenReadyToAuthenticate.promise;
+ } catch (e) {
+ ex = e;
+ }
+
+ Assert.ok(ex.message.indexOf("missing kA or kB") >= 0);
+});
+
+// End of tests
+// Utility functions follow
+
+// Create a new browserid_identity object and initialize it with a
+// hawk mock that simulates HTTP responses.
+// The callback function will be called each time the mocked hawk server wants
+// to make a request. The result of the callback should be the mock response
+// object that will be returned to hawk.
+// A token server mock will be used that doesn't hit a server, so we move
+// directly to a hawk request.
+function* initializeIdentityWithHAWKResponseFactory(config, cbGetResponse) {
+ // A mock request object.
+ function MockRESTRequest(uri, credentials, extra) {
+ this._uri = uri;
+ this._credentials = credentials;
+ this._extra = extra;
+ };
+ MockRESTRequest.prototype = {
+ setHeader: function() {},
+ post: function(data, callback) {
+ this.response = cbGetResponse("post", data, this._uri, this._credentials, this._extra);
+ callback.call(this);
+ },
+ get: function(callback) {
+ this.response = cbGetResponse("get", null, this._uri, this._credentials, this._extra);
+ callback.call(this);
+ }
+ }
+
+ // The hawk client.
+ function MockedHawkClient() {}
+ MockedHawkClient.prototype = new HawkClient("http://mockedserver:9999");
+ MockedHawkClient.prototype.constructor = MockedHawkClient;
+ MockedHawkClient.prototype.newHAWKAuthenticatedRESTRequest = function(uri, credentials, extra) {
+ return new MockRESTRequest(uri, credentials, extra);
+ }
+ // Arrange for the same observerPrefix as FxAccountsClient uses
+ MockedHawkClient.prototype.observerPrefix = "FxA:hawk";
+
+ // tie it all together - configureFxAccountIdentity isn't useful here :(
+ let fxaClient = new MockFxAccountsClient();
+ fxaClient.hawk = new MockedHawkClient();
+ let internal = {
+ fxAccountsClient: fxaClient,
+ }
+ let fxa = new FxAccounts(internal);
+ fxa.internal.currentAccountState.signedInUser = {
+ accountData: config.fxaccount.user,
+ };
+
+ browseridManager._fxaService = fxa;
+ browseridManager._signedInUser = null;
+ yield browseridManager.initializeWithCurrentIdentity();
+ yield Assert.rejects(browseridManager.whenReadyToAuthenticate.promise,
+ "expecting rejection due to hawk error");
+}
+
+
+function getTimestamp(hawkAuthHeader) {
+ return parseInt(/ts="(\d+)"/.exec(hawkAuthHeader)[1], 10) * SECOND_MS;
+}
+
+function getTimestampDelta(hawkAuthHeader, now=Date.now()) {
+ return Math.abs(getTimestamp(hawkAuthHeader) - now);
+}
+
diff --git a/services/sync/tests/unit/test_clients_engine.js b/services/sync/tests/unit/test_clients_engine.js
index b2c48f79e..919913f82 100644
--- a/services/sync/tests/unit/test_clients_engine.js
+++ b/services/sync/tests/unit/test_clients_engine.js
@@ -4,6 +4,7 @@
Cu.import("resource://services-sync/constants.js");
Cu.import("resource://services-sync/engines.js");
Cu.import("resource://services-sync/engines/clients.js");
+Cu.import("resource://services-sync/record.js");
Cu.import("resource://services-sync/service.js");
Cu.import("resource://services-sync/util.js");
Cu.import("resource://testing-common/services/sync/utils.js");
@@ -13,6 +14,29 @@ const LESS_THAN_CLIENTS_TTL_REFRESH = 86400; // 1 day
let engine = Service.clientsEngine;
+/**
+ * Unpack the record with this ID, and verify that it has the same version that
+ * we should be putting into records.
+ */
+function check_record_version(user, id) {
+ let payload = JSON.parse(user.collection("clients").wbo(id).payload);
+
+ let rec = new CryptoWrapper();
+ rec.id = id;
+ rec.collection = "clients";
+ rec.ciphertext = payload.ciphertext;
+ rec.hmac = payload.hmac;
+ rec.IV = payload.IV;
+
+ let cleartext = rec.decrypt(Service.collectionKeys.keyForCollection("clients"));
+
+ _("Payload is " + JSON.stringify(cleartext));
+ do_check_eq(Services.appinfo.version, cleartext.version);
+ do_check_eq(2, cleartext.protocols.length);
+ do_check_eq("1.1", cleartext.protocols[0]);
+ do_check_eq("1.5", cleartext.protocols[1]);
+}
+
add_test(function test_bad_hmac() {
_("Ensure that Clients engine deletes corrupt records.");
let contents = {
@@ -57,9 +81,9 @@ add_test(function test_bad_hmac() {
}
try {
+ ensureLegacyIdentityManager();
let passphrase = "abcdeabcdeabcdeabcdeabcdea";
- Service.serverURL = TEST_SERVER_URL;
- Service.clusterURL = TEST_CLUSTER_URL;
+ Service.serverURL = server.baseURI;
Service.login("foo", "ilovejane", passphrase);
generateNewKeys(Service.collectionKeys);
@@ -71,6 +95,9 @@ add_test(function test_bad_hmac() {
check_clients_count(1);
do_check_true(engine.lastRecordUpload > 0);
+ // Our uploaded record has a version.
+ check_record_version(user, engine.localID);
+
// Initial setup can wipe the server, so clean up.
deletedCollections = [];
deletedItems = [];
@@ -169,9 +196,6 @@ add_test(function test_properties() {
add_test(function test_sync() {
_("Ensure that Clients engine uploads a new client record once a week.");
- new SyncTestingInfrastructure();
- generateNewKeys(Service.collectionKeys);
-
let contents = {
meta: {global: {engines: {clients: {version: engine.version,
syncID: engine.syncID}}}},
@@ -181,6 +205,9 @@ add_test(function test_sync() {
let server = serverForUsers({"foo": "password"}, contents);
let user = server.user("foo");
+ new SyncTestingInfrastructure(server.server);
+ generateNewKeys(Service.collectionKeys);
+
function clientWBO() {
return user.collection("clients").wbo(engine.localID);
}
@@ -407,8 +434,6 @@ add_test(function test_process_incoming_commands() {
add_test(function test_command_sync() {
_("Ensure that commands are synced across clients.");
- new SyncTestingInfrastructure();
-
engine._store.wipe();
generateNewKeys(Service.collectionKeys);
@@ -419,6 +444,8 @@ add_test(function test_command_sync() {
crypto: {}
};
let server = serverForUsers({"foo": "password"}, contents);
+ new SyncTestingInfrastructure(server.server);
+
let user = server.user("foo");
let remoteId = Utils.makeGUID();
@@ -550,8 +577,34 @@ add_test(function test_receive_display_uri() {
do_check_true(engine.processIncomingCommands());
});
+add_test(function test_optional_client_fields() {
+ _("Ensure that we produce records with the fields added in Bug 1097222.");
+
+ const SUPPORTED_PROTOCOL_VERSIONS = ["1.1", "1.5"];
+ let local = engine._store.createRecord(engine.localID, "clients");
+ do_check_eq(local.name, engine.localName);
+ do_check_eq(local.type, engine.localType);
+ do_check_eq(local.version, Services.appinfo.version);
+ do_check_array_eq(local.protocols, SUPPORTED_PROTOCOL_VERSIONS);
+
+ // Optional fields.
+ // Make sure they're what they ought to be...
+ do_check_eq(local.os, Services.appinfo.OS);
+ do_check_eq(local.appPackage, Services.appinfo.ID);
+
+ // ... and also that they're non-empty.
+ do_check_true(!!local.os);
+ do_check_true(!!local.appPackage);
+ do_check_true(!!local.application);
+
+ // We don't currently populate device or formfactor.
+ // See Bug 1100722, Bug 1100723.
+
+ run_next_test();
+});
+
function run_test() {
initTestLogging("Trace");
- Log4Moz.repository.getLogger("Sync.Engine.Clients").level = Log4Moz.Level.Trace;
+ Log.repository.getLogger("Sync.Engine.Clients").level = Log.Level.Trace;
run_next_test();
}
diff --git a/services/sync/tests/unit/test_clients_escape.js b/services/sync/tests/unit/test_clients_escape.js
index 3512087d8..8c8cd63e3 100644
--- a/services/sync/tests/unit/test_clients_escape.js
+++ b/services/sync/tests/unit/test_clients_escape.js
@@ -5,10 +5,12 @@ Cu.import("resource://services-sync/keys.js");
Cu.import("resource://services-sync/record.js");
Cu.import("resource://services-sync/service.js");
Cu.import("resource://services-sync/util.js");
+Cu.import("resource://testing-common/services/sync/utils.js");
function run_test() {
_("Set up test fixtures.");
+ ensureLegacyIdentityManager();
Service.identity.username = "john@example.com";
Service.clusterURL = "http://fakebase/";
let baseUri = "http://fakebase/1.1/foo/storage/";
diff --git a/services/sync/tests/unit/test_collections_recovery.js b/services/sync/tests/unit/test_collections_recovery.js
index 2a089fd05..377a05383 100644
--- a/services/sync/tests/unit/test_collections_recovery.js
+++ b/services/sync/tests/unit/test_collections_recovery.js
@@ -6,7 +6,7 @@ Cu.import("resource://services-sync/service.js");
Cu.import("resource://services-sync/util.js");
Cu.import("resource://testing-common/services/sync/utils.js");
-add_test(function test_missing_crypto_collection() {
+add_identity_test(this, function test_missing_crypto_collection() {
let johnHelper = track_collections_helper();
let johnU = johnHelper.with_updated_collection;
let johnColls = johnHelper.collections;
@@ -24,9 +24,7 @@ add_test(function test_missing_crypto_collection() {
};
}
- setBasicCredentials("johndoe", "ilovejane", "a-aaaaa-aaaaa-aaaaa-aaaaa-aaaaa");
- Service.serverURL = TEST_SERVER_URL;
- Service.clusterURL = TEST_CLUSTER_URL;
+ yield configureIdentity({username: "johndoe"});
let handlers = {
"/1.1/johndoe/info/collections": maybe_empty(johnHelper.handler),
@@ -40,6 +38,7 @@ add_test(function test_missing_crypto_collection() {
johnU(coll, new ServerCollection({}, true).handler());
}
let server = httpd_setup(handlers);
+ Service.serverURL = server.baseURI;
try {
let fresh = 0;
@@ -71,7 +70,9 @@ add_test(function test_missing_crypto_collection() {
} finally {
Svc.Prefs.resetBranch("");
- server.stop(run_next_test);
+ let deferred = Promise.defer();
+ server.stop(deferred.resolve);
+ yield deferred.promise;
}
});
diff --git a/services/sync/tests/unit/test_corrupt_keys.js b/services/sync/tests/unit/test_corrupt_keys.js
index 9ee0f34b3..2db080a8f 100644
--- a/services/sync/tests/unit/test_corrupt_keys.js
+++ b/services/sync/tests/unit/test_corrupt_keys.js
@@ -1,7 +1,8 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
-Cu.import("resource://services-common/log4moz.js");
+Cu.import("resource://gre/modules/PlacesUtils.jsm");
+Cu.import("resource://gre/modules/Log.jsm");
Cu.import("resource://services-sync/constants.js");
Cu.import("resource://services-sync/engines.js");
Cu.import("resource://services-sync/engines/tabs.js");
@@ -46,18 +47,16 @@ add_task(function test_locally_changed_keys() {
}],
attributes: {
image: "image"
- },
- extData: {
- weaveLastUsed: 1
- }}]}]};
+ }
+ }]}]};
delete Svc.Session;
Svc.Session = {
getBrowserState: function () JSON.stringify(myTabs)
};
setBasicCredentials("johndoe", "password", passphrase);
- Service.serverURL = TEST_SERVER_URL;
- Service.clusterURL = TEST_CLUSTER_URL;
+ Service.serverURL = server.baseURI;
+ Service.clusterURL = server.baseURI;
Service.engineManager.register(HistoryEngine);
@@ -203,8 +202,10 @@ add_task(function test_locally_changed_keys() {
});
function run_test() {
- let logger = Log4Moz.repository.rootLogger;
- Log4Moz.repository.rootLogger.addAppender(new Log4Moz.DumpAppender());
+ let logger = Log.repository.rootLogger;
+ Log.repository.rootLogger.addAppender(new Log.DumpAppender());
+
+ ensureLegacyIdentityManager();
run_next_test();
}
diff --git a/services/sync/tests/unit/test_declined.js b/services/sync/tests/unit/test_declined.js
new file mode 100644
index 000000000..e9e9b002a
--- /dev/null
+++ b/services/sync/tests/unit/test_declined.js
@@ -0,0 +1,153 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+Cu.import("resource://services-sync/stages/declined.js");
+Cu.import("resource://services-sync/stages/enginesync.js");
+Cu.import("resource://services-sync/engines.js");
+Cu.import("resource://services-sync/service.js");
+Cu.import("resource://services-common/observers.js");
+
+function run_test() {
+ run_next_test();
+}
+
+function PetrolEngine() {}
+PetrolEngine.prototype.name = "petrol";
+
+function DieselEngine() {}
+DieselEngine.prototype.name = "diesel";
+
+function DummyEngine() {}
+DummyEngine.prototype.name = "dummy";
+
+function ActualEngine() {}
+ActualEngine.prototype = {__proto__: Engine.prototype,
+ name: 'actual'};
+
+function getEngineManager() {
+ let manager = new EngineManager(Service);
+ Service.engineManager = manager;
+ manager._engines = {
+ "petrol": new PetrolEngine(),
+ "diesel": new DieselEngine(),
+ "dummy": new DummyEngine(),
+ "actual": new ActualEngine(),
+ };
+ return manager;
+}
+
+/**
+ * 'Fetch' a meta/global record that doesn't mention declined.
+ *
+ * Push it into the EngineSynchronizer to set enabled; verify that those are
+ * correct.
+ *
+ * Then push it into DeclinedEngines to set declined; verify that none are
+ * declined, and a notification is sent for our locally disabled-but-not-
+ * declined engines.
+ */
+add_test(function testOldMeta() {
+ let meta = {
+ payload: {
+ engines: {
+ "petrol": 1,
+ "diesel": 2,
+ "nonlocal": 3, // Enabled but not supported.
+ },
+ },
+ };
+
+ _("Record: " + JSON.stringify(meta));
+
+ let manager = getEngineManager();
+
+ // Update enabled from meta/global.
+ let engineSync = new EngineSynchronizer(Service);
+ engineSync._updateEnabledFromMeta(meta, 3, manager);
+
+ Assert.ok(manager._engines["petrol"].enabled, "'petrol' locally enabled.");
+ Assert.ok(manager._engines["diesel"].enabled, "'diesel' locally enabled.");
+ Assert.ok(!("nonlocal" in manager._engines), "We don't know anything about the 'nonlocal' engine.");
+ Assert.ok(!manager._engines["actual"].enabled, "'actual' not locally enabled.");
+ Assert.ok(!manager.isDeclined("actual"), "'actual' not declined, though.");
+
+ let declinedEngines = new DeclinedEngines(Service);
+
+ function onNotDeclined(subject, topic, data) {
+ Observers.remove("weave:engines:notdeclined", onNotDeclined);
+ Assert.ok(subject.undecided.has("actual"), "EngineManager observed that 'actual' was undecided.");
+
+ let declined = manager.getDeclined();
+ _("Declined: " + JSON.stringify(declined));
+
+ Assert.ok(!meta.changed, "No need to upload a new meta/global.");
+ run_next_test();
+ }
+
+ Observers.add("weave:engines:notdeclined", onNotDeclined);
+
+ declinedEngines.updateDeclined(meta, manager);
+});
+
+/**
+ * 'Fetch' a meta/global that declines an engine we don't
+ * recognize. Ensure that we track that declined engine along
+ * with any we locally declined, and that the meta/global
+ * record is marked as changed and includes all declined
+ * engines.
+ */
+add_test(function testDeclinedMeta() {
+ let meta = {
+ payload: {
+ engines: {
+ "petrol": 1,
+ "diesel": 2,
+ "nonlocal": 3, // Enabled but not supported.
+ },
+ declined: ["nonexistent"], // Declined and not supported.
+ },
+ };
+
+ _("Record: " + JSON.stringify(meta));
+
+ let manager = getEngineManager();
+ manager._engines["petrol"].enabled = true;
+ manager._engines["diesel"].enabled = true;
+ manager._engines["dummy"].enabled = true;
+ manager._engines["actual"].enabled = false; // Disabled but not declined.
+
+ manager.decline(["localdecline"]); // Declined and not supported.
+
+ let declinedEngines = new DeclinedEngines(Service);
+
+ function onNotDeclined(subject, topic, data) {
+ Observers.remove("weave:engines:notdeclined", onNotDeclined);
+ Assert.ok(subject.undecided.has("actual"), "EngineManager observed that 'actual' was undecided.");
+
+ let declined = manager.getDeclined();
+ _("Declined: " + JSON.stringify(declined));
+
+ Assert.equal(declined.indexOf("actual"), -1, "'actual' is locally disabled, but not marked as declined.");
+
+ Assert.equal(declined.indexOf("clients"), -1, "'clients' is enabled and not remotely declined.");
+ Assert.equal(declined.indexOf("petrol"), -1, "'petrol' is enabled and not remotely declined.");
+ Assert.equal(declined.indexOf("diesel"), -1, "'diesel' is enabled and not remotely declined.");
+ Assert.equal(declined.indexOf("dummy"), -1, "'dummy' is enabled and not remotely declined.");
+
+ Assert.ok(0 <= declined.indexOf("nonexistent"), "'nonexistent' was declined on the server.");
+
+ Assert.ok(0 <= declined.indexOf("localdecline"), "'localdecline' was declined locally.");
+
+ // The meta/global is modified, too.
+ Assert.ok(0 <= meta.payload.declined.indexOf("nonexistent"), "meta/global's declined contains 'nonexistent'.");
+ Assert.ok(0 <= meta.payload.declined.indexOf("localdecline"), "meta/global's declined contains 'localdecline'.");
+ Assert.strictEqual(true, meta.changed, "meta/global was changed.");
+
+ run_next_test();
+ }
+
+ Observers.add("weave:engines:notdeclined", onNotDeclined);
+
+ declinedEngines.updateDeclined(meta, manager);
+});
+
diff --git a/services/sync/tests/unit/test_engine.js b/services/sync/tests/unit/test_engine.js
index 350f7b5b8..000cd5b4a 100644
--- a/services/sync/tests/unit/test_engine.js
+++ b/services/sync/tests/unit/test_engine.js
@@ -18,15 +18,15 @@ SteamStore.prototype = {
}
};
-function SteamTracker(engine) {
- Tracker.call(this, "Steam", engine);
+function SteamTracker(name, engine) {
+ Tracker.call(this, name || "Steam", engine);
}
SteamTracker.prototype = {
__proto__: Tracker.prototype
};
-function SteamEngine() {
- Engine.call(this, "Steam", Service);
+function SteamEngine(service) {
+ Engine.call(this, "Steam", service);
this.wasReset = false;
this.wasSynced = false;
}
@@ -186,3 +186,32 @@ add_test(function test_sync() {
engineObserver.reset();
}
});
+
+add_test(function test_disabled_no_track() {
+ _("When an engine is disabled, its tracker is not tracking.");
+ let engine = new SteamEngine(Service);
+ let tracker = engine._tracker;
+ do_check_eq(engine, tracker.engine);
+
+ do_check_false(engine.enabled);
+ do_check_false(tracker._isTracking);
+ do_check_empty(tracker.changedIDs);
+
+ do_check_false(tracker.engineIsEnabled());
+ tracker.observe(null, "weave:engine:start-tracking", null);
+ do_check_false(tracker._isTracking);
+ do_check_empty(tracker.changedIDs);
+
+ engine.enabled = true;
+ tracker.observe(null, "weave:engine:start-tracking", null);
+ do_check_true(tracker._isTracking);
+ do_check_empty(tracker.changedIDs);
+
+ tracker.addChangedID("abcdefghijkl");
+ do_check_true(0 < tracker.changedIDs["abcdefghijkl"]);
+ Svc.Prefs.set("engine." + engine.prefName, false);
+ do_check_false(tracker._isTracking);
+ do_check_empty(tracker.changedIDs);
+
+ run_next_test();
+});
diff --git a/services/sync/tests/unit/test_engine_abort.js b/services/sync/tests/unit/test_engine_abort.js
index 9e4cd9a96..8ec866443 100644
--- a/services/sync/tests/unit/test_engine_abort.js
+++ b/services/sync/tests/unit/test_engine_abort.js
@@ -10,17 +10,8 @@ Cu.import("resource://testing-common/services/sync/utils.js");
add_test(function test_processIncoming_abort() {
_("An abort exception, raised in applyIncoming, will abort _processIncoming.");
- new SyncTestingInfrastructure();
- generateNewKeys(Service.collectionKeys);
-
let engine = new RotaryEngine(Service);
- _("Create some server data.");
- let meta_global = Service.recordManager.set(engine.metaURL,
- new WBORecord(engine.metaURL));
- meta_global.payload.engines = {rotary: {version: engine.version,
- syncID: engine.syncID}};
-
let collection = new ServerCollection();
let id = Utils.makeGUID();
let payload = encryptPayload({id: id, denomination: "Record No. " + id});
@@ -30,6 +21,14 @@ add_test(function test_processIncoming_abort() {
"/1.1/foo/storage/rotary": collection.handler()
});
+ new SyncTestingInfrastructure(server);
+ generateNewKeys(Service.collectionKeys);
+
+ _("Create some server data.");
+ let meta_global = Service.recordManager.set(engine.metaURL,
+ new WBORecord(engine.metaURL));
+ meta_global.payload.engines = {rotary: {version: engine.version,
+ syncID: engine.syncID}};
_("Fake applyIncoming to abort.");
engine._store.applyIncoming = function (record) {
let ex = {code: Engine.prototype.eEngineAbortApplyIncoming,
diff --git a/services/sync/tests/unit/test_enginemanager.js b/services/sync/tests/unit/test_enginemanager.js
index 3459b9c17..8917cc5bc 100644
--- a/services/sync/tests/unit/test_enginemanager.js
+++ b/services/sync/tests/unit/test_enginemanager.js
@@ -5,6 +5,23 @@ Cu.import("resource://services-sync/engines.js");
Cu.import("resource://services-sync/service.js");
function run_test() {
+ run_next_test();
+}
+
+function PetrolEngine() {}
+PetrolEngine.prototype.name = "petrol";
+
+function DieselEngine() {}
+DieselEngine.prototype.name = "diesel";
+
+function DummyEngine() {}
+DummyEngine.prototype.name = "dummy";
+
+function ActualEngine() {}
+ActualEngine.prototype = {__proto__: Engine.prototype,
+ name: 'actual'};
+
+add_test(function test_basics() {
_("We start out with a clean slate");
let manager = new EngineManager(Service);
@@ -14,8 +31,6 @@ function run_test() {
do_check_eq(manager.get('dummy'), undefined);
_("Register an engine");
- function DummyEngine() {}
- DummyEngine.prototype.name = "dummy";
manager.register(DummyEngine);
let dummy = manager.get('dummy');
do_check_true(dummy instanceof DummyEngine);
@@ -29,11 +44,6 @@ function run_test() {
do_check_eq(manager.get('dummy'), dummy);
_("Register multiple engines in one go");
- function PetrolEngine() {}
- PetrolEngine.prototype.name = "petrol";
- function DieselEngine() {}
- DieselEngine.prototype.name = "diesel";
-
manager.register([PetrolEngine, DieselEngine]);
let petrol = manager.get('petrol');
let diesel = manager.get('diesel');
@@ -65,6 +75,23 @@ function run_test() {
engines = manager.getEnabled();
do_check_eq(engines.length, 3);
+ _("getEnabled() returns enabled engines in sorted order");
+ petrol.syncPriority = 1;
+ dummy.syncPriority = 2;
+ diesel.syncPriority = 3;
+
+ engines = manager.getEnabled();
+
+ do_check_array_eq(engines, [petrol, dummy, diesel]);
+
+ _("Changing the priorities should change the order in getEnabled()");
+
+ dummy.syncPriority = 4;
+
+ engines = manager.getEnabled();
+
+ do_check_array_eq(engines, [petrol, diesel, dummy]);
+
_("Unregister an engine by name");
manager.unregister('dummy');
do_check_eq(manager.get('dummy'), undefined);
@@ -74,9 +101,6 @@ function run_test() {
_("Unregister an engine by value");
// manager.unregister() checks for instanceof Engine, so let's make one:
- function ActualEngine() {}
- ActualEngine.prototype = {__proto__: Engine.prototype,
- name: 'actual'};
manager.register(ActualEngine);
let actual = manager.get('actual');
do_check_true(actual instanceof ActualEngine);
@@ -84,4 +108,7 @@ function run_test() {
manager.unregister(actual);
do_check_eq(manager.get('actual'), undefined);
-}
+
+ run_next_test();
+});
+
diff --git a/services/sync/tests/unit/test_errorhandler.js b/services/sync/tests/unit/test_errorhandler.js
index 3a1256d8a..c087acc9f 100644
--- a/services/sync/tests/unit/test_errorhandler.js
+++ b/services/sync/tests/unit/test_errorhandler.js
@@ -10,11 +10,10 @@ Cu.import("resource://services-sync/service.js");
Cu.import("resource://services-sync/status.js");
Cu.import("resource://services-sync/util.js");
Cu.import("resource://testing-common/services/sync/utils.js");
+Cu.import("resource://gre/modules/FileUtils.jsm");
-const TEST_MAINTENANCE_URL = "http://localhost:8080/maintenance/";
+const FAKE_SERVER_URL = "http://dummy:9000/";
const logsdir = FileUtils.getDir("ProfD", ["weave", "logs"], true);
-const LOG_PREFIX_SUCCESS = "success-";
-const LOG_PREFIX_ERROR = "error-";
const PROLONGED_ERROR_DURATION =
(Svc.Prefs.get('errorhandler.networkFailureReportTimeout') * 2) * 1000;
@@ -51,9 +50,11 @@ let errorHandler = Service.errorHandler;
function run_test() {
initTestLogging("Trace");
- Log4Moz.repository.getLogger("Sync.Service").level = Log4Moz.Level.Trace;
- Log4Moz.repository.getLogger("Sync.SyncScheduler").level = Log4Moz.Level.Trace;
- Log4Moz.repository.getLogger("Sync.ErrorHandler").level = Log4Moz.Level.Trace;
+ Log.repository.getLogger("Sync.Service").level = Log.Level.Trace;
+ Log.repository.getLogger("Sync.SyncScheduler").level = Log.Level.Trace;
+ Log.repository.getLogger("Sync.ErrorHandler").level = Log.Level.Trace;
+
+ ensureLegacyIdentityManager();
run_next_test();
}
@@ -125,12 +126,15 @@ function sync_httpd_setup() {
});
}
-function setUp() {
- setBasicCredentials("johndoe", "ilovejane", "abcdeabcdeabcdeabcdeabcdea");
- Service.serverURL = TEST_SERVER_URL;
- Service.clusterURL = TEST_CLUSTER_URL;
-
- return generateAndUploadKeys();
+function setUp(server) {
+ return configureIdentity({username: "johndoe"}).then(
+ () => {
+ Service.serverURL = server.baseURI + "/";
+ Service.clusterURL = server.baseURI + "/";
+ }
+ ).then(
+ () => generateAndUploadKeys()
+ );
}
function generateAndUploadKeys() {
@@ -144,17 +148,19 @@ function clean() {
Service.startOver();
Status.resetSync();
Status.resetBackoff();
+ errorHandler.didReportProlongedError = false;
}
-add_test(function test_401_logout() {
+add_identity_test(this, function test_401_logout() {
let server = sync_httpd_setup();
- setUp();
+ yield setUp(server);
// By calling sync, we ensure we're logged in.
Service.sync();
do_check_eq(Status.sync, SYNC_SUCCEEDED);
do_check_true(Service.isLoggedIn);
+ let deferred = Promise.defer();
Svc.Obs.add("weave:service:sync:error", onSyncError);
function onSyncError() {
_("Got weave:service:sync:error in first sync.");
@@ -171,24 +177,25 @@ add_test(function test_401_logout() {
// Clean up.
Utils.nextTick(function () {
Service.startOver();
- server.stop(run_next_test);
+ server.stop(deferred.resolve);
});
}
Svc.Obs.add("weave:service:login:error", onLoginError);
}
// Make sync fail due to login rejected.
- setBasicCredentials("janedoe", "irrelevant", "irrelevant");
+ yield configureIdentity({username: "janedoe"});
Service._updateCachedURLs();
_("Starting first sync.");
Service.sync();
_("First sync done.");
+ yield deferred.promise;
});
-add_test(function test_credentials_changed_logout() {
+add_identity_test(this, function test_credentials_changed_logout() {
let server = sync_httpd_setup();
- setUp();
+ yield setUp(server);
// By calling sync, we ensure we're logged in.
Service.sync();
@@ -203,10 +210,12 @@ add_test(function test_credentials_changed_logout() {
// Clean up.
Service.startOver();
- server.stop(run_next_test);
+ let deferred = Promise.defer();
+ server.stop(deferred.resolve);
+ yield deferred.promise;
});
-add_test(function test_no_lastSync_pref() {
+add_identity_test(this, function test_no_lastSync_pref() {
// Test reported error.
Status.resetSync();
errorHandler.dontIgnoreErrors = true;
@@ -219,17 +228,16 @@ add_test(function test_no_lastSync_pref() {
Status.login = LOGIN_FAILED_NETWORK_ERROR;
do_check_true(errorHandler.shouldReportError());
- run_next_test();
});
-add_test(function test_shouldReportError() {
+add_identity_test(this, function test_shouldReportError() {
Status.login = MASTER_PASSWORD_LOCKED;
do_check_false(errorHandler.shouldReportError());
// Give ourselves a clusterURL so that the temporary 401 no-error situation
// doesn't come into play.
- Service.serverURL = TEST_SERVER_URL;
- Service.clusterURL = TEST_CLUSTER_URL;
+ Service.serverURL = FAKE_SERVER_URL;
+ Service.clusterURL = FAKE_SERVER_URL;
// Test dontIgnoreErrors, non-network, non-prolonged, login error reported
Status.resetSync();
@@ -288,18 +296,32 @@ add_test(function test_shouldReportError() {
do_check_true(errorHandler.shouldReportError());
// Test non-network, prolonged, login error reported
+ do_check_false(errorHandler.didReportProlongedError);
Status.resetSync();
setLastSync(PROLONGED_ERROR_DURATION);
errorHandler.dontIgnoreErrors = false;
Status.login = LOGIN_FAILED_NO_PASSWORD;
do_check_true(errorHandler.shouldReportError());
+ do_check_true(errorHandler.didReportProlongedError);
+
+ // Second time with prolonged error and without resetting
+ // didReportProlongedError, sync error should not be reported.
+ Status.resetSync();
+ setLastSync(PROLONGED_ERROR_DURATION);
+ errorHandler.dontIgnoreErrors = false;
+ Status.login = LOGIN_FAILED_NO_PASSWORD;
+ do_check_false(errorHandler.shouldReportError());
+ do_check_true(errorHandler.didReportProlongedError);
// Test non-network, prolonged, sync error reported
Status.resetSync();
setLastSync(PROLONGED_ERROR_DURATION);
errorHandler.dontIgnoreErrors = false;
+ errorHandler.didReportProlongedError = false;
Status.sync = CREDENTIALS_CHANGED;
do_check_true(errorHandler.shouldReportError());
+ do_check_true(errorHandler.didReportProlongedError);
+ errorHandler.didReportProlongedError = false;
// Test network, prolonged, login error reported
Status.resetSync();
@@ -307,6 +329,8 @@ add_test(function test_shouldReportError() {
errorHandler.dontIgnoreErrors = false;
Status.login = LOGIN_FAILED_NETWORK_ERROR;
do_check_true(errorHandler.shouldReportError());
+ do_check_true(errorHandler.didReportProlongedError);
+ errorHandler.didReportProlongedError = false;
// Test network, prolonged, sync error reported
Status.resetSync();
@@ -314,6 +338,8 @@ add_test(function test_shouldReportError() {
errorHandler.dontIgnoreErrors = false;
Status.sync = LOGIN_FAILED_NETWORK_ERROR;
do_check_true(errorHandler.shouldReportError());
+ do_check_true(errorHandler.didReportProlongedError);
+ errorHandler.didReportProlongedError = false;
// Test non-network, non-prolonged, login error reported
Status.resetSync();
@@ -321,6 +347,7 @@ add_test(function test_shouldReportError() {
errorHandler.dontIgnoreErrors = false;
Status.login = LOGIN_FAILED_NO_PASSWORD;
do_check_true(errorHandler.shouldReportError());
+ do_check_false(errorHandler.didReportProlongedError);
// Test non-network, non-prolonged, sync error reported
Status.resetSync();
@@ -328,6 +355,7 @@ add_test(function test_shouldReportError() {
errorHandler.dontIgnoreErrors = false;
Status.sync = CREDENTIALS_CHANGED;
do_check_true(errorHandler.shouldReportError());
+ do_check_false(errorHandler.didReportProlongedError);
// Test network, non-prolonged, login error reported
Status.resetSync();
@@ -335,6 +363,7 @@ add_test(function test_shouldReportError() {
errorHandler.dontIgnoreErrors = false;
Status.login = LOGIN_FAILED_NETWORK_ERROR;
do_check_false(errorHandler.shouldReportError());
+ do_check_false(errorHandler.didReportProlongedError);
// Test network, non-prolonged, sync error reported
Status.resetSync();
@@ -342,6 +371,7 @@ add_test(function test_shouldReportError() {
errorHandler.dontIgnoreErrors = false;
Status.sync = LOGIN_FAILED_NETWORK_ERROR;
do_check_false(errorHandler.shouldReportError());
+ do_check_false(errorHandler.didReportProlongedError);
// Test server maintenance, sync errors are not reported
Status.resetSync();
@@ -349,6 +379,7 @@ add_test(function test_shouldReportError() {
errorHandler.dontIgnoreErrors = false;
Status.sync = SERVER_MAINTENANCE;
do_check_false(errorHandler.shouldReportError());
+ do_check_false(errorHandler.didReportProlongedError);
// Test server maintenance, login errors are not reported
Status.resetSync();
@@ -356,6 +387,7 @@ add_test(function test_shouldReportError() {
errorHandler.dontIgnoreErrors = false;
Status.login = SERVER_MAINTENANCE;
do_check_false(errorHandler.shouldReportError());
+ do_check_false(errorHandler.didReportProlongedError);
// Test prolonged, server maintenance, sync errors are reported
Status.resetSync();
@@ -363,6 +395,8 @@ add_test(function test_shouldReportError() {
errorHandler.dontIgnoreErrors = false;
Status.sync = SERVER_MAINTENANCE;
do_check_true(errorHandler.shouldReportError());
+ do_check_true(errorHandler.didReportProlongedError);
+ errorHandler.didReportProlongedError = false;
// Test prolonged, server maintenance, login errors are reported
Status.resetSync();
@@ -370,6 +404,8 @@ add_test(function test_shouldReportError() {
errorHandler.dontIgnoreErrors = false;
Status.login = SERVER_MAINTENANCE;
do_check_true(errorHandler.shouldReportError());
+ do_check_true(errorHandler.didReportProlongedError);
+ errorHandler.didReportProlongedError = false;
// Test dontIgnoreErrors, server maintenance, sync errors are reported
Status.resetSync();
@@ -377,6 +413,8 @@ add_test(function test_shouldReportError() {
errorHandler.dontIgnoreErrors = true;
Status.sync = SERVER_MAINTENANCE;
do_check_true(errorHandler.shouldReportError());
+ // dontIgnoreErrors means we don't set didReportProlongedError
+ do_check_false(errorHandler.didReportProlongedError);
// Test dontIgnoreErrors, server maintenance, login errors are reported
Status.resetSync();
@@ -384,6 +422,7 @@ add_test(function test_shouldReportError() {
errorHandler.dontIgnoreErrors = true;
Status.login = SERVER_MAINTENANCE;
do_check_true(errorHandler.shouldReportError());
+ do_check_false(errorHandler.didReportProlongedError);
// Test dontIgnoreErrors, prolonged, server maintenance,
// sync errors are reported
@@ -392,6 +431,7 @@ add_test(function test_shouldReportError() {
errorHandler.dontIgnoreErrors = true;
Status.sync = SERVER_MAINTENANCE;
do_check_true(errorHandler.shouldReportError());
+ do_check_false(errorHandler.didReportProlongedError);
// Test dontIgnoreErrors, prolonged, server maintenance,
// login errors are reported
@@ -400,14 +440,13 @@ add_test(function test_shouldReportError() {
errorHandler.dontIgnoreErrors = true;
Status.login = SERVER_MAINTENANCE;
do_check_true(errorHandler.shouldReportError());
-
- run_next_test();
+ do_check_false(errorHandler.didReportProlongedError);
});
-add_test(function test_shouldReportError_master_password() {
+add_identity_test(this, function test_shouldReportError_master_password() {
_("Test error ignored due to locked master password");
let server = sync_httpd_setup();
- setUp();
+ yield setUp(server);
// Monkey patch Service.verifyLogin to imitate
// master password being locked.
@@ -424,33 +463,57 @@ add_test(function test_shouldReportError_master_password() {
// Clean up.
Service.verifyLogin = Service._verifyLogin;
clean();
- server.stop(run_next_test);
+ let deferred = Promise.defer();
+ server.stop(deferred.resolve);
+ yield deferred.promise;
});
-add_test(function test_login_syncAndReportErrors_non_network_error() {
+// Test that even if we don't have a cluster URL, a login failure due to
+// authentication errors is always reported.
+add_identity_test(this, function test_shouldReportLoginFailureWithNoCluster() {
+ // Ensure no clusterURL - any error not specific to login should not be reported.
+ Service.serverURL = "";
+ Service.clusterURL = "";
+
+ // Test explicit "login rejected" state.
+ Status.resetSync();
+ // If we have a LOGIN_REJECTED state, we always report the error.
+ Status.login = LOGIN_FAILED_LOGIN_REJECTED;
+ do_check_true(errorHandler.shouldReportError());
+ // But any other status with a missing clusterURL is treated as a mid-sync
+ // 401 (ie, should be treated as a node reassignment)
+ Status.login = LOGIN_SUCCEEDED;
+ do_check_false(errorHandler.shouldReportError());
+});
+
+// XXX - how to arrange for 'Service.identity.basicPassword = null;' in
+// an fxaccounts environment?
+add_task(function test_login_syncAndReportErrors_non_network_error() {
// Test non-network errors are reported
// when calling syncAndReportErrors
let server = sync_httpd_setup();
- setUp();
+ yield setUp(server);
Service.identity.basicPassword = null;
+ let deferred = Promise.defer();
Svc.Obs.add("weave:ui:login:error", function onSyncError() {
Svc.Obs.remove("weave:ui:login:error", onSyncError);
do_check_eq(Status.login, LOGIN_FAILED_NO_PASSWORD);
clean();
- server.stop(run_next_test);
+ server.stop(deferred.resolve);
});
setLastSync(NON_PROLONGED_ERROR_DURATION);
errorHandler.syncAndReportErrors();
+ yield deferred.promise;
});
-add_test(function test_sync_syncAndReportErrors_non_network_error() {
+add_identity_test(this, function test_sync_syncAndReportErrors_non_network_error() {
// Test non-network errors are reported
// when calling syncAndReportErrors
let server = sync_httpd_setup();
- setUp();
+ yield setUp(server);
// By calling sync, we ensure we're logged in.
Service.sync();
@@ -459,42 +522,48 @@ add_test(function test_sync_syncAndReportErrors_non_network_error() {
generateCredentialsChangedFailure();
+ let deferred = Promise.defer();
Svc.Obs.add("weave:ui:sync:error", function onSyncError() {
Svc.Obs.remove("weave:ui:sync:error", onSyncError);
do_check_eq(Status.sync, CREDENTIALS_CHANGED);
clean();
- server.stop(run_next_test);
+ server.stop(deferred.resolve);
});
setLastSync(NON_PROLONGED_ERROR_DURATION);
errorHandler.syncAndReportErrors();
+ yield deferred.promise;
});
-add_test(function test_login_syncAndReportErrors_prolonged_non_network_error() {
+// XXX - how to arrange for 'Service.identity.basicPassword = null;' in
+// an fxaccounts environment?
+add_task(function test_login_syncAndReportErrors_prolonged_non_network_error() {
// Test prolonged, non-network errors are
// reported when calling syncAndReportErrors.
let server = sync_httpd_setup();
- setUp();
+ yield setUp(server);
Service.identity.basicPassword = null;
+ let deferred = Promise.defer();
Svc.Obs.add("weave:ui:login:error", function onSyncError() {
Svc.Obs.remove("weave:ui:login:error", onSyncError);
do_check_eq(Status.login, LOGIN_FAILED_NO_PASSWORD);
clean();
- server.stop(run_next_test);
+ server.stop(deferred.resolve);
});
setLastSync(PROLONGED_ERROR_DURATION);
errorHandler.syncAndReportErrors();
+ yield deferred.promise;
});
-add_test(function test_sync_syncAndReportErrors_prolonged_non_network_error() {
+add_identity_test(this, function test_sync_syncAndReportErrors_prolonged_non_network_error() {
// Test prolonged, non-network errors are
// reported when calling syncAndReportErrors.
let server = sync_httpd_setup();
- setUp();
+ yield setUp(server);
// By calling sync, we ensure we're logged in.
Service.sync();
@@ -503,34 +572,38 @@ add_test(function test_sync_syncAndReportErrors_prolonged_non_network_error() {
generateCredentialsChangedFailure();
+ let deferred = Promise.defer();
Svc.Obs.add("weave:ui:sync:error", function onSyncError() {
Svc.Obs.remove("weave:ui:sync:error", onSyncError);
do_check_eq(Status.sync, CREDENTIALS_CHANGED);
clean();
- server.stop(run_next_test);
+ server.stop(deferred.resolve);
});
setLastSync(PROLONGED_ERROR_DURATION);
errorHandler.syncAndReportErrors();
+ yield deferred.promise;
});
-add_test(function test_login_syncAndReportErrors_network_error() {
+add_identity_test(this, function test_login_syncAndReportErrors_network_error() {
// Test network errors are reported when calling syncAndReportErrors.
- setBasicCredentials("broken.wipe", "ilovejane", "abcdeabcdeabcdeabcdeabcdea");
- Service.serverURL = TEST_SERVER_URL;
- Service.clusterURL = TEST_CLUSTER_URL;
+ yield configureIdentity({username: "broken.wipe"});
+ Service.serverURL = FAKE_SERVER_URL;
+ Service.clusterURL = FAKE_SERVER_URL;
+ let deferred = Promise.defer();
Svc.Obs.add("weave:ui:login:error", function onSyncError() {
Svc.Obs.remove("weave:ui:login:error", onSyncError);
do_check_eq(Status.login, LOGIN_FAILED_NETWORK_ERROR);
clean();
- run_next_test();
+ deferred.resolve();
});
setLastSync(NON_PROLONGED_ERROR_DURATION);
errorHandler.syncAndReportErrors();
+ yield deferred.promise;
});
@@ -551,23 +624,26 @@ add_test(function test_sync_syncAndReportErrors_network_error() {
errorHandler.syncAndReportErrors();
});
-add_test(function test_login_syncAndReportErrors_prolonged_network_error() {
+add_identity_test(this, function test_login_syncAndReportErrors_prolonged_network_error() {
// Test prolonged, network errors are reported
// when calling syncAndReportErrors.
- setBasicCredentials("johndoe", "ilovejane", "abcdeabcdeabcdeabcdeabcdea");
- Service.serverURL = TEST_SERVER_URL;
- Service.clusterURL = TEST_CLUSTER_URL;
+ yield configureIdentity({username: "johndoe"});
+
+ Service.serverURL = FAKE_SERVER_URL;
+ Service.clusterURL = FAKE_SERVER_URL;
+ let deferred = Promise.defer();
Svc.Obs.add("weave:ui:login:error", function onSyncError() {
Svc.Obs.remove("weave:ui:login:error", onSyncError);
do_check_eq(Status.login, LOGIN_FAILED_NETWORK_ERROR);
clean();
- run_next_test();
+ deferred.resolve();
});
setLastSync(PROLONGED_ERROR_DURATION);
errorHandler.syncAndReportErrors();
+ yield deferred.promise;
});
add_test(function test_sync_syncAndReportErrors_prolonged_network_error() {
@@ -588,28 +664,31 @@ add_test(function test_sync_syncAndReportErrors_prolonged_network_error() {
errorHandler.syncAndReportErrors();
});
-add_test(function test_login_prolonged_non_network_error() {
+add_task(function test_login_prolonged_non_network_error() {
// Test prolonged, non-network errors are reported
let server = sync_httpd_setup();
- setUp();
+ yield setUp(server);
Service.identity.basicPassword = null;
+ let deferred = Promise.defer();
Svc.Obs.add("weave:ui:login:error", function onSyncError() {
Svc.Obs.remove("weave:ui:login:error", onSyncError);
do_check_eq(Status.sync, PROLONGED_SYNC_FAILURE);
+ do_check_true(errorHandler.didReportProlongedError);
clean();
- server.stop(run_next_test);
+ server.stop(deferred.resolve);
});
setLastSync(PROLONGED_ERROR_DURATION);
Service.sync();
+ yield deferred.promise;
});
-add_test(function test_sync_prolonged_non_network_error() {
+add_task(function test_sync_prolonged_non_network_error() {
// Test prolonged, non-network errors are reported
let server = sync_httpd_setup();
- setUp();
+ yield setUp(server);
// By calling sync, we ensure we're logged in.
Service.sync();
@@ -618,34 +697,40 @@ add_test(function test_sync_prolonged_non_network_error() {
generateCredentialsChangedFailure();
+ let deferred = Promise.defer();
Svc.Obs.add("weave:ui:sync:error", function onSyncError() {
Svc.Obs.remove("weave:ui:sync:error", onSyncError);
do_check_eq(Status.sync, PROLONGED_SYNC_FAILURE);
+ do_check_true(errorHandler.didReportProlongedError);
clean();
- server.stop(run_next_test);
+ server.stop(deferred.resolve);
});
setLastSync(PROLONGED_ERROR_DURATION);
Service.sync();
+ yield deferred.promise;
});
-add_test(function test_login_prolonged_network_error() {
+add_identity_test(this, function test_login_prolonged_network_error() {
// Test prolonged, network errors are reported
- setBasicCredentials("johndoe", "ilovejane", "abcdeabcdeabcdeabcdeabcdea");
- Service.serverURL = TEST_SERVER_URL;
- Service.clusterURL = TEST_CLUSTER_URL;
+ yield configureIdentity({username: "johndoe"});
+ Service.serverURL = FAKE_SERVER_URL;
+ Service.clusterURL = FAKE_SERVER_URL;
+ let deferred = Promise.defer();
Svc.Obs.add("weave:ui:login:error", function onSyncError() {
Svc.Obs.remove("weave:ui:login:error", onSyncError);
do_check_eq(Status.sync, PROLONGED_SYNC_FAILURE);
+ do_check_true(errorHandler.didReportProlongedError);
clean();
- run_next_test();
+ deferred.resolve();
});
setLastSync(PROLONGED_ERROR_DURATION);
Service.sync();
+ yield deferred.promise;
});
add_test(function test_sync_prolonged_network_error() {
@@ -655,6 +740,7 @@ add_test(function test_sync_prolonged_network_error() {
Svc.Obs.add("weave:ui:sync:error", function onSyncError() {
Svc.Obs.remove("weave:ui:sync:error", onSyncError);
do_check_eq(Status.sync, PROLONGED_SYNC_FAILURE);
+ do_check_true(errorHandler.didReportProlongedError);
Services.io.offline = false;
clean();
@@ -665,28 +751,31 @@ add_test(function test_sync_prolonged_network_error() {
Service.sync();
});
-add_test(function test_login_non_network_error() {
+add_task(function test_login_non_network_error() {
// Test non-network errors are reported
let server = sync_httpd_setup();
- setUp();
+ yield setUp(server);
Service.identity.basicPassword = null;
+ let deferred = Promise.defer();
Svc.Obs.add("weave:ui:login:error", function onSyncError() {
Svc.Obs.remove("weave:ui:login:error", onSyncError);
do_check_eq(Status.login, LOGIN_FAILED_NO_PASSWORD);
+ do_check_false(errorHandler.didReportProlongedError);
clean();
- server.stop(run_next_test);
+ server.stop(deferred.resolve);
});
setLastSync(NON_PROLONGED_ERROR_DURATION);
Service.sync();
+ yield deferred.promise;
});
-add_test(function test_sync_non_network_error() {
+add_task(function test_sync_non_network_error() {
// Test non-network errors are reported
let server = sync_httpd_setup();
- setUp();
+ yield setUp(server);
// By calling sync, we ensure we're logged in.
Service.sync();
@@ -695,36 +784,42 @@ add_test(function test_sync_non_network_error() {
generateCredentialsChangedFailure();
+ let deferred = Promise.defer();
Svc.Obs.add("weave:ui:sync:error", function onSyncError() {
Svc.Obs.remove("weave:ui:sync:error", onSyncError);
do_check_eq(Status.sync, CREDENTIALS_CHANGED);
+ do_check_false(errorHandler.didReportProlongedError);
clean();
- server.stop(run_next_test);
+ server.stop(deferred.resolve);
});
setLastSync(NON_PROLONGED_ERROR_DURATION);
Service.sync();
+ yield deferred.promise;
});
-add_test(function test_login_network_error() {
- setBasicCredentials("johndoe", "ilovejane", "abcdeabcdeabcdeabcdeabcdea");
- Service.serverURL = TEST_SERVER_URL;
- Service.clusterURL = TEST_CLUSTER_URL;
+add_identity_test(this, function test_login_network_error() {
+ yield configureIdentity({username: "johndoe"});
+ Service.serverURL = FAKE_SERVER_URL;
+ Service.clusterURL = FAKE_SERVER_URL;
+ let deferred = Promise.defer();
// Test network errors are not reported.
Svc.Obs.add("weave:ui:clear-error", function onClearError() {
Svc.Obs.remove("weave:ui:clear-error", onClearError);
do_check_eq(Status.login, LOGIN_FAILED_NETWORK_ERROR);
+ do_check_false(errorHandler.didReportProlongedError);
Services.io.offline = false;
clean();
- run_next_test();
+ deferred.resolve()
});
setLastSync(NON_PROLONGED_ERROR_DURATION);
Service.sync();
+ yield deferred.promise;
});
add_test(function test_sync_network_error() {
@@ -734,6 +829,7 @@ add_test(function test_sync_network_error() {
Svc.Obs.add("weave:ui:sync:finish", function onUIUpdate() {
Svc.Obs.remove("weave:ui:sync:finish", onUIUpdate);
do_check_eq(Status.sync, LOGIN_FAILED_NETWORK_ERROR);
+ do_check_false(errorHandler.didReportProlongedError);
Services.io.offline = false;
clean();
@@ -744,10 +840,10 @@ add_test(function test_sync_network_error() {
Service.sync();
});
-add_test(function test_sync_server_maintenance_error() {
+add_identity_test(this, function test_sync_server_maintenance_error() {
// Test server maintenance errors are not reported.
let server = sync_httpd_setup();
- setUp();
+ yield setUp(server);
const BACKOFF = 42;
let engine = engineManager.get("catapult");
@@ -762,30 +858,33 @@ add_test(function test_sync_server_maintenance_error() {
do_check_eq(Status.service, STATUS_OK);
+ let deferred = Promise.defer();
Svc.Obs.add("weave:ui:sync:finish", function onSyncFinish() {
Svc.Obs.remove("weave:ui:sync:finish", onSyncFinish);
do_check_eq(Status.service, SYNC_FAILED_PARTIAL);
do_check_eq(Status.sync, SERVER_MAINTENANCE);
+ do_check_false(errorHandler.didReportProlongedError);
Svc.Obs.remove("weave:ui:sync:error", onSyncError);
clean();
- server.stop(run_next_test);
+ server.stop(deferred.resolve);
});
setLastSync(NON_PROLONGED_ERROR_DURATION);
Service.sync();
+ yield deferred.promise;
});
-add_test(function test_info_collections_login_server_maintenance_error() {
+add_identity_test(this, function test_info_collections_login_server_maintenance_error() {
// Test info/collections server maintenance errors are not reported.
let server = sync_httpd_setup();
- setUp();
+ yield setUp(server);
Service.username = "broken.info";
- setBasicCredentials("broken.info", "irrelevant", "irrelevant");
- Service.serverURL = TEST_MAINTENANCE_URL;
- Service.clusterURL = TEST_MAINTENANCE_URL;
+ yield configureIdentity({username: "broken.info"});
+ Service.serverURL = server.baseURI + "/maintenance/";
+ Service.clusterURL = server.baseURI + "/maintenance/";
let backoffInterval;
Svc.Obs.add("weave:service:backoff:interval", function observe(subject, data) {
@@ -801,6 +900,7 @@ add_test(function test_info_collections_login_server_maintenance_error() {
do_check_false(Status.enforceBackoff);
do_check_eq(Status.service, STATUS_OK);
+ let deferred = Promise.defer();
Svc.Obs.add("weave:ui:clear-error", function onLoginFinish() {
Svc.Obs.remove("weave:ui:clear-error", onLoginFinish);
@@ -808,24 +908,26 @@ add_test(function test_info_collections_login_server_maintenance_error() {
do_check_eq(backoffInterval, 42);
do_check_eq(Status.service, LOGIN_FAILED);
do_check_eq(Status.login, SERVER_MAINTENANCE);
+ do_check_false(errorHandler.didReportProlongedError);
Svc.Obs.remove("weave:ui:login:error", onUIUpdate);
clean();
- server.stop(run_next_test);
+ server.stop(deferred.resolve);
});
setLastSync(NON_PROLONGED_ERROR_DURATION);
Service.sync();
+ yield deferred.promise;
});
-add_test(function test_meta_global_login_server_maintenance_error() {
+add_identity_test(this, function test_meta_global_login_server_maintenance_error() {
// Test meta/global server maintenance errors are not reported.
let server = sync_httpd_setup();
- setUp();
+ yield setUp(server);
- setBasicCredentials("broken.meta", "irrelevant", "irrelevant");
- Service.serverURL = TEST_MAINTENANCE_URL;
- Service.clusterURL = TEST_MAINTENANCE_URL;
+ yield configureIdentity({username: "broken.meta"});
+ Service.serverURL = server.baseURI + "/maintenance/";
+ Service.clusterURL = server.baseURI + "/maintenance/";
let backoffInterval;
Svc.Obs.add("weave:service:backoff:interval", function observe(subject, data) {
@@ -841,6 +943,7 @@ add_test(function test_meta_global_login_server_maintenance_error() {
do_check_false(Status.enforceBackoff);
do_check_eq(Status.service, STATUS_OK);
+ let deferred = Promise.defer();
Svc.Obs.add("weave:ui:clear-error", function onLoginFinish() {
Svc.Obs.remove("weave:ui:clear-error", onLoginFinish);
@@ -848,24 +951,26 @@ add_test(function test_meta_global_login_server_maintenance_error() {
do_check_eq(backoffInterval, 42);
do_check_eq(Status.service, LOGIN_FAILED);
do_check_eq(Status.login, SERVER_MAINTENANCE);
+ do_check_false(errorHandler.didReportProlongedError);
Svc.Obs.remove("weave:ui:login:error", onUIUpdate);
clean();
- server.stop(run_next_test);
+ server.stop(deferred.resolve);
});
setLastSync(NON_PROLONGED_ERROR_DURATION);
Service.sync();
+ yield deferred.promise;
});
-add_test(function test_crypto_keys_login_server_maintenance_error() {
+add_identity_test(this, function test_crypto_keys_login_server_maintenance_error() {
// Test crypto/keys server maintenance errors are not reported.
let server = sync_httpd_setup();
- setUp();
+ yield setUp(server);
- setBasicCredentials("broken.keys", "irrelevant", "irrelevant");
- Service.serverURL = TEST_MAINTENANCE_URL;
- Service.clusterURL = TEST_MAINTENANCE_URL;
+ yield configureIdentity({username: "broken.keys"});
+ Service.serverURL = server.baseURI + "/maintenance/";
+ Service.clusterURL = server.baseURI + "/maintenance/";
// Force re-download of keys
Service.collectionKeys.clear();
@@ -884,6 +989,7 @@ add_test(function test_crypto_keys_login_server_maintenance_error() {
do_check_false(Status.enforceBackoff);
do_check_eq(Status.service, STATUS_OK);
+ let deferred = Promise.defer();
Svc.Obs.add("weave:ui:clear-error", function onLoginFinish() {
Svc.Obs.remove("weave:ui:clear-error", onLoginFinish);
@@ -891,20 +997,22 @@ add_test(function test_crypto_keys_login_server_maintenance_error() {
do_check_eq(backoffInterval, 42);
do_check_eq(Status.service, LOGIN_FAILED);
do_check_eq(Status.login, SERVER_MAINTENANCE);
+ do_check_false(errorHandler.didReportProlongedError);
Svc.Obs.remove("weave:ui:login:error", onUIUpdate);
clean();
- server.stop(run_next_test);
+ server.stop(deferred.resolve);
});
setLastSync(NON_PROLONGED_ERROR_DURATION);
Service.sync();
+ yield deferred.promise;
});
-add_test(function test_sync_prolonged_server_maintenance_error() {
+add_task(function test_sync_prolonged_server_maintenance_error() {
// Test prolonged server maintenance errors are reported.
let server = sync_httpd_setup();
- setUp();
+ yield setUp(server);
const BACKOFF = 42;
let engine = engineManager.get("catapult");
@@ -912,29 +1020,32 @@ add_test(function test_sync_prolonged_server_maintenance_error() {
engine.exception = {status: 503,
headers: {"retry-after": BACKOFF}};
+ let deferred = Promise.defer();
Svc.Obs.add("weave:ui:sync:error", function onUIUpdate() {
Svc.Obs.remove("weave:ui:sync:error", onUIUpdate);
do_check_eq(Status.service, SYNC_FAILED);
do_check_eq(Status.sync, PROLONGED_SYNC_FAILURE);
+ do_check_true(errorHandler.didReportProlongedError);
clean();
- server.stop(run_next_test);
+ server.stop(deferred.resolve);
});
do_check_eq(Status.service, STATUS_OK);
setLastSync(PROLONGED_ERROR_DURATION);
Service.sync();
+ yield deferred.promise;
});
-add_test(function test_info_collections_login_prolonged_server_maintenance_error(){
+add_identity_test(this, function test_info_collections_login_prolonged_server_maintenance_error(){
// Test info/collections prolonged server maintenance errors are reported.
let server = sync_httpd_setup();
- setUp();
+ yield setUp(server);
- setBasicCredentials("broken.info", "irrelevant", "irrelevant");
- Service.serverURL = TEST_MAINTENANCE_URL;
- Service.clusterURL = TEST_MAINTENANCE_URL;
+ yield configureIdentity({username: "broken.info"});
+ Service.serverURL = server.baseURI + "/maintenance/";
+ Service.clusterURL = server.baseURI + "/maintenance/";
let backoffInterval;
Svc.Obs.add("weave:service:backoff:interval", function observe(subject, data) {
@@ -942,15 +1053,17 @@ add_test(function test_info_collections_login_prolonged_server_maintenance_error
backoffInterval = subject;
});
+ let deferred = Promise.defer();
Svc.Obs.add("weave:ui:login:error", function onUIUpdate() {
Svc.Obs.remove("weave:ui:login:error", onUIUpdate);
do_check_true(Status.enforceBackoff);
do_check_eq(backoffInterval, 42);
do_check_eq(Status.service, SYNC_FAILED);
do_check_eq(Status.sync, PROLONGED_SYNC_FAILURE);
+ do_check_true(errorHandler.didReportProlongedError);
clean();
- server.stop(run_next_test);
+ server.stop(deferred.resolve);
});
do_check_false(Status.enforceBackoff);
@@ -958,16 +1071,17 @@ add_test(function test_info_collections_login_prolonged_server_maintenance_error
setLastSync(PROLONGED_ERROR_DURATION);
Service.sync();
+ yield deferred.promise;
});
-add_test(function test_meta_global_login_prolonged_server_maintenance_error(){
+add_identity_test(this, function test_meta_global_login_prolonged_server_maintenance_error(){
// Test meta/global prolonged server maintenance errors are reported.
let server = sync_httpd_setup();
- setUp();
+ yield setUp(server);
- setBasicCredentials("broken.meta", "irrelevant", "irrelevant");
- Service.serverURL = TEST_MAINTENANCE_URL;
- Service.clusterURL = TEST_MAINTENANCE_URL;
+ yield configureIdentity({username: "broken.meta"});
+ Service.serverURL = server.baseURI + "/maintenance/";
+ Service.clusterURL = server.baseURI + "/maintenance/";
let backoffInterval;
Svc.Obs.add("weave:service:backoff:interval", function observe(subject, data) {
@@ -975,15 +1089,17 @@ add_test(function test_meta_global_login_prolonged_server_maintenance_error(){
backoffInterval = subject;
});
+ let deferred = Promise.defer();
Svc.Obs.add("weave:ui:login:error", function onUIUpdate() {
Svc.Obs.remove("weave:ui:login:error", onUIUpdate);
do_check_true(Status.enforceBackoff);
do_check_eq(backoffInterval, 42);
do_check_eq(Status.service, SYNC_FAILED);
do_check_eq(Status.sync, PROLONGED_SYNC_FAILURE);
+ do_check_true(errorHandler.didReportProlongedError);
clean();
- server.stop(run_next_test);
+ server.stop(deferred.resolve);
});
do_check_false(Status.enforceBackoff);
@@ -991,16 +1107,17 @@ add_test(function test_meta_global_login_prolonged_server_maintenance_error(){
setLastSync(PROLONGED_ERROR_DURATION);
Service.sync();
+ yield deferred.promise;
});
-add_test(function test_download_crypto_keys_login_prolonged_server_maintenance_error(){
+add_identity_test(this, function test_download_crypto_keys_login_prolonged_server_maintenance_error(){
// Test crypto/keys prolonged server maintenance errors are reported.
let server = sync_httpd_setup();
- setUp();
+ yield setUp(server);
- setBasicCredentials("broken.keys", "irrelevant", "irrelevant");
- Service.serverURL = TEST_MAINTENANCE_URL;
- Service.clusterURL = TEST_MAINTENANCE_URL;
+ yield configureIdentity({username: "broken.keys"});
+ Service.serverURL = server.baseURI + "/maintenance/";
+ Service.clusterURL = server.baseURI + "/maintenance/";
// Force re-download of keys
Service.collectionKeys.clear();
@@ -1010,15 +1127,17 @@ add_test(function test_download_crypto_keys_login_prolonged_server_maintenance_e
backoffInterval = subject;
});
+ let deferred = Promise.defer();
Svc.Obs.add("weave:ui:login:error", function onUIUpdate() {
Svc.Obs.remove("weave:ui:login:error", onUIUpdate);
do_check_true(Status.enforceBackoff);
do_check_eq(backoffInterval, 42);
do_check_eq(Status.service, SYNC_FAILED);
do_check_eq(Status.sync, PROLONGED_SYNC_FAILURE);
+ do_check_true(errorHandler.didReportProlongedError);
clean();
- server.stop(run_next_test);
+ server.stop(deferred.resolve);
});
do_check_false(Status.enforceBackoff);
@@ -1026,16 +1145,17 @@ add_test(function test_download_crypto_keys_login_prolonged_server_maintenance_e
setLastSync(PROLONGED_ERROR_DURATION);
Service.sync();
+ yield deferred.promise;
});
-add_test(function test_upload_crypto_keys_login_prolonged_server_maintenance_error(){
+add_identity_test(this, function test_upload_crypto_keys_login_prolonged_server_maintenance_error(){
// Test crypto/keys prolonged server maintenance errors are reported.
let server = sync_httpd_setup();
// Start off with an empty account, do not upload a key.
- setBasicCredentials("broken.keys", "ilovejane", "abcdeabcdeabcdeabcdeabcdea");
- Service.serverURL = TEST_MAINTENANCE_URL;
- Service.clusterURL = TEST_MAINTENANCE_URL;
+ yield configureIdentity({username: "broken.keys"});
+ Service.serverURL = server.baseURI + "/maintenance/";
+ Service.clusterURL = server.baseURI + "/maintenance/";
let backoffInterval;
Svc.Obs.add("weave:service:backoff:interval", function observe(subject, data) {
@@ -1043,15 +1163,17 @@ add_test(function test_upload_crypto_keys_login_prolonged_server_maintenance_err
backoffInterval = subject;
});
+ let deferred = Promise.defer();
Svc.Obs.add("weave:ui:login:error", function onUIUpdate() {
Svc.Obs.remove("weave:ui:login:error", onUIUpdate);
do_check_true(Status.enforceBackoff);
do_check_eq(backoffInterval, 42);
do_check_eq(Status.service, SYNC_FAILED);
do_check_eq(Status.sync, PROLONGED_SYNC_FAILURE);
+ do_check_true(errorHandler.didReportProlongedError);
clean();
- server.stop(run_next_test);
+ server.stop(deferred.resolve);
});
do_check_false(Status.enforceBackoff);
@@ -1059,17 +1181,18 @@ add_test(function test_upload_crypto_keys_login_prolonged_server_maintenance_err
setLastSync(PROLONGED_ERROR_DURATION);
Service.sync();
+ yield deferred.promise;
});
-add_test(function test_wipeServer_login_prolonged_server_maintenance_error(){
+add_identity_test(this, function test_wipeServer_login_prolonged_server_maintenance_error(){
// Test that we report prolonged server maintenance errors that occur whilst
// wiping the server.
let server = sync_httpd_setup();
// Start off with an empty account, do not upload a key.
- setBasicCredentials("broken.wipe", "ilovejane", "abcdeabcdeabcdeabcdeabcdea");
- Service.serverURL = TEST_MAINTENANCE_URL;
- Service.clusterURL = TEST_MAINTENANCE_URL;
+ yield configureIdentity({username: "broken.wipe"});
+ Service.serverURL = server.baseURI + "/maintenance/";
+ Service.clusterURL = server.baseURI + "/maintenance/";
let backoffInterval;
Svc.Obs.add("weave:service:backoff:interval", function observe(subject, data) {
@@ -1077,15 +1200,17 @@ add_test(function test_wipeServer_login_prolonged_server_maintenance_error(){
backoffInterval = subject;
});
+ let deferred = Promise.defer();
Svc.Obs.add("weave:ui:login:error", function onUIUpdate() {
Svc.Obs.remove("weave:ui:login:error", onUIUpdate);
do_check_true(Status.enforceBackoff);
do_check_eq(backoffInterval, 42);
do_check_eq(Status.service, SYNC_FAILED);
do_check_eq(Status.sync, PROLONGED_SYNC_FAILURE);
+ do_check_true(errorHandler.didReportProlongedError);
clean();
- server.stop(run_next_test);
+ server.stop(deferred.resolve);
});
do_check_false(Status.enforceBackoff);
@@ -1093,17 +1218,18 @@ add_test(function test_wipeServer_login_prolonged_server_maintenance_error(){
setLastSync(PROLONGED_ERROR_DURATION);
Service.sync();
+ yield deferred.promise;
});
-add_test(function test_wipeRemote_prolonged_server_maintenance_error(){
+add_identity_test(this, function test_wipeRemote_prolonged_server_maintenance_error(){
// Test that we report prolonged server maintenance errors that occur whilst
// wiping all remote devices.
let server = sync_httpd_setup();
server.registerPathHandler("/1.1/broken.wipe/storage/catapult", service_unavailable);
- setBasicCredentials("broken.wipe", "ilovejane", "abcdeabcdeabcdeabcdeabcdea");
- Service.serverURL = TEST_MAINTENANCE_URL;
- Service.clusterURL = TEST_MAINTENANCE_URL;
+ yield configureIdentity({username: "broken.wipe"});
+ Service.serverURL = server.baseURI + "/maintenance/";
+ Service.clusterURL = server.baseURI + "/maintenance/";
generateAndUploadKeys();
let engine = engineManager.get("catapult");
@@ -1116,6 +1242,7 @@ add_test(function test_wipeRemote_prolonged_server_maintenance_error(){
backoffInterval = subject;
});
+ let deferred = Promise.defer();
Svc.Obs.add("weave:ui:sync:error", function onUIUpdate() {
Svc.Obs.remove("weave:ui:sync:error", onUIUpdate);
do_check_true(Status.enforceBackoff);
@@ -1123,9 +1250,10 @@ add_test(function test_wipeRemote_prolonged_server_maintenance_error(){
do_check_eq(Status.service, SYNC_FAILED);
do_check_eq(Status.sync, PROLONGED_SYNC_FAILURE);
do_check_eq(Svc.Prefs.get("firstSync"), "wipeRemote");
+ do_check_true(errorHandler.didReportProlongedError);
clean();
- server.stop(run_next_test);
+ server.stop(deferred.resolve);
});
do_check_false(Status.enforceBackoff);
@@ -1134,13 +1262,14 @@ add_test(function test_wipeRemote_prolonged_server_maintenance_error(){
Svc.Prefs.set("firstSync", "wipeRemote");
setLastSync(PROLONGED_ERROR_DURATION);
Service.sync();
+ yield deferred.promise;
});
-add_test(function test_sync_syncAndReportErrors_server_maintenance_error() {
+add_task(function test_sync_syncAndReportErrors_server_maintenance_error() {
// Test server maintenance errors are reported
// when calling syncAndReportErrors.
let server = sync_httpd_setup();
- setUp();
+ yield setUp(server);
const BACKOFF = 42;
let engine = engineManager.get("catapult");
@@ -1148,30 +1277,33 @@ add_test(function test_sync_syncAndReportErrors_server_maintenance_error() {
engine.exception = {status: 503,
headers: {"retry-after": BACKOFF}};
+ let deferred = Promise.defer();
Svc.Obs.add("weave:ui:sync:error", function onUIUpdate() {
Svc.Obs.remove("weave:ui:sync:error", onUIUpdate);
do_check_eq(Status.service, SYNC_FAILED_PARTIAL);
do_check_eq(Status.sync, SERVER_MAINTENANCE);
+ do_check_false(errorHandler.didReportProlongedError);
clean();
- server.stop(run_next_test);
+ server.stop(deferred.resolve);
});
do_check_eq(Status.service, STATUS_OK);
setLastSync(NON_PROLONGED_ERROR_DURATION);
errorHandler.syncAndReportErrors();
+ yield deferred.promise;
});
-add_test(function test_info_collections_login_syncAndReportErrors_server_maintenance_error() {
+add_identity_test(this, function test_info_collections_login_syncAndReportErrors_server_maintenance_error() {
// Test info/collections server maintenance errors are reported
// when calling syncAndReportErrors.
let server = sync_httpd_setup();
- setUp();
+ yield setUp(server);
- setBasicCredentials("broken.info", "irrelevant", "irrelevant");
- Service.serverURL = TEST_MAINTENANCE_URL;
- Service.clusterURL = TEST_MAINTENANCE_URL;
+ yield configureIdentity({username: "broken.info"});
+ Service.serverURL = server.baseURI + "/maintenance/";
+ Service.clusterURL = server.baseURI + "/maintenance/";
let backoffInterval;
Svc.Obs.add("weave:service:backoff:interval", function observe(subject, data) {
@@ -1179,15 +1311,17 @@ add_test(function test_info_collections_login_syncAndReportErrors_server_mainten
backoffInterval = subject;
});
+ let deferred = Promise.defer();
Svc.Obs.add("weave:ui:login:error", function onUIUpdate() {
Svc.Obs.remove("weave:ui:login:error", onUIUpdate);
do_check_true(Status.enforceBackoff);
do_check_eq(backoffInterval, 42);
do_check_eq(Status.service, LOGIN_FAILED);
do_check_eq(Status.login, SERVER_MAINTENANCE);
+ do_check_false(errorHandler.didReportProlongedError);
clean();
- server.stop(run_next_test);
+ server.stop(deferred.resolve);
});
do_check_false(Status.enforceBackoff);
@@ -1195,17 +1329,18 @@ add_test(function test_info_collections_login_syncAndReportErrors_server_mainten
setLastSync(NON_PROLONGED_ERROR_DURATION);
errorHandler.syncAndReportErrors();
+ yield deferred.promise;
});
-add_test(function test_meta_global_login_syncAndReportErrors_server_maintenance_error() {
+add_identity_test(this, function test_meta_global_login_syncAndReportErrors_server_maintenance_error() {
// Test meta/global server maintenance errors are reported
// when calling syncAndReportErrors.
let server = sync_httpd_setup();
- setUp();
+ yield setUp(server);
- setBasicCredentials("broken.meta", "irrelevant", "irrelevant");
- Service.serverURL = TEST_MAINTENANCE_URL;
- Service.clusterURL = TEST_MAINTENANCE_URL;
+ yield configureIdentity({username: "broken.meta"});
+ Service.serverURL = server.baseURI + "/maintenance/";
+ Service.clusterURL = server.baseURI + "/maintenance/";
let backoffInterval;
Svc.Obs.add("weave:service:backoff:interval", function observe(subject, data) {
@@ -1213,15 +1348,17 @@ add_test(function test_meta_global_login_syncAndReportErrors_server_maintenance_
backoffInterval = subject;
});
+ let deferred = Promise.defer();
Svc.Obs.add("weave:ui:login:error", function onUIUpdate() {
Svc.Obs.remove("weave:ui:login:error", onUIUpdate);
do_check_true(Status.enforceBackoff);
do_check_eq(backoffInterval, 42);
do_check_eq(Status.service, LOGIN_FAILED);
do_check_eq(Status.login, SERVER_MAINTENANCE);
+ do_check_false(errorHandler.didReportProlongedError);
clean();
- server.stop(run_next_test);
+ server.stop(deferred.resolve);
});
do_check_false(Status.enforceBackoff);
@@ -1229,17 +1366,18 @@ add_test(function test_meta_global_login_syncAndReportErrors_server_maintenance_
setLastSync(NON_PROLONGED_ERROR_DURATION);
errorHandler.syncAndReportErrors();
+ yield deferred.promise;
});
-add_test(function test_download_crypto_keys_login_syncAndReportErrors_server_maintenance_error() {
+add_identity_test(this, function test_download_crypto_keys_login_syncAndReportErrors_server_maintenance_error() {
// Test crypto/keys server maintenance errors are reported
// when calling syncAndReportErrors.
let server = sync_httpd_setup();
- setUp();
+ yield setUp(server);
- setBasicCredentials("broken.keys", "irrelevant", "irrelevant");
- Service.serverURL = TEST_MAINTENANCE_URL;
- Service.clusterURL = TEST_MAINTENANCE_URL;
+ yield configureIdentity({username: "broken.keys"});
+ Service.serverURL = server.baseURI + "/maintenance/";
+ Service.clusterURL = server.baseURI + "/maintenance/";
// Force re-download of keys
Service.collectionKeys.clear();
@@ -1249,15 +1387,17 @@ add_test(function test_download_crypto_keys_login_syncAndReportErrors_server_mai
backoffInterval = subject;
});
+ let deferred = Promise.defer();
Svc.Obs.add("weave:ui:login:error", function onUIUpdate() {
Svc.Obs.remove("weave:ui:login:error", onUIUpdate);
do_check_true(Status.enforceBackoff);
do_check_eq(backoffInterval, 42);
do_check_eq(Status.service, LOGIN_FAILED);
do_check_eq(Status.login, SERVER_MAINTENANCE);
+ do_check_false(errorHandler.didReportProlongedError);
clean();
- server.stop(run_next_test);
+ server.stop(deferred.resolve);
});
do_check_false(Status.enforceBackoff);
@@ -1265,17 +1405,18 @@ add_test(function test_download_crypto_keys_login_syncAndReportErrors_server_mai
setLastSync(NON_PROLONGED_ERROR_DURATION);
errorHandler.syncAndReportErrors();
+ yield deferred.promise;
});
-add_test(function test_upload_crypto_keys_login_syncAndReportErrors_server_maintenance_error() {
+add_identity_test(this, function test_upload_crypto_keys_login_syncAndReportErrors_server_maintenance_error() {
// Test crypto/keys server maintenance errors are reported
// when calling syncAndReportErrors.
let server = sync_httpd_setup();
// Start off with an empty account, do not upload a key.
- setBasicCredentials("broken.keys", "ilovejane", "abcdeabcdeabcdeabcdeabcdea");
- Service.serverURL = TEST_MAINTENANCE_URL;
- Service.clusterURL = TEST_MAINTENANCE_URL;
+ yield configureIdentity({username: "broken.keys"});
+ Service.serverURL = server.baseURI + "/maintenance/";
+ Service.clusterURL = server.baseURI + "/maintenance/";
let backoffInterval;
Svc.Obs.add("weave:service:backoff:interval", function observe(subject, data) {
@@ -1283,15 +1424,17 @@ add_test(function test_upload_crypto_keys_login_syncAndReportErrors_server_maint
backoffInterval = subject;
});
+ let deferred = Promise.defer();
Svc.Obs.add("weave:ui:login:error", function onUIUpdate() {
Svc.Obs.remove("weave:ui:login:error", onUIUpdate);
do_check_true(Status.enforceBackoff);
do_check_eq(backoffInterval, 42);
do_check_eq(Status.service, LOGIN_FAILED);
do_check_eq(Status.login, SERVER_MAINTENANCE);
+ do_check_false(errorHandler.didReportProlongedError);
clean();
- server.stop(run_next_test);
+ server.stop(deferred.resolve);
});
do_check_false(Status.enforceBackoff);
@@ -1299,17 +1442,18 @@ add_test(function test_upload_crypto_keys_login_syncAndReportErrors_server_maint
setLastSync(NON_PROLONGED_ERROR_DURATION);
errorHandler.syncAndReportErrors();
+ yield deferred.promise;
});
-add_test(function test_wipeServer_login_syncAndReportErrors_server_maintenance_error() {
+add_identity_test(this, function test_wipeServer_login_syncAndReportErrors_server_maintenance_error() {
// Test crypto/keys server maintenance errors are reported
// when calling syncAndReportErrors.
let server = sync_httpd_setup();
// Start off with an empty account, do not upload a key.
- setBasicCredentials("broken.wipe", "ilovejane", "abcdeabcdeabcdeabcdeabcdea");
- Service.serverURL = TEST_MAINTENANCE_URL;
- Service.clusterURL = TEST_MAINTENANCE_URL;
+ yield configureIdentity({username: "broken.wipe"});
+ Service.serverURL = server.baseURI + "/maintenance/";
+ Service.clusterURL = server.baseURI + "/maintenance/";
let backoffInterval;
Svc.Obs.add("weave:service:backoff:interval", function observe(subject, data) {
@@ -1317,15 +1461,17 @@ add_test(function test_wipeServer_login_syncAndReportErrors_server_maintenance_e
backoffInterval = subject;
});
+ let deferred = Promise.defer();
Svc.Obs.add("weave:ui:login:error", function onUIUpdate() {
Svc.Obs.remove("weave:ui:login:error", onUIUpdate);
do_check_true(Status.enforceBackoff);
do_check_eq(backoffInterval, 42);
do_check_eq(Status.service, LOGIN_FAILED);
do_check_eq(Status.login, SERVER_MAINTENANCE);
+ do_check_false(errorHandler.didReportProlongedError);
clean();
- server.stop(run_next_test);
+ server.stop(deferred.resolve);
});
do_check_false(Status.enforceBackoff);
@@ -1333,16 +1479,17 @@ add_test(function test_wipeServer_login_syncAndReportErrors_server_maintenance_e
setLastSync(NON_PROLONGED_ERROR_DURATION);
errorHandler.syncAndReportErrors();
+ yield deferred.promise;
});
-add_test(function test_wipeRemote_syncAndReportErrors_server_maintenance_error(){
+add_identity_test(this, function test_wipeRemote_syncAndReportErrors_server_maintenance_error(){
// Test that we report prolonged server maintenance errors that occur whilst
// wiping all remote devices.
let server = sync_httpd_setup();
- setBasicCredentials("broken.wipe", "ilovejane", "abcdeabcdeabcdeabcdeabcdea");
- Service.serverURL = TEST_MAINTENANCE_URL;
- Service.clusterURL = TEST_MAINTENANCE_URL;
+ yield configureIdentity({username: "broken.wipe"});
+ Service.serverURL = server.baseURI + "/maintenance/";
+ Service.clusterURL = server.baseURI + "/maintenance/";
generateAndUploadKeys();
let engine = engineManager.get("catapult");
@@ -1355,6 +1502,7 @@ add_test(function test_wipeRemote_syncAndReportErrors_server_maintenance_error()
backoffInterval = subject;
});
+ let deferred = Promise.defer();
Svc.Obs.add("weave:ui:sync:error", function onUIUpdate() {
Svc.Obs.remove("weave:ui:sync:error", onUIUpdate);
do_check_true(Status.enforceBackoff);
@@ -1362,9 +1510,10 @@ add_test(function test_wipeRemote_syncAndReportErrors_server_maintenance_error()
do_check_eq(Status.service, SYNC_FAILED);
do_check_eq(Status.sync, SERVER_MAINTENANCE);
do_check_eq(Svc.Prefs.get("firstSync"), "wipeRemote");
+ do_check_false(errorHandler.didReportProlongedError);
clean();
- server.stop(run_next_test);
+ server.stop(deferred.resolve);
});
do_check_false(Status.enforceBackoff);
@@ -1373,13 +1522,14 @@ add_test(function test_wipeRemote_syncAndReportErrors_server_maintenance_error()
Svc.Prefs.set("firstSync", "wipeRemote");
setLastSync(NON_PROLONGED_ERROR_DURATION);
errorHandler.syncAndReportErrors();
+ yield deferred.promise;
});
-add_test(function test_sync_syncAndReportErrors_prolonged_server_maintenance_error() {
+add_task(function test_sync_syncAndReportErrors_prolonged_server_maintenance_error() {
// Test prolonged server maintenance errors are
// reported when calling syncAndReportErrors.
let server = sync_httpd_setup();
- setUp();
+ yield setUp(server);
const BACKOFF = 42;
let engine = engineManager.get("catapult");
@@ -1387,30 +1537,35 @@ add_test(function test_sync_syncAndReportErrors_prolonged_server_maintenance_err
engine.exception = {status: 503,
headers: {"retry-after": BACKOFF}};
+ let deferred = Promise.defer();
Svc.Obs.add("weave:ui:sync:error", function onUIUpdate() {
Svc.Obs.remove("weave:ui:sync:error", onUIUpdate);
do_check_eq(Status.service, SYNC_FAILED_PARTIAL);
do_check_eq(Status.sync, SERVER_MAINTENANCE);
+ // syncAndReportErrors means dontIgnoreErrors, which means
+ // didReportProlongedError not touched.
+ do_check_false(errorHandler.didReportProlongedError);
clean();
- server.stop(run_next_test);
+ server.stop(deferred.resolve);
});
do_check_eq(Status.service, STATUS_OK);
setLastSync(PROLONGED_ERROR_DURATION);
errorHandler.syncAndReportErrors();
+ yield deferred.promise;
});
-add_test(function test_info_collections_login_syncAndReportErrors_prolonged_server_maintenance_error() {
+add_identity_test(this, function test_info_collections_login_syncAndReportErrors_prolonged_server_maintenance_error() {
// Test info/collections server maintenance errors are reported
// when calling syncAndReportErrors.
let server = sync_httpd_setup();
- setUp();
+ yield setUp(server);
- setBasicCredentials("broken.info", "irrelevant", "irrelevant");
- Service.serverURL = TEST_MAINTENANCE_URL;
- Service.clusterURL = TEST_MAINTENANCE_URL;
+ yield configureIdentity({username: "broken.info"});
+ Service.serverURL = server.baseURI + "/maintenance/";
+ Service.clusterURL = server.baseURI + "/maintenance/";
let backoffInterval;
Svc.Obs.add("weave:service:backoff:interval", function observe(subject, data) {
@@ -1418,15 +1573,19 @@ add_test(function test_info_collections_login_syncAndReportErrors_prolonged_serv
backoffInterval = subject;
});
+ let deferred = Promise.defer();
Svc.Obs.add("weave:ui:login:error", function onUIUpdate() {
Svc.Obs.remove("weave:ui:login:error", onUIUpdate);
do_check_true(Status.enforceBackoff);
do_check_eq(backoffInterval, 42);
do_check_eq(Status.service, LOGIN_FAILED);
do_check_eq(Status.login, SERVER_MAINTENANCE);
+ // syncAndReportErrors means dontIgnoreErrors, which means
+ // didReportProlongedError not touched.
+ do_check_false(errorHandler.didReportProlongedError);
clean();
- server.stop(run_next_test);
+ server.stop(deferred.resolve);
});
do_check_false(Status.enforceBackoff);
@@ -1434,17 +1593,18 @@ add_test(function test_info_collections_login_syncAndReportErrors_prolonged_serv
setLastSync(PROLONGED_ERROR_DURATION);
errorHandler.syncAndReportErrors();
+ yield deferred.promise;
});
-add_test(function test_meta_global_login_syncAndReportErrors_prolonged_server_maintenance_error() {
+add_identity_test(this, function test_meta_global_login_syncAndReportErrors_prolonged_server_maintenance_error() {
// Test meta/global server maintenance errors are reported
// when calling syncAndReportErrors.
let server = sync_httpd_setup();
- setUp();
+ yield setUp(server);
- setBasicCredentials("broken.meta", "irrelevant", "irrelevant");
- Service.serverURL = TEST_MAINTENANCE_URL;
- Service.clusterURL = TEST_MAINTENANCE_URL;
+ yield configureIdentity({username: "broken.meta"});
+ Service.serverURL = server.baseURI + "/maintenance/";
+ Service.clusterURL = server.baseURI + "/maintenance/";
let backoffInterval;
Svc.Obs.add("weave:service:backoff:interval", function observe(subject, data) {
@@ -1452,15 +1612,19 @@ add_test(function test_meta_global_login_syncAndReportErrors_prolonged_server_ma
backoffInterval = subject;
});
+ let deferred = Promise.defer();
Svc.Obs.add("weave:ui:login:error", function onUIUpdate() {
Svc.Obs.remove("weave:ui:login:error", onUIUpdate);
do_check_true(Status.enforceBackoff);
do_check_eq(backoffInterval, 42);
do_check_eq(Status.service, LOGIN_FAILED);
do_check_eq(Status.login, SERVER_MAINTENANCE);
+ // syncAndReportErrors means dontIgnoreErrors, which means
+ // didReportProlongedError not touched.
+ do_check_false(errorHandler.didReportProlongedError);
clean();
- server.stop(run_next_test);
+ server.stop(deferred.resolve);
});
do_check_false(Status.enforceBackoff);
@@ -1468,17 +1632,18 @@ add_test(function test_meta_global_login_syncAndReportErrors_prolonged_server_ma
setLastSync(PROLONGED_ERROR_DURATION);
errorHandler.syncAndReportErrors();
+ yield deferred.promise;
});
-add_test(function test_download_crypto_keys_login_syncAndReportErrors_prolonged_server_maintenance_error() {
+add_identity_test(this, function test_download_crypto_keys_login_syncAndReportErrors_prolonged_server_maintenance_error() {
// Test crypto/keys server maintenance errors are reported
// when calling syncAndReportErrors.
let server = sync_httpd_setup();
- setUp();
+ yield setUp(server);
- setBasicCredentials("broken.keys", "irrelevant", "irrelevant");
- Service.serverURL = TEST_MAINTENANCE_URL;
- Service.clusterURL = TEST_MAINTENANCE_URL;
+ yield configureIdentity({username: "broken.keys"});
+ Service.serverURL = server.baseURI + "/maintenance/";
+ Service.clusterURL = server.baseURI + "/maintenance/";
// Force re-download of keys
Service.collectionKeys.clear();
@@ -1488,15 +1653,19 @@ add_test(function test_download_crypto_keys_login_syncAndReportErrors_prolonged_
backoffInterval = subject;
});
+ let deferred = Promise.defer();
Svc.Obs.add("weave:ui:login:error", function onUIUpdate() {
Svc.Obs.remove("weave:ui:login:error", onUIUpdate);
do_check_true(Status.enforceBackoff);
do_check_eq(backoffInterval, 42);
do_check_eq(Status.service, LOGIN_FAILED);
do_check_eq(Status.login, SERVER_MAINTENANCE);
+ // syncAndReportErrors means dontIgnoreErrors, which means
+ // didReportProlongedError not touched.
+ do_check_false(errorHandler.didReportProlongedError);
clean();
- server.stop(run_next_test);
+ server.stop(deferred.resolve);
});
do_check_false(Status.enforceBackoff);
@@ -1504,17 +1673,18 @@ add_test(function test_download_crypto_keys_login_syncAndReportErrors_prolonged_
setLastSync(PROLONGED_ERROR_DURATION);
errorHandler.syncAndReportErrors();
+ yield deferred.promise;
});
-add_test(function test_upload_crypto_keys_login_syncAndReportErrors_prolonged_server_maintenance_error() {
+add_identity_test(this, function test_upload_crypto_keys_login_syncAndReportErrors_prolonged_server_maintenance_error() {
// Test crypto/keys server maintenance errors are reported
// when calling syncAndReportErrors.
let server = sync_httpd_setup();
// Start off with an empty account, do not upload a key.
- setBasicCredentials("broken.keys", "ilovejane", "abcdeabcdeabcdeabcdeabcdea");
- Service.serverURL = TEST_MAINTENANCE_URL;
- Service.clusterURL = TEST_MAINTENANCE_URL;
+ yield configureIdentity({username: "broken.keys"});
+ Service.serverURL = server.baseURI + "/maintenance/";
+ Service.clusterURL = server.baseURI + "/maintenance/";
let backoffInterval;
Svc.Obs.add("weave:service:backoff:interval", function observe(subject, data) {
@@ -1522,15 +1692,19 @@ add_test(function test_upload_crypto_keys_login_syncAndReportErrors_prolonged_se
backoffInterval = subject;
});
+ let deferred = Promise.defer();
Svc.Obs.add("weave:ui:login:error", function onUIUpdate() {
Svc.Obs.remove("weave:ui:login:error", onUIUpdate);
do_check_true(Status.enforceBackoff);
do_check_eq(backoffInterval, 42);
do_check_eq(Status.service, LOGIN_FAILED);
do_check_eq(Status.login, SERVER_MAINTENANCE);
+ // syncAndReportErrors means dontIgnoreErrors, which means
+ // didReportProlongedError not touched.
+ do_check_false(errorHandler.didReportProlongedError);
clean();
- server.stop(run_next_test);
+ server.stop(deferred.resolve);
});
do_check_false(Status.enforceBackoff);
@@ -1538,17 +1712,18 @@ add_test(function test_upload_crypto_keys_login_syncAndReportErrors_prolonged_se
setLastSync(PROLONGED_ERROR_DURATION);
errorHandler.syncAndReportErrors();
+ yield deferred.promise;
});
-add_test(function test_wipeServer_login_syncAndReportErrors_prolonged_server_maintenance_error() {
+add_identity_test(this, function test_wipeServer_login_syncAndReportErrors_prolonged_server_maintenance_error() {
// Test crypto/keys server maintenance errors are reported
// when calling syncAndReportErrors.
let server = sync_httpd_setup();
// Start off with an empty account, do not upload a key.
- setBasicCredentials("broken.wipe", "ilovejane", "abcdeabcdeabcdeabcdeabcdea");
- Service.serverURL = TEST_MAINTENANCE_URL;
- Service.clusterURL = TEST_MAINTENANCE_URL;
+ yield configureIdentity({username: "broken.wipe"});
+ Service.serverURL = server.baseURI + "/maintenance/";
+ Service.clusterURL = server.baseURI + "/maintenance/";
let backoffInterval;
Svc.Obs.add("weave:service:backoff:interval", function observe(subject, data) {
@@ -1556,15 +1731,19 @@ add_test(function test_wipeServer_login_syncAndReportErrors_prolonged_server_mai
backoffInterval = subject;
});
+ let deferred = Promise.defer();
Svc.Obs.add("weave:ui:login:error", function onUIUpdate() {
Svc.Obs.remove("weave:ui:login:error", onUIUpdate);
do_check_true(Status.enforceBackoff);
do_check_eq(backoffInterval, 42);
do_check_eq(Status.service, LOGIN_FAILED);
do_check_eq(Status.login, SERVER_MAINTENANCE);
+ // syncAndReportErrors means dontIgnoreErrors, which means
+ // didReportProlongedError not touched.
+ do_check_false(errorHandler.didReportProlongedError);
clean();
- server.stop(run_next_test);
+ server.stop(deferred.resolve);
});
do_check_false(Status.enforceBackoff);
@@ -1572,9 +1751,10 @@ add_test(function test_wipeServer_login_syncAndReportErrors_prolonged_server_mai
setLastSync(PROLONGED_ERROR_DURATION);
errorHandler.syncAndReportErrors();
+ yield deferred.promise;
});
-add_test(function test_sync_engine_generic_fail() {
+add_task(function test_sync_engine_generic_fail() {
let server = sync_httpd_setup();
let engine = engineManager.get("catapult");
@@ -1583,11 +1763,12 @@ add_test(function test_sync_engine_generic_fail() {
Svc.Obs.notify("weave:engine:sync:error", "", "catapult");
};
- let log = Log4Moz.repository.getLogger("Sync.ErrorHandler");
+ let log = Log.repository.getLogger("Sync.ErrorHandler");
Svc.Prefs.set("log.appender.file.logOnError", true);
do_check_eq(Status.engines["catapult"], undefined);
+ let deferred = Promise.defer();
// Don't wait for reset-file-log until the sync is underway.
// This avoids us catching a delayed notification from an earlier test.
Svc.Obs.add("weave:engine:sync:finish", function onEngineFinish() {
@@ -1607,23 +1788,23 @@ add_test(function test_sync_engine_generic_fail() {
let entries = logsdir.directoryEntries;
do_check_true(entries.hasMoreElements());
let logfile = entries.getNext().QueryInterface(Ci.nsILocalFile);
- do_check_eq(logfile.leafName.slice(0, LOG_PREFIX_ERROR.length),
- LOG_PREFIX_ERROR);
+ do_check_true(logfile.leafName.startsWith("error-sync-"), logfile.leafName);
clean();
- server.stop(run_next_test);
+ server.stop(deferred.resolve);
});
});
- do_check_true(setUp());
+ do_check_true(yield setUp(server));
Service.sync();
+ yield deferred.promise;
});
add_test(function test_logs_on_sync_error_despite_shouldReportError() {
_("Ensure that an error is still logged when weave:service:sync:error " +
"is notified, despite shouldReportError returning false.");
- let log = Log4Moz.repository.getLogger("Sync.ErrorHandler");
+ let log = Log.repository.getLogger("Sync.ErrorHandler");
Svc.Prefs.set("log.appender.file.logOnError", true);
log.info("TESTING");
@@ -1638,8 +1819,7 @@ add_test(function test_logs_on_sync_error_despite_shouldReportError() {
let entries = logsdir.directoryEntries;
do_check_true(entries.hasMoreElements());
let logfile = entries.getNext().QueryInterface(Ci.nsILocalFile);
- do_check_eq(logfile.leafName.slice(0, LOG_PREFIX_ERROR.length),
- LOG_PREFIX_ERROR);
+ do_check_true(logfile.leafName.startsWith("error-sync-"), logfile.leafName);
clean();
run_next_test();
@@ -1651,7 +1831,7 @@ add_test(function test_logs_on_login_error_despite_shouldReportError() {
_("Ensure that an error is still logged when weave:service:login:error " +
"is notified, despite shouldReportError returning false.");
- let log = Log4Moz.repository.getLogger("Sync.ErrorHandler");
+ let log = Log.repository.getLogger("Sync.ErrorHandler");
Svc.Prefs.set("log.appender.file.logOnError", true);
log.info("TESTING");
@@ -1666,8 +1846,7 @@ add_test(function test_logs_on_login_error_despite_shouldReportError() {
let entries = logsdir.directoryEntries;
do_check_true(entries.hasMoreElements());
let logfile = entries.getNext().QueryInterface(Ci.nsILocalFile);
- do_check_eq(logfile.leafName.slice(0, LOG_PREFIX_ERROR.length),
- LOG_PREFIX_ERROR);
+ do_check_true(logfile.leafName.startsWith("error-sync-"), logfile.leafName);
clean();
run_next_test();
@@ -1677,7 +1856,7 @@ add_test(function test_logs_on_login_error_despite_shouldReportError() {
// This test should be the last one since it monkeypatches the engine object
// and we should only have one engine object throughout the file (bug 629664).
-add_test(function test_engine_applyFailed() {
+add_task(function test_engine_applyFailed() {
let server = sync_httpd_setup();
let engine = engineManager.get("catapult");
@@ -1687,9 +1866,10 @@ add_test(function test_engine_applyFailed() {
Svc.Obs.notify("weave:engine:sync:applied", {newFailed:1}, "catapult");
};
- let log = Log4Moz.repository.getLogger("Sync.ErrorHandler");
+ let log = Log.repository.getLogger("Sync.ErrorHandler");
Svc.Prefs.set("log.appender.file.logOnError", true);
+ let deferred = Promise.defer();
Svc.Obs.add("weave:service:reset-file-log", function onResetFileLog() {
Svc.Obs.remove("weave:service:reset-file-log", onResetFileLog);
@@ -1700,14 +1880,14 @@ add_test(function test_engine_applyFailed() {
let entries = logsdir.directoryEntries;
do_check_true(entries.hasMoreElements());
let logfile = entries.getNext().QueryInterface(Ci.nsILocalFile);
- do_check_eq(logfile.leafName.slice(0, LOG_PREFIX_ERROR.length),
- LOG_PREFIX_ERROR);
+ do_check_true(logfile.leafName.startsWith("error-sync-"), logfile.leafName);
clean();
- server.stop(run_next_test);
+ server.stop(deferred.resolve);
});
do_check_eq(Status.engines["catapult"], undefined);
- do_check_true(setUp());
+ do_check_true(yield setUp(server));
Service.sync();
+ yield deferred.promise;
});
diff --git a/services/sync/tests/unit/test_errorhandler_eol.js b/services/sync/tests/unit/test_errorhandler_eol.js
new file mode 100644
index 000000000..381bc7268
--- /dev/null
+++ b/services/sync/tests/unit/test_errorhandler_eol.js
@@ -0,0 +1,137 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+Cu.import("resource://services-sync/service.js");
+Cu.import("resource://services-sync/status.js");
+Cu.import("resource://services-sync/util.js");
+
+Cu.import("resource://testing-common/services/sync/fakeservices.js");
+Cu.import("resource://testing-common/services/sync/utils.js");
+
+function baseHandler(eolCode, request, response, statusCode, status, body) {
+ let alertBody = {
+ code: eolCode,
+ message: "Service is EOLed.",
+ url: "http://getfirefox.com",
+ };
+ response.setHeader("X-Weave-Timestamp", "" + new_timestamp(), false);
+ response.setHeader("X-Weave-Alert", "" + JSON.stringify(alertBody), false);
+ response.setStatusLine(request.httpVersion, statusCode, status);
+ response.bodyOutputStream.write(body, body.length);
+}
+
+function handler513(request, response) {
+ let statusCode = 513;
+ let status = "Upgrade Required";
+ let body = "{}";
+ baseHandler("hard-eol", request, response, statusCode, status, body);
+}
+
+function handler200(eolCode) {
+ return function (request, response) {
+ let statusCode = 200;
+ let status = "OK";
+ let body = "{\"meta\": 123456789010}";
+ baseHandler(eolCode, request, response, statusCode, status, body);
+ };
+}
+
+function sync_httpd_setup(infoHandler) {
+ let handlers = {
+ "/1.1/johndoe/info/collections": infoHandler,
+ };
+ return httpd_setup(handlers);
+}
+
+function setUp(server) {
+ yield configureIdentity({username: "johndoe"});
+ Service.serverURL = server.baseURI + "/";
+ Service.clusterURL = server.baseURI + "/";
+ new FakeCryptoService();
+}
+
+function run_test() {
+ run_next_test();
+}
+
+function do_check_soft_eol(eh, start) {
+ // We subtract 1000 because the stored value is in second precision.
+ do_check_true(eh.earliestNextAlert >= (start + eh.MINIMUM_ALERT_INTERVAL_MSEC - 1000));
+ do_check_eq("soft-eol", eh.currentAlertMode);
+}
+function do_check_hard_eol(eh, start) {
+ // We subtract 1000 because the stored value is in second precision.
+ do_check_true(eh.earliestNextAlert >= (start + eh.MINIMUM_ALERT_INTERVAL_MSEC - 1000));
+ do_check_eq("hard-eol", eh.currentAlertMode);
+ do_check_true(Status.eol);
+}
+
+add_identity_test(this, function test_200_hard() {
+ let eh = Service.errorHandler;
+ let start = Date.now();
+ let server = sync_httpd_setup(handler200("hard-eol"));
+ yield setUp(server);
+
+ let deferred = Promise.defer();
+ let obs = function (subject, topic, data) {
+ Svc.Obs.remove("weave:eol", obs);
+ do_check_eq("hard-eol", subject.code);
+ do_check_hard_eol(eh, start);
+ do_check_eq(Service.scheduler.eolInterval, Service.scheduler.syncInterval);
+ eh.clearServerAlerts();
+ server.stop(deferred.resolve);
+ };
+
+ Svc.Obs.add("weave:eol", obs);
+ Service._fetchInfo();
+ Service.scheduler.adjustSyncInterval(); // As if we failed or succeeded in syncing.
+ yield deferred.promise;
+});
+
+add_identity_test(this, function test_513_hard() {
+ let eh = Service.errorHandler;
+ let start = Date.now();
+ let server = sync_httpd_setup(handler513);
+ yield setUp(server);
+
+ let deferred = Promise.defer();
+ let obs = function (subject, topic, data) {
+ Svc.Obs.remove("weave:eol", obs);
+ do_check_eq("hard-eol", subject.code);
+ do_check_hard_eol(eh, start);
+ do_check_eq(Service.scheduler.eolInterval, Service.scheduler.syncInterval);
+ eh.clearServerAlerts();
+ server.stop(deferred.resolve);
+ };
+
+ Svc.Obs.add("weave:eol", obs);
+ try {
+ Service._fetchInfo();
+ Service.scheduler.adjustSyncInterval(); // As if we failed or succeeded in syncing.
+ } catch (ex) {
+ // Because fetchInfo will fail on a 513.
+ }
+ yield deferred.promise;
+});
+
+add_identity_test(this, function test_200_soft() {
+ let eh = Service.errorHandler;
+ let start = Date.now();
+ let server = sync_httpd_setup(handler200("soft-eol"));
+ yield setUp(server);
+
+ let deferred = Promise.defer();
+ let obs = function (subject, topic, data) {
+ Svc.Obs.remove("weave:eol", obs);
+ do_check_eq("soft-eol", subject.code);
+ do_check_soft_eol(eh, start);
+ do_check_eq(Service.scheduler.singleDeviceInterval, Service.scheduler.syncInterval);
+ eh.clearServerAlerts();
+ server.stop(deferred.resolve);
+ };
+
+ Svc.Obs.add("weave:eol", obs);
+ Service._fetchInfo();
+ Service.scheduler.adjustSyncInterval(); // As if we failed or succeeded in syncing.
+ yield deferred.promise;
+});
diff --git a/services/sync/tests/unit/test_errorhandler_filelog.js b/services/sync/tests/unit/test_errorhandler_filelog.js
index dbb7c71a8..0ce82b170 100644
--- a/services/sync/tests/unit/test_errorhandler_filelog.js
+++ b/services/sync/tests/unit/test_errorhandler_filelog.js
@@ -1,14 +1,15 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
-Cu.import("resource://services-common/log4moz.js");
+Cu.import("resource://gre/modules/Log.jsm");
Cu.import("resource://services-common/utils.js");
Cu.import("resource://services-sync/service.js");
Cu.import("resource://services-sync/util.js");
+Cu.import("resource://gre/modules/FileUtils.jsm");
+Cu.import("resource://gre/modules/NetUtil.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
const logsdir = FileUtils.getDir("ProfD", ["weave", "logs"], true);
-const LOG_PREFIX_SUCCESS = "success-";
-const LOG_PREFIX_ERROR = "error-";
// Delay to wait before cleanup, to allow files to age.
// This is so large because the file timestamp granularity is per-second, and
@@ -29,16 +30,17 @@ function setLastSync(lastSyncValue) {
function run_test() {
initTestLogging("Trace");
- Log4Moz.repository.getLogger("Sync.Service").level = Log4Moz.Level.Trace;
- Log4Moz.repository.getLogger("Sync.SyncScheduler").level = Log4Moz.Level.Trace;
- Log4Moz.repository.getLogger("Sync.ErrorHandler").level = Log4Moz.Level.Trace;
+ Log.repository.getLogger("Sync.LogManager").level = Log.Level.Trace;
+ Log.repository.getLogger("Sync.Service").level = Log.Level.Trace;
+ Log.repository.getLogger("Sync.SyncScheduler").level = Log.Level.Trace;
+ Log.repository.getLogger("Sync.ErrorHandler").level = Log.Level.Trace;
run_next_test();
}
add_test(function test_noOutput() {
// Ensure that the log appender won't print anything.
- errorHandler._logAppender.level = Log4Moz.Level.Fatal + 1;
+ errorHandler._logManager._fileAppender.level = Log.Level.Fatal + 1;
// Clear log output from startup.
Svc.Prefs.set("log.appender.file.logOnSuccess", false);
@@ -50,7 +52,7 @@ add_test(function test_noOutput() {
Svc.Obs.add("weave:service:reset-file-log", function onResetFileLog() {
Svc.Obs.remove("weave:service:reset-file-log", onResetFileLog);
- errorHandler._logAppender.level = Log4Moz.Level.Trace;
+ errorHandler._logManager._fileAppender.level = Log.Level.Trace;
Svc.Prefs.resetBranch("");
run_next_test();
});
@@ -62,7 +64,7 @@ add_test(function test_noOutput() {
add_test(function test_logOnSuccess_false() {
Svc.Prefs.set("log.appender.file.logOnSuccess", false);
- let log = Log4Moz.repository.getLogger("Sync.Test.FileLog");
+ let log = Log.repository.getLogger("Sync.Test.FileLog");
log.info("this won't show up");
Svc.Obs.add("weave:service:reset-file-log", function onResetFileLog() {
@@ -79,17 +81,22 @@ add_test(function test_logOnSuccess_false() {
});
function readFile(file, callback) {
- NetUtil.asyncFetch(file, function (inputStream, statusCode, request) {
+ NetUtil.asyncFetch2(file, function (inputStream, statusCode, request) {
let data = NetUtil.readInputStreamToString(inputStream,
inputStream.available());
callback(statusCode, data);
- });
+ },
+ null, // aLoadingNode
+ Services.scriptSecurityManager.getSystemPrincipal(),
+ null, // aTriggeringPrincipal
+ Ci.nsILoadInfo.SEC_NORMAL,
+ Ci.nsIContentPolicy.TYPE_OTHER);
}
add_test(function test_logOnSuccess_true() {
Svc.Prefs.set("log.appender.file.logOnSuccess", true);
- let log = Log4Moz.repository.getLogger("Sync.Test.FileLog");
+ let log = Log.repository.getLogger("Sync.Test.FileLog");
const MESSAGE = "this WILL show up";
log.info(MESSAGE);
@@ -101,8 +108,7 @@ add_test(function test_logOnSuccess_true() {
do_check_true(entries.hasMoreElements());
let logfile = entries.getNext().QueryInterface(Ci.nsILocalFile);
do_check_eq(logfile.leafName.slice(-4), ".txt");
- do_check_eq(logfile.leafName.slice(0, LOG_PREFIX_SUCCESS.length),
- LOG_PREFIX_SUCCESS);
+ do_check_true(logfile.leafName.startsWith("success-sync-"), logfile.leafName);
do_check_false(entries.hasMoreElements());
// Ensure the log message was actually written to file.
@@ -130,7 +136,7 @@ add_test(function test_logOnSuccess_true() {
add_test(function test_sync_error_logOnError_false() {
Svc.Prefs.set("log.appender.file.logOnError", false);
- let log = Log4Moz.repository.getLogger("Sync.Test.FileLog");
+ let log = Log.repository.getLogger("Sync.Test.FileLog");
log.info("this won't show up");
Svc.Obs.add("weave:service:reset-file-log", function onResetFileLog() {
@@ -150,10 +156,17 @@ add_test(function test_sync_error_logOnError_false() {
add_test(function test_sync_error_logOnError_true() {
Svc.Prefs.set("log.appender.file.logOnError", true);
- let log = Log4Moz.repository.getLogger("Sync.Test.FileLog");
+ let log = Log.repository.getLogger("Sync.Test.FileLog");
const MESSAGE = "this WILL show up";
log.info(MESSAGE);
+ // We need to wait until the log cleanup started by this test is complete
+ // or the next test will fail as it is ongoing.
+ Svc.Obs.add("services-tests:common:log-manager:cleanup-logs", function onCleanupLogs() {
+ Svc.Obs.remove("services-tests:common:log-manager:cleanup-logs", onCleanupLogs);
+ run_next_test();
+ });
+
Svc.Obs.add("weave:service:reset-file-log", function onResetFileLog() {
Svc.Obs.remove("weave:service:reset-file-log", onResetFileLog);
@@ -162,8 +175,7 @@ add_test(function test_sync_error_logOnError_true() {
do_check_true(entries.hasMoreElements());
let logfile = entries.getNext().QueryInterface(Ci.nsILocalFile);
do_check_eq(logfile.leafName.slice(-4), ".txt");
- do_check_eq(logfile.leafName.slice(0, LOG_PREFIX_ERROR.length),
- LOG_PREFIX_ERROR);
+ do_check_true(logfile.leafName.startsWith("error-sync-"), logfile.leafName);
do_check_false(entries.hasMoreElements());
// Ensure the log message was actually written to file.
@@ -180,7 +192,6 @@ add_test(function test_sync_error_logOnError_true() {
}
Svc.Prefs.resetBranch("");
- run_next_test();
});
});
@@ -192,7 +203,7 @@ add_test(function test_sync_error_logOnError_true() {
add_test(function test_login_error_logOnError_false() {
Svc.Prefs.set("log.appender.file.logOnError", false);
- let log = Log4Moz.repository.getLogger("Sync.Test.FileLog");
+ let log = Log.repository.getLogger("Sync.Test.FileLog");
log.info("this won't show up");
Svc.Obs.add("weave:service:reset-file-log", function onResetFileLog() {
@@ -212,10 +223,17 @@ add_test(function test_login_error_logOnError_false() {
add_test(function test_login_error_logOnError_true() {
Svc.Prefs.set("log.appender.file.logOnError", true);
- let log = Log4Moz.repository.getLogger("Sync.Test.FileLog");
+ let log = Log.repository.getLogger("Sync.Test.FileLog");
const MESSAGE = "this WILL show up";
log.info(MESSAGE);
+ // We need to wait until the log cleanup started by this test is complete
+ // or the next test will fail as it is ongoing.
+ Svc.Obs.add("services-tests:common:log-manager:cleanup-logs", function onCleanupLogs() {
+ Svc.Obs.remove("services-tests:common:log-manager:cleanup-logs", onCleanupLogs);
+ run_next_test();
+ });
+
Svc.Obs.add("weave:service:reset-file-log", function onResetFileLog() {
Svc.Obs.remove("weave:service:reset-file-log", onResetFileLog);
@@ -224,8 +242,7 @@ add_test(function test_login_error_logOnError_true() {
do_check_true(entries.hasMoreElements());
let logfile = entries.getNext().QueryInterface(Ci.nsILocalFile);
do_check_eq(logfile.leafName.slice(-4), ".txt");
- do_check_eq(logfile.leafName.slice(0, LOG_PREFIX_ERROR.length),
- LOG_PREFIX_ERROR);
+ do_check_true(logfile.leafName.startsWith("error-sync-"), logfile.leafName);
do_check_false(entries.hasMoreElements());
// Ensure the log message was actually written to file.
@@ -242,7 +259,6 @@ add_test(function test_login_error_logOnError_true() {
}
Svc.Prefs.resetBranch("");
- run_next_test();
});
});
@@ -265,7 +281,7 @@ add_test(function test_logErrorCleanup_age() {
_("Making some files.");
for (let i = 0; i < numLogs; i++) {
let now = Date.now();
- let filename = LOG_PREFIX_ERROR + now + "" + i + ".txt";
+ let filename = "error-sync-" + now + "" + i + ".txt";
let newLog = FileUtils.getFile("ProfD", ["weave", "logs", filename]);
let foStream = FileUtils.openFileOutputStream(newLog);
foStream.write(errString, errString.length);
@@ -274,8 +290,8 @@ add_test(function test_logErrorCleanup_age() {
oldLogs.push(newLog.leafName);
}
- Svc.Obs.add("weave:service:cleanup-logs", function onCleanupLogs() {
- Svc.Obs.remove("weave:service:cleanup-logs", onCleanupLogs);
+ Svc.Obs.add("services-tests:common:log-manager:cleanup-logs", function onCleanupLogs() {
+ Svc.Obs.remove("services-tests:common:log-manager:cleanup-logs", onCleanupLogs);
// Only the newest created log file remains.
let entries = logsdir.directoryEntries;
diff --git a/services/sync/tests/unit/test_errorhandler_sync_checkServerError.js b/services/sync/tests/unit/test_errorhandler_sync_checkServerError.js
index 1440aa9a4..18cea2cce 100644
--- a/services/sync/tests/unit/test_errorhandler_sync_checkServerError.js
+++ b/services/sync/tests/unit/test_errorhandler_sync_checkServerError.js
@@ -16,6 +16,12 @@ initTestLogging("Trace");
let engineManager = Service.engineManager;
engineManager.clear();
+function promiseStopServer(server) {
+ let deferred = Promise.defer();
+ server.stop(deferred.resolve);
+ return deferred.promise;
+}
+
function CatapultEngine() {
SyncEngine.call(this, "Catapult", Service);
}
@@ -53,26 +59,26 @@ function sync_httpd_setup() {
return httpd_setup(handlers);
}
-function setUp() {
- setBasicCredentials("johndoe", "ilovejane", "aabcdeabcdeabcdeabcdeabcde");
- Service.serverURL = TEST_SERVER_URL;
- Service.clusterURL = TEST_CLUSTER_URL;
+function setUp(server) {
+ yield configureIdentity({username: "johndoe"});
+ Service.serverURL = server.baseURI + "/";
+ Service.clusterURL = server.baseURI + "/";
new FakeCryptoService();
}
-function generateAndUploadKeys() {
+function generateAndUploadKeys(server) {
generateNewKeys(Service.collectionKeys);
let serverKeys = Service.collectionKeys.asWBO("crypto", "keys");
serverKeys.encrypt(Service.identity.syncKeyBundle);
- let res = Service.resource("http://localhost:8080/1.1/johndoe/storage/crypto/keys");
+ let res = Service.resource(server.baseURI + "/1.1/johndoe/storage/crypto/keys");
return serverKeys.upload(res).success;
}
-add_test(function test_backoff500() {
+add_identity_test(this, function test_backoff500() {
_("Test: HTTP 500 sets backoff status.");
- setUp();
let server = sync_httpd_setup();
+ yield setUp(server);
let engine = engineManager.get("catapult");
engine.enabled = true;
@@ -82,7 +88,7 @@ add_test(function test_backoff500() {
do_check_false(Status.enforceBackoff);
// Forcibly create and upload keys here -- otherwise we don't get to the 500!
- do_check_true(generateAndUploadKeys());
+ do_check_true(generateAndUploadKeys(server));
Service.login();
Service.sync();
@@ -93,13 +99,13 @@ add_test(function test_backoff500() {
Status.resetBackoff();
Service.startOver();
}
- server.stop(run_next_test);
+ yield promiseStopServer(server);
});
-add_test(function test_backoff503() {
+add_identity_test(this, function test_backoff503() {
_("Test: HTTP 503 with Retry-After header leads to backoff notification and sets backoff status.");
- setUp();
let server = sync_httpd_setup();
+ yield setUp(server);
const BACKOFF = 42;
let engine = engineManager.get("catapult");
@@ -115,7 +121,7 @@ add_test(function test_backoff503() {
try {
do_check_false(Status.enforceBackoff);
- do_check_true(generateAndUploadKeys());
+ do_check_true(generateAndUploadKeys(server));
Service.login();
Service.sync();
@@ -129,23 +135,25 @@ add_test(function test_backoff503() {
Status.resetSync();
Service.startOver();
}
- server.stop(run_next_test);
+ yield promiseStopServer(server);
});
-add_test(function test_overQuota() {
+add_identity_test(this, function test_overQuota() {
_("Test: HTTP 400 with body error code 14 means over quota.");
- setUp();
let server = sync_httpd_setup();
+ yield setUp(server);
let engine = engineManager.get("catapult");
engine.enabled = true;
engine.exception = {status: 400,
- toString: function() "14"};
+ toString() {
+ return "14";
+ }};
try {
do_check_eq(Status.sync, SYNC_SUCCEEDED);
- do_check_true(generateAndUploadKeys());
+ do_check_true(generateAndUploadKeys(server));
Service.login();
Service.sync();
@@ -156,55 +164,65 @@ add_test(function test_overQuota() {
Status.resetSync();
Service.startOver();
}
- server.stop(run_next_test);
+ yield promiseStopServer(server);
});
-add_test(function test_service_networkError() {
+add_identity_test(this, function test_service_networkError() {
_("Test: Connection refused error from Service.sync() leads to the right status code.");
- setUp();
- // Provoke connection refused.
- Service.clusterURL = "http://localhost:12345/";
-
- try {
- do_check_eq(Status.sync, SYNC_SUCCEEDED);
-
- Service._loggedIn = true;
- Service.sync();
-
- do_check_eq(Status.sync, LOGIN_FAILED_NETWORK_ERROR);
- do_check_eq(Status.service, SYNC_FAILED);
- } finally {
- Status.resetSync();
- Service.startOver();
- }
- run_next_test();
+ let server = sync_httpd_setup();
+ yield setUp(server);
+ let deferred = Promise.defer();
+ server.stop(() => {
+ // Provoke connection refused.
+ Service.clusterURL = "http://localhost:12345/";
+
+ try {
+ do_check_eq(Status.sync, SYNC_SUCCEEDED);
+
+ Service._loggedIn = true;
+ Service.sync();
+
+ do_check_eq(Status.sync, LOGIN_FAILED_NETWORK_ERROR);
+ do_check_eq(Status.service, SYNC_FAILED);
+ } finally {
+ Status.resetSync();
+ Service.startOver();
+ }
+ deferred.resolve();
+ });
+ yield deferred.promise;
});
-add_test(function test_service_offline() {
+add_identity_test(this, function test_service_offline() {
_("Test: Wanting to sync in offline mode leads to the right status code but does not increment the ignorable error count.");
- setUp();
- Services.io.offline = true;
-
- try {
- do_check_eq(Status.sync, SYNC_SUCCEEDED);
-
- Service._loggedIn = true;
- Service.sync();
-
- do_check_eq(Status.sync, LOGIN_FAILED_NETWORK_ERROR);
- do_check_eq(Status.service, SYNC_FAILED);
- } finally {
- Status.resetSync();
- Service.startOver();
- }
- Services.io.offline = false;
- run_next_test();
+ let server = sync_httpd_setup();
+ yield setUp(server);
+ let deferred = Promise.defer();
+ server.stop(() => {
+ Services.io.offline = true;
+
+ try {
+ do_check_eq(Status.sync, SYNC_SUCCEEDED);
+
+ Service._loggedIn = true;
+ Service.sync();
+
+ do_check_eq(Status.sync, LOGIN_FAILED_NETWORK_ERROR);
+ do_check_eq(Status.service, SYNC_FAILED);
+ } finally {
+ Status.resetSync();
+ Service.startOver();
+ }
+ Services.io.offline = false;
+ deferred.resolve();
+ });
+ yield deferred.promise;
});
-add_test(function test_engine_networkError() {
+add_identity_test(this, function test_engine_networkError() {
_("Test: Network related exceptions from engine.sync() lead to the right status code.");
- setUp();
let server = sync_httpd_setup();
+ yield setUp(server);
let engine = engineManager.get("catapult");
engine.enabled = true;
@@ -214,7 +232,7 @@ add_test(function test_engine_networkError() {
try {
do_check_eq(Status.sync, SYNC_SUCCEEDED);
- do_check_true(generateAndUploadKeys());
+ do_check_true(generateAndUploadKeys(server));
Service.login();
Service.sync();
@@ -225,12 +243,12 @@ add_test(function test_engine_networkError() {
Status.resetSync();
Service.startOver();
}
- server.stop(run_next_test);
+ yield promiseStopServer(server);
});
-add_test(function test_resource_timeout() {
- setUp();
+add_identity_test(this, function test_resource_timeout() {
let server = sync_httpd_setup();
+ yield setUp(server);
let engine = engineManager.get("catapult");
engine.enabled = true;
@@ -241,7 +259,7 @@ add_test(function test_resource_timeout() {
try {
do_check_eq(Status.sync, SYNC_SUCCEEDED);
- do_check_true(generateAndUploadKeys());
+ do_check_true(generateAndUploadKeys(server));
Service.login();
Service.sync();
@@ -252,7 +270,7 @@ add_test(function test_resource_timeout() {
Status.resetSync();
Service.startOver();
}
- server.stop(run_next_test);
+ yield promiseStopServer(server);
});
function run_test() {
diff --git a/services/sync/tests/unit/test_forms_store.js b/services/sync/tests/unit/test_forms_store.js
index 865ba4d63..6963df1c0 100644
--- a/services/sync/tests/unit/test_forms_store.js
+++ b/services/sync/tests/unit/test_forms_store.js
@@ -5,6 +5,7 @@ _("Make sure the form store follows the Store api and correctly accesses the bac
Cu.import("resource://services-sync/engines/forms.js");
Cu.import("resource://services-sync/service.js");
Cu.import("resource://services-sync/util.js");
+Cu.import("resource://gre/modules/Services.jsm");
function run_test() {
let baseuri = "http://fake/uri/";
@@ -129,4 +130,22 @@ function run_test() {
for (let id in store.getAllIDs()) {
do_throw("Shouldn't get any ids!");
}
+
+ _("Ensure we work if formfill is disabled.");
+ Services.prefs.setBoolPref("browser.formfill.enable", false);
+ try {
+ // a search
+ for (let id in store.getAllIDs()) {
+ do_throw("Shouldn't get any ids!");
+ }
+ // an update.
+ applyEnsureNoFailures([{
+ id: Utils.makeGUID(),
+ name: "some",
+ value: "entry"
+ }]);
+ } finally {
+ Services.prefs.clearUserPref("browser.formfill.enable");
+ store.wipe();
+ }
}
diff --git a/services/sync/tests/unit/test_forms_tracker.js b/services/sync/tests/unit/test_forms_tracker.js
index 3d0139962..5f7aaa648 100644
--- a/services/sync/tests/unit/test_forms_tracker.js
+++ b/services/sync/tests/unit/test_forms_tracker.js
@@ -1,7 +1,7 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
-Cu.import("resource://services-common/log4moz.js");
+Cu.import("resource://gre/modules/Log.jsm");
Cu.import("resource://services-sync/engines/forms.js");
Cu.import("resource://services-sync/service.js");
Cu.import("resource://services-sync/util.js");
@@ -14,7 +14,7 @@ function run_test() {
tracker.persistChangedIDs = false;
do_check_empty(tracker.changedIDs);
- Log4Moz.repository.rootLogger.addAppender(new Log4Moz.DumpAppender());
+ Log.repository.rootLogger.addAppender(new Log.DumpAppender());
function addEntry(name, value) {
engine._store.create({name: name, value: value});
diff --git a/services/sync/tests/unit/test_fxa_migration.js b/services/sync/tests/unit/test_fxa_migration.js
new file mode 100644
index 000000000..7c65d5996
--- /dev/null
+++ b/services/sync/tests/unit/test_fxa_migration.js
@@ -0,0 +1,279 @@
+// Test the FxAMigration module
+Cu.import("resource://services-sync/FxaMigrator.jsm");
+Cu.import("resource://gre/modules/Promise.jsm");
+Cu.import("resource://gre/modules/FxAccounts.jsm");
+Cu.import("resource://gre/modules/FxAccountsCommon.js");
+Cu.import("resource://services-sync/browserid_identity.js");
+
+// Set our username pref early so sync initializes with the legacy provider.
+Services.prefs.setCharPref("services.sync.username", "foo");
+// And ensure all debug messages end up being printed.
+Services.prefs.setCharPref("services.sync.log.appender.dump", "Debug");
+
+// Now import sync
+Cu.import("resource://services-sync/service.js");
+Cu.import("resource://services-sync/record.js");
+Cu.import("resource://services-sync/util.js");
+
+// And reset the username.
+Services.prefs.clearUserPref("services.sync.username");
+
+Cu.import("resource://testing-common/services/sync/utils.js");
+Cu.import("resource://testing-common/services/common/logging.js");
+Cu.import("resource://testing-common/services/sync/rotaryengine.js");
+
+const FXA_USERNAME = "someone@somewhere";
+
+// Utilities
+function promiseOneObserver(topic) {
+ return new Promise((resolve, reject) => {
+ let observer = function(subject, topic, data) {
+ Services.obs.removeObserver(observer, topic);
+ resolve({ subject: subject, data: data });
+ }
+ Services.obs.addObserver(observer, topic, false);
+ });
+}
+
+function promiseStopServer(server) {
+ return new Promise((resolve, reject) => {
+ server.stop(resolve);
+ });
+}
+
+
+// Helpers
+function configureLegacySync() {
+ let engine = new RotaryEngine(Service);
+ engine.enabled = true;
+ Svc.Prefs.set("registerEngines", engine.name);
+ Svc.Prefs.set("log.logger.engine.rotary", "Trace");
+
+ let contents = {
+ meta: {global: {engines: {rotary: {version: engine.version,
+ syncID: engine.syncID}}}},
+ crypto: {},
+ rotary: {}
+ };
+
+ const USER = "foo";
+ const PASSPHRASE = "abcdeabcdeabcdeabcdeabcdea";
+
+ setBasicCredentials(USER, "password", PASSPHRASE);
+
+ let onRequest = function(request, response) {
+ // ideally we'd only do this while a legacy user is configured, but WTH.
+ response.setHeader("x-weave-alert", JSON.stringify({code: "soft-eol"}));
+ }
+ let server = new SyncServer({onRequest: onRequest});
+ server.registerUser(USER, "password");
+ server.createContents(USER, contents);
+ server.start();
+
+ Service.serverURL = server.baseURI;
+ Service.clusterURL = server.baseURI;
+ Service.identity.username = USER;
+ Service._updateCachedURLs();
+
+ Service.engineManager._engines[engine.name] = engine;
+
+ return [engine, server];
+}
+
+function configureFxa() {
+ Services.prefs.setCharPref("identity.fxaccounts.auth.uri", "http://localhost");
+}
+
+add_task(function *testMigration() {
+ configureFxa();
+
+ // when we do a .startOver we want the new provider.
+ let oldValue = Services.prefs.getBoolPref("services.sync-testing.startOverKeepIdentity");
+ Services.prefs.setBoolPref("services.sync-testing.startOverKeepIdentity", false);
+
+ // disable the addons engine - this engine choice is arbitrary, but we
+ // want to check it remains disabled after migration.
+ Services.prefs.setBoolPref("services.sync.engine.addons", false);
+
+ do_register_cleanup(() => {
+ Services.prefs.setBoolPref("services.sync-testing.startOverKeepIdentity", oldValue)
+ Services.prefs.setBoolPref("services.sync.engine.addons", true);
+ });
+
+ // No sync user - that should report no user-action necessary.
+ Assert.deepEqual((yield fxaMigrator._queueCurrentUserState()), null,
+ "no user state when complete");
+
+ // Arrange for a legacy sync user and manually bump the migrator
+ let [engine, server] = configureLegacySync();
+
+ // Check our disabling of the "addons" engine worked, and for good measure,
+ // that the "passwords" engine is enabled.
+ Assert.ok(!Service.engineManager.get("addons").enabled, "addons is disabled");
+ Assert.ok(Service.engineManager.get("passwords").enabled, "passwords is enabled");
+
+ // monkey-patch the migration sentinel code so we know it was called.
+ let haveStartedSentinel = false;
+ let origSetFxAMigrationSentinel = Service.setFxAMigrationSentinel;
+ let promiseSentinelWritten = new Promise((resolve, reject) => {
+ Service.setFxAMigrationSentinel = function(arg) {
+ haveStartedSentinel = true;
+ return origSetFxAMigrationSentinel.call(Service, arg).then(result => {
+ Service.setFxAMigrationSentinel = origSetFxAMigrationSentinel;
+ resolve(result);
+ return result;
+ });
+ }
+ });
+
+ // We are now configured for legacy sync, but we aren't in an EOL state yet,
+ // so should still be not waiting for a user.
+ Assert.deepEqual((yield fxaMigrator._queueCurrentUserState()), null,
+ "no user state before server EOL");
+
+ // Start a sync - this will cause an EOL notification which the migrator's
+ // observer will notice.
+ let promise = promiseOneObserver("fxa-migration:state-changed");
+ _("Starting sync");
+ Service.sync();
+ _("Finished sync");
+
+ // We should have seen the observer, so be waiting for an FxA user.
+ Assert.equal((yield promise).data, fxaMigrator.STATE_USER_FXA, "now waiting for FxA.")
+
+ // Re-calling our user-state promise should also reflect the same state.
+ Assert.equal((yield fxaMigrator._queueCurrentUserState()),
+ fxaMigrator.STATE_USER_FXA,
+ "still waiting for FxA.");
+
+ // arrange for an unverified FxA user.
+ let config = makeIdentityConfig({username: FXA_USERNAME});
+ let fxa = new FxAccounts({});
+ config.fxaccount.user.email = config.username;
+ delete config.fxaccount.user.verified;
+ // *sob* - shouldn't need this boilerplate
+ fxa.internal.currentAccountState.getCertificate = function(data, keyPair, mustBeValidUntil) {
+ this.cert = {
+ validUntil: fxa.internal.now() + CERT_LIFETIME,
+ cert: "certificate",
+ };
+ return Promise.resolve(this.cert.cert);
+ };
+
+ // As soon as we set the FxA user the observers should fire and magically
+ // transition.
+ promise = promiseOneObserver("fxa-migration:state-changed");
+ fxAccounts.setSignedInUser(config.fxaccount.user);
+
+ let observerInfo = yield promise;
+ Assert.equal(observerInfo.data,
+ fxaMigrator.STATE_USER_FXA_VERIFIED,
+ "now waiting for verification");
+ Assert.ok(observerInfo.subject instanceof Ci.nsISupportsString,
+ "email was passed to observer");
+ Assert.equal(observerInfo.subject.data,
+ FXA_USERNAME,
+ "email passed to observer is correct");
+
+ // should have seen the user set, so state should automatically update.
+ Assert.equal((yield fxaMigrator._queueCurrentUserState()),
+ fxaMigrator.STATE_USER_FXA_VERIFIED,
+ "now waiting for verification");
+
+ // Before we verify the user, fire off a sync that calls us back during
+ // the sync and before it completes - this way we can ensure we do the right
+ // thing in terms of blocking sync and waiting for it to complete.
+
+ let wasWaiting = false;
+ // This is a PITA as sync is pseudo-blocking.
+ engine._syncFinish = function () {
+ // We aren't in a generator here, so use a helper to block on promises.
+ function getState() {
+ let cb = Async.makeSpinningCallback();
+ fxaMigrator._queueCurrentUserState().then(state => cb(null, state));
+ return cb.wait();
+ }
+ // should still be waiting for verification.
+ Assert.equal(getState(), fxaMigrator.STATE_USER_FXA_VERIFIED,
+ "still waiting for verification");
+
+ // arrange for the user to be verified. The fxAccount's mock story is
+ // broken, so go behind its back.
+ config.fxaccount.user.verified = true;
+ fxAccounts.setSignedInUser(config.fxaccount.user);
+ Services.obs.notifyObservers(null, ONVERIFIED_NOTIFICATION, null);
+
+ // spinningly wait for the migrator to catch up - sync is running so
+ // we should be in a 'null' user-state as there is no user-action
+ // necessary.
+ let cb = Async.makeSpinningCallback();
+ promiseOneObserver("fxa-migration:state-changed").then(({ data: state }) => cb(null, state));
+ Assert.equal(cb.wait(), null, "no user action necessary while sync completes.");
+
+ // We must not have started writing the sentinel yet.
+ Assert.ok(!haveStartedSentinel, "haven't written a sentinel yet");
+
+ // sync should be blocked from continuing
+ Assert.ok(Service.scheduler.isBlocked, "sync is blocked.")
+
+ wasWaiting = true;
+ throw ex;
+ };
+
+ _("Starting sync");
+ Service.sync();
+ _("Finished sync");
+
+ // mock sync so we can ensure the final sync is scheduled with the FxA user.
+ // (letting a "normal" sync complete is a PITA without mocking huge amounts
+ // of FxA infra)
+ let promiseFinalSync = new Promise((resolve, reject) => {
+ let oldSync = Service.sync;
+ Service.sync = function() {
+ Service.sync = oldSync;
+ resolve();
+ }
+ });
+
+ Assert.ok(wasWaiting, "everything was good while sync was running.")
+
+ // The migration is now going to run to completion.
+ // sync should still be "blocked"
+ Assert.ok(Service.scheduler.isBlocked, "sync is blocked.");
+
+ // We should see the migration sentinel written and it should return true.
+ Assert.ok((yield promiseSentinelWritten), "wrote the sentinel");
+
+ // And we should see a new sync start
+ yield promiseFinalSync;
+
+ // and we should be configured for FxA
+ let WeaveService = Cc["@mozilla.org/weave/service;1"]
+ .getService(Components.interfaces.nsISupports)
+ .wrappedJSObject;
+ Assert.ok(WeaveService.fxAccountsEnabled, "FxA is enabled");
+ Assert.ok(Service.identity instanceof BrowserIDManager,
+ "sync is configured with the browserid_identity provider.");
+ Assert.equal(Service.identity.username, config.username, "correct user configured")
+ Assert.ok(!Service.scheduler.isBlocked, "sync is not blocked.")
+ // and the user state should remain null.
+ Assert.deepEqual((yield fxaMigrator._queueCurrentUserState()),
+ null,
+ "still no user action necessary");
+ // and our engines should be in the same enabled/disabled state as before.
+ Assert.ok(!Service.engineManager.get("addons").enabled, "addons is still disabled");
+ Assert.ok(Service.engineManager.get("passwords").enabled, "passwords is still enabled");
+
+ // aaaand, we are done - clean up.
+ yield promiseStopServer(server);
+});
+
+
+function run_test() {
+ initTestLogging();
+ do_register_cleanup(() => {
+ fxaMigrator.finalize();
+ Svc.Prefs.resetBranch("");
+ });
+ run_next_test();
+}
diff --git a/services/sync/tests/unit/test_fxa_migration_sentinel.js b/services/sync/tests/unit/test_fxa_migration_sentinel.js
new file mode 100644
index 000000000..bed2dd756
--- /dev/null
+++ b/services/sync/tests/unit/test_fxa_migration_sentinel.js
@@ -0,0 +1,150 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test the reading and writing of the sync migration sentinel.
+Cu.import("resource://gre/modules/Promise.jsm");
+Cu.import("resource://gre/modules/FxAccounts.jsm");
+Cu.import("resource://gre/modules/FxAccountsCommon.js");
+
+Cu.import("resource://testing-common/services/sync/utils.js");
+Cu.import("resource://testing-common/services/common/logging.js");
+
+Cu.import("resource://services-sync/record.js");
+
+// Set our username pref early so sync initializes with the legacy provider.
+Services.prefs.setCharPref("services.sync.username", "foo");
+
+// Now import sync
+Cu.import("resource://services-sync/service.js");
+
+const USER = "foo";
+const PASSPHRASE = "abcdeabcdeabcdeabcdeabcdea";
+
+function promiseStopServer(server) {
+ return new Promise((resolve, reject) => {
+ server.stop(resolve);
+ });
+}
+
+let numServerRequests = 0;
+
+// Helpers
+function configureLegacySync() {
+ let contents = {
+ meta: {global: {}},
+ crypto: {},
+ };
+
+ setBasicCredentials(USER, "password", PASSPHRASE);
+
+ numServerRequests = 0;
+ let server = new SyncServer({
+ onRequest: () => {
+ ++numServerRequests
+ }
+ });
+ server.registerUser(USER, "password");
+ server.createContents(USER, contents);
+ server.start();
+
+ Service.serverURL = server.baseURI;
+ Service.clusterURL = server.baseURI;
+ Service.identity.username = USER;
+ Service._updateCachedURLs();
+
+ return server;
+}
+
+// Test a simple round-trip of the get/set functions.
+add_task(function *() {
+ // Arrange for a legacy sync user.
+ let server = configureLegacySync();
+
+ Assert.equal((yield Service.getFxAMigrationSentinel()), null, "no sentinel to start");
+
+ let sentinel = {foo: "bar"};
+ yield Service.setFxAMigrationSentinel(sentinel);
+
+ Assert.deepEqual((yield Service.getFxAMigrationSentinel()), sentinel, "got the sentinel back");
+
+ yield promiseStopServer(server);
+});
+
+// Test the records are cached by the record manager.
+add_task(function *() {
+ // Arrange for a legacy sync user.
+ let server = configureLegacySync();
+ Service.login();
+
+ // Reset the request count here as the login would have made some.
+ numServerRequests = 0;
+
+ Assert.equal((yield Service.getFxAMigrationSentinel()), null, "no sentinel to start");
+ Assert.equal(numServerRequests, 1, "first fetch should hit the server");
+
+ let sentinel = {foo: "bar"};
+ yield Service.setFxAMigrationSentinel(sentinel);
+ Assert.equal(numServerRequests, 2, "setting sentinel should hit the server");
+
+ Assert.deepEqual((yield Service.getFxAMigrationSentinel()), sentinel, "got the sentinel back");
+ Assert.equal(numServerRequests, 2, "second fetch should not should hit the server");
+
+ // Clobber the caches and ensure we still get the correct value back when we
+ // do hit the server.
+ Service.recordManager.clearCache();
+ Assert.deepEqual((yield Service.getFxAMigrationSentinel()), sentinel, "got the sentinel back");
+ Assert.equal(numServerRequests, 3, "should have re-hit the server with empty caches");
+
+ yield promiseStopServer(server);
+});
+
+// Test the records are cached by a sync.
+add_task(function* () {
+ let server = configureLegacySync();
+
+ // A first sync clobbers meta/global due to it being empty, so we first
+ // do a sync which forces a good set of data on the server.
+ Service.sync();
+
+ // Now create a sentinel exists on the server. It's encrypted, so we need to
+ // put an encrypted version.
+ let cryptoWrapper = new CryptoWrapper("meta", "fxa_credentials");
+ let sentinel = {foo: "bar"};
+ cryptoWrapper.cleartext = {
+ id: "fxa_credentials",
+ sentinel: sentinel,
+ deleted: false,
+ }
+ cryptoWrapper.encrypt(Service.identity.syncKeyBundle);
+ let payload = {
+ ciphertext: cryptoWrapper.ciphertext,
+ IV: cryptoWrapper.IV,
+ hmac: cryptoWrapper.hmac,
+ };
+
+ server.createContents(USER, {
+ meta: {fxa_credentials: payload},
+ crypto: {},
+ });
+
+ // Another sync - this will cause the encrypted record to be fetched.
+ Service.sync();
+ // Reset the request count here as the sync will have made many!
+ numServerRequests = 0;
+
+ // Asking for the sentinel should use the copy cached in the record manager.
+ Assert.deepEqual((yield Service.getFxAMigrationSentinel()), sentinel, "got it");
+ Assert.equal(numServerRequests, 0, "should not have hit the server");
+
+ // And asking for it again should work (we have to work around the fact the
+ // ciphertext is clobbered on first decrypt...)
+ Assert.deepEqual((yield Service.getFxAMigrationSentinel()), sentinel, "got it again");
+ Assert.equal(numServerRequests, 0, "should not have hit the server");
+
+ yield promiseStopServer(server);
+});
+
+function run_test() {
+ initTestLogging();
+ run_next_test();
+}
diff --git a/services/sync/tests/unit/test_fxa_node_reassignment.js b/services/sync/tests/unit/test_fxa_node_reassignment.js
new file mode 100644
index 000000000..2f61afd6f
--- /dev/null
+++ b/services/sync/tests/unit/test_fxa_node_reassignment.js
@@ -0,0 +1,321 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+_("Test that node reassignment happens correctly using the FxA identity mgr.");
+// The node-reassignment logic is quite different for FxA than for the legacy
+// provider. In particular, there's no special request necessary for
+// reassignment - it comes from the token server - so we need to ensure the
+// Fxa cluster manager grabs a new token.
+
+Cu.import("resource://gre/modules/Log.jsm");
+Cu.import("resource://services-common/rest.js");
+Cu.import("resource://services-sync/constants.js");
+Cu.import("resource://services-sync/service.js");
+Cu.import("resource://services-sync/status.js");
+Cu.import("resource://services-sync/util.js");
+Cu.import("resource://testing-common/services/sync/rotaryengine.js");
+Cu.import("resource://services-sync/browserid_identity.js");
+Cu.import("resource://testing-common/services/sync/utils.js");
+
+Service.engineManager.clear();
+
+function run_test() {
+ Log.repository.getLogger("Sync.AsyncResource").level = Log.Level.Trace;
+ Log.repository.getLogger("Sync.ErrorHandler").level = Log.Level.Trace;
+ Log.repository.getLogger("Sync.Resource").level = Log.Level.Trace;
+ Log.repository.getLogger("Sync.RESTRequest").level = Log.Level.Trace;
+ Log.repository.getLogger("Sync.Service").level = Log.Level.Trace;
+ Log.repository.getLogger("Sync.SyncScheduler").level = Log.Level.Trace;
+ initTestLogging();
+
+ Service.engineManager.register(RotaryEngine);
+
+ // Setup the FxA identity manager and cluster manager.
+ Status.__authManager = Service.identity = new BrowserIDManager();
+ Service._clusterManager = Service.identity.createClusterManager(Service);
+
+ // None of the failures in this file should result in a UI error.
+ function onUIError() {
+ do_throw("Errors should not be presented in the UI.");
+ }
+ Svc.Obs.add("weave:ui:login:error", onUIError);
+ Svc.Obs.add("weave:ui:sync:error", onUIError);
+
+ run_next_test();
+}
+
+
+// API-compatible with SyncServer handler. Bind `handler` to something to use
+// as a ServerCollection handler.
+function handleReassign(handler, req, resp) {
+ resp.setStatusLine(req.httpVersion, 401, "Node reassignment");
+ resp.setHeader("Content-Type", "application/json");
+ let reassignBody = JSON.stringify({error: "401inator in place"});
+ resp.bodyOutputStream.write(reassignBody, reassignBody.length);
+}
+
+let numTokenRequests = 0;
+
+function prepareServer(cbAfterTokenFetch) {
+ let config = makeIdentityConfig({username: "johndoe"});
+ let server = new SyncServer();
+ server.registerUser("johndoe");
+ server.start();
+
+ // Set the token endpoint for the initial token request that's done implicitly
+ // via configureIdentity.
+ config.fxaccount.token.endpoint = server.baseURI + "1.1/johndoe";
+ // And future token fetches will do magic around numReassigns.
+ let numReassigns = 0;
+ return configureIdentity(config).then(() => {
+ Service.identity._tokenServerClient = {
+ getTokenFromBrowserIDAssertion: function(uri, assertion, cb) {
+ // Build a new URL with trailing zeros for the SYNC_VERSION part - this
+ // will still be seen as equivalent by the test server, but different
+ // by sync itself.
+ numReassigns += 1;
+ let trailingZeros = new Array(numReassigns + 1).join('0');
+ let token = config.fxaccount.token;
+ token.endpoint = server.baseURI + "1.1" + trailingZeros + "/johndoe";
+ token.uid = config.username;
+ numTokenRequests += 1;
+ cb(null, token);
+ if (cbAfterTokenFetch) {
+ cbAfterTokenFetch();
+ }
+ },
+ };
+ Service.clusterURL = config.fxaccount.token.endpoint;
+ return server;
+ });
+}
+
+function getReassigned() {
+ try {
+ return Services.prefs.getBoolPref("services.sync.lastSyncReassigned");
+ } catch (ex if (ex.result == Cr.NS_ERROR_UNEXPECTED)) {
+ return false;
+ } catch (ex) {
+ do_throw("Got exception retrieving lastSyncReassigned: " +
+ Utils.exceptionStr(ex));
+ }
+}
+
+/**
+ * Make a test request to `url`, then watch the result of two syncs
+ * to ensure that a node request was made.
+ * Runs `between` between the two. This can be used to undo deliberate failure
+ * setup, detach observers, etc.
+ */
+function syncAndExpectNodeReassignment(server, firstNotification, between,
+ secondNotification, url) {
+ _("Starting syncAndExpectNodeReassignment\n");
+ let deferred = Promise.defer();
+ function onwards() {
+ let numTokenRequestsBefore;
+ function onFirstSync() {
+ _("First sync completed.");
+ Svc.Obs.remove(firstNotification, onFirstSync);
+ Svc.Obs.add(secondNotification, onSecondSync);
+
+ do_check_eq(Service.clusterURL, "");
+
+ // Track whether we fetched a new token.
+ numTokenRequestsBefore = numTokenRequests;
+
+ // Allow for tests to clean up error conditions.
+ between();
+ }
+ function onSecondSync() {
+ _("Second sync completed.");
+ Svc.Obs.remove(secondNotification, onSecondSync);
+ Service.scheduler.clearSyncTriggers();
+
+ // Make absolutely sure that any event listeners are done with their work
+ // before we proceed.
+ waitForZeroTimer(function () {
+ _("Second sync nextTick.");
+ do_check_eq(numTokenRequests, numTokenRequestsBefore + 1, "fetched a new token");
+ Service.startOver();
+ server.stop(deferred.resolve);
+ });
+ }
+
+ Svc.Obs.add(firstNotification, onFirstSync);
+ Service.sync();
+ }
+
+ // Make sure that it works!
+ _("Making request to " + url + " which should 401");
+ let request = new RESTRequest(url);
+ request.get(function () {
+ do_check_eq(request.response.status, 401);
+ Utils.nextTick(onwards);
+ });
+ yield deferred.promise;
+}
+
+add_task(function test_momentary_401_engine() {
+ _("Test a failure for engine URLs that's resolved by reassignment.");
+ let server = yield prepareServer();
+ let john = server.user("johndoe");
+
+ _("Enabling the Rotary engine.");
+ let engine = Service.engineManager.get("rotary");
+ engine.enabled = true;
+
+ // We need the server to be correctly set up prior to experimenting. Do this
+ // through a sync.
+ let global = {syncID: Service.syncID,
+ storageVersion: STORAGE_VERSION,
+ rotary: {version: engine.version,
+ syncID: engine.syncID}}
+ john.createCollection("meta").insert("global", global);
+
+ _("First sync to prepare server contents.");
+ Service.sync();
+
+ _("Setting up Rotary collection to 401.");
+ let rotary = john.createCollection("rotary");
+ let oldHandler = rotary.collectionHandler;
+ rotary.collectionHandler = handleReassign.bind(this, undefined);
+
+ // We want to verify that the clusterURL pref has been cleared after a 401
+ // inside a sync. Flag the Rotary engine to need syncing.
+ john.collection("rotary").timestamp += 1000;
+
+ function between() {
+ _("Undoing test changes.");
+ rotary.collectionHandler = oldHandler;
+
+ function onLoginStart() {
+ // lastSyncReassigned shouldn't be cleared until a sync has succeeded.
+ _("Ensuring that lastSyncReassigned is still set at next sync start.");
+ Svc.Obs.remove("weave:service:login:start", onLoginStart);
+ do_check_true(getReassigned());
+ }
+
+ _("Adding observer that lastSyncReassigned is still set on login.");
+ Svc.Obs.add("weave:service:login:start", onLoginStart);
+ }
+
+ yield syncAndExpectNodeReassignment(server,
+ "weave:service:sync:finish",
+ between,
+ "weave:service:sync:finish",
+ Service.storageURL + "rotary");
+});
+
+// This test ends up being a failing info fetch *after we're already logged in*.
+add_task(function test_momentary_401_info_collections_loggedin() {
+ _("Test a failure for info/collections after login that's resolved by reassignment.");
+ let server = yield prepareServer();
+
+ _("First sync to prepare server contents.");
+ Service.sync();
+
+ _("Arrange for info/collections to return a 401.");
+ let oldHandler = server.toplevelHandlers.info;
+ server.toplevelHandlers.info = handleReassign;
+
+ function undo() {
+ _("Undoing test changes.");
+ server.toplevelHandlers.info = oldHandler;
+ }
+
+ do_check_true(Service.isLoggedIn, "already logged in");
+
+ yield syncAndExpectNodeReassignment(server,
+ "weave:service:sync:error",
+ undo,
+ "weave:service:sync:finish",
+ Service.infoURL);
+});
+
+// This test ends up being a failing info fetch *before we're logged in*.
+// In this case we expect to recover during the login phase - so the first
+// sync succeeds.
+add_task(function test_momentary_401_info_collections_loggedout() {
+ _("Test a failure for info/collections before login that's resolved by reassignment.");
+
+ let oldHandler;
+ let sawTokenFetch = false;
+
+ function afterTokenFetch() {
+ // After a single token fetch, we undo our evil handleReassign hack, so
+ // the next /info request returns the collection instead of a 401
+ server.toplevelHandlers.info = oldHandler;
+ sawTokenFetch = true;
+ }
+
+ let server = yield prepareServer(afterTokenFetch);
+
+ // Return a 401 for the next /info request - it will be reset immediately
+ // after a new token is fetched.
+ oldHandler = server.toplevelHandlers.info
+ server.toplevelHandlers.info = handleReassign;
+
+ do_check_false(Service.isLoggedIn, "not already logged in");
+
+ Service.sync();
+ do_check_eq(Status.sync, SYNC_SUCCEEDED, "sync succeeded");
+ // sync was successful - check we grabbed a new token.
+ do_check_true(sawTokenFetch, "a new token was fetched by this test.")
+ // and we are done.
+ Service.startOver();
+ let deferred = Promise.defer();
+ server.stop(deferred.resolve);
+ yield deferred.promise;
+});
+
+// This test ends up being a failing meta/global fetch *after we're already logged in*.
+add_task(function test_momentary_401_storage_loggedin() {
+ _("Test a failure for any storage URL after login that's resolved by" +
+ "reassignment.");
+ let server = yield prepareServer();
+
+ _("First sync to prepare server contents.");
+ Service.sync();
+
+ _("Arrange for meta/global to return a 401.");
+ let oldHandler = server.toplevelHandlers.storage;
+ server.toplevelHandlers.storage = handleReassign;
+
+ function undo() {
+ _("Undoing test changes.");
+ server.toplevelHandlers.storage = oldHandler;
+ }
+
+ do_check_true(Service.isLoggedIn, "already logged in");
+
+ yield syncAndExpectNodeReassignment(server,
+ "weave:service:sync:error",
+ undo,
+ "weave:service:sync:finish",
+ Service.storageURL + "meta/global");
+});
+
+// This test ends up being a failing meta/global fetch *before we've logged in*.
+add_task(function test_momentary_401_storage_loggedout() {
+ _("Test a failure for any storage URL before login, not just engine parts. " +
+ "Resolved by reassignment.");
+ let server = yield prepareServer();
+
+ // Return a 401 for all storage requests.
+ let oldHandler = server.toplevelHandlers.storage;
+ server.toplevelHandlers.storage = handleReassign;
+
+ function undo() {
+ _("Undoing test changes.");
+ server.toplevelHandlers.storage = oldHandler;
+ }
+
+ do_check_false(Service.isLoggedIn, "already logged in");
+
+ yield syncAndExpectNodeReassignment(server,
+ "weave:service:login:error",
+ undo,
+ "weave:service:sync:finish",
+ Service.storageURL + "meta/global");
+});
+
diff --git a/services/sync/tests/unit/test_fxa_service_cluster.js b/services/sync/tests/unit/test_fxa_service_cluster.js
new file mode 100644
index 000000000..f6f97184a
--- /dev/null
+++ b/services/sync/tests/unit/test_fxa_service_cluster.js
@@ -0,0 +1,68 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+Cu.import("resource://services-sync/service.js");
+Cu.import("resource://services-sync/util.js");
+Cu.import("resource://testing-common/services/sync/fxa_utils.js");
+Cu.import("resource://testing-common/services/sync/utils.js");
+
+add_task(function test_findCluster() {
+ _("Test FxA _findCluster()");
+
+ _("_findCluster() throws on 500 errors.");
+ initializeIdentityWithTokenServerResponse({
+ status: 500,
+ headers: [],
+ body: "",
+ });
+
+ yield Service.identity.initializeWithCurrentIdentity();
+ yield Assert.rejects(Service.identity.whenReadyToAuthenticate.promise,
+ "should reject due to 500");
+
+ Assert.throws(function() {
+ Service._clusterManager._findCluster();
+ });
+
+ _("_findCluster() returns null on authentication errors.");
+ initializeIdentityWithTokenServerResponse({
+ status: 401,
+ headers: {"content-type": "application/json"},
+ body: "{}",
+ });
+
+ yield Service.identity.initializeWithCurrentIdentity();
+ yield Assert.rejects(Service.identity.whenReadyToAuthenticate.promise,
+ "should reject due to 401");
+
+ cluster = Service._clusterManager._findCluster();
+ Assert.strictEqual(cluster, null);
+
+ _("_findCluster() works with correct tokenserver response.");
+ let endpoint = "http://example.com/something";
+ initializeIdentityWithTokenServerResponse({
+ status: 200,
+ headers: {"content-type": "application/json"},
+ body:
+ JSON.stringify({
+ api_endpoint: endpoint,
+ duration: 300,
+ id: "id",
+ key: "key",
+ uid: "uid",
+ })
+ });
+
+ yield Service.identity.initializeWithCurrentIdentity();
+ yield Service.identity.whenReadyToAuthenticate.promise;
+ cluster = Service._clusterManager._findCluster();
+ // The cluster manager ensures a trailing "/"
+ Assert.strictEqual(cluster, endpoint + "/");
+
+ Svc.Prefs.resetBranch("");
+});
+
+function run_test() {
+ initTestLogging();
+ run_next_test();
+}
diff --git a/services/sync/tests/unit/test_fxa_startOver.js b/services/sync/tests/unit/test_fxa_startOver.js
new file mode 100644
index 000000000..e27d86ea0
--- /dev/null
+++ b/services/sync/tests/unit/test_fxa_startOver.js
@@ -0,0 +1,63 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+Cu.import("resource://testing-common/services/sync/utils.js");
+Cu.import("resource://services-sync/identity.js");
+Cu.import("resource://services-sync/browserid_identity.js");
+Cu.import("resource://services-sync/service.js");
+
+function run_test() {
+ initTestLogging("Trace");
+ run_next_test();
+}
+
+add_task(function* test_startover() {
+ let oldValue = Services.prefs.getBoolPref("services.sync-testing.startOverKeepIdentity", true);
+ Services.prefs.setBoolPref("services.sync-testing.startOverKeepIdentity", false);
+
+ ensureLegacyIdentityManager();
+ yield configureIdentity({username: "johndoe"});
+
+ // The boolean flag on the xpcom service should reflect a legacy provider.
+ let xps = Cc["@mozilla.org/weave/service;1"]
+ .getService(Components.interfaces.nsISupports)
+ .wrappedJSObject;
+ do_check_false(xps.fxAccountsEnabled);
+
+ // we expect the "legacy" provider (but can't instanceof that, as BrowserIDManager
+ // extends it)
+ do_check_false(Service.identity instanceof BrowserIDManager);
+
+ Service.serverURL = "https://localhost/";
+ Service.clusterURL = Service.serverURL;
+
+ Service.login();
+ // We should have a cluster URL
+ do_check_true(Service.clusterURL.length > 0);
+
+ // remember some stuff so we can reset it after.
+ let oldIdentity = Service.identity;
+ let oldClusterManager = Service._clusterManager;
+ let deferred = Promise.defer();
+ Services.obs.addObserver(function observeStartOverFinished() {
+ Services.obs.removeObserver(observeStartOverFinished, "weave:service:start-over:finish");
+ deferred.resolve();
+ }, "weave:service:start-over:finish", false);
+
+ Service.startOver();
+ yield deferred.promise; // wait for the observer to fire.
+
+ // the xpcom service should indicate FxA is enabled.
+ do_check_true(xps.fxAccountsEnabled);
+ // should have swapped identities.
+ do_check_true(Service.identity instanceof BrowserIDManager);
+ // should have clobbered the cluster URL
+ do_check_eq(Service.clusterURL, "");
+
+ // we should have thrown away the old identity provider and cluster manager.
+ do_check_neq(oldIdentity, Service.identity);
+ do_check_neq(oldClusterManager, Service._clusterManager);
+
+ // reset the world.
+ Services.prefs.setBoolPref("services.sync-testing.startOverKeepIdentity", oldValue);
+});
diff --git a/services/sync/tests/unit/test_healthreport.js b/services/sync/tests/unit/test_healthreport.js
new file mode 100644
index 000000000..486320b6a
--- /dev/null
+++ b/services/sync/tests/unit/test_healthreport.js
@@ -0,0 +1,194 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+Cu.import("resource://gre/modules/Metrics.jsm", this);
+Cu.import("resource://gre/modules/Preferences.jsm", this);
+Cu.import("resource://gre/modules/Promise.jsm", this);
+Cu.import("resource://services-sync/main.js", this);
+Cu.import("resource://services-sync/healthreport.jsm", this);
+Cu.import("resource://testing-common/services/common/logging.js", this);
+Cu.import("resource://testing-common/services/healthreport/utils.jsm", this);
+
+function run_test() {
+ initTestLogging();
+
+ run_next_test();
+}
+
+add_task(function test_constructor() {
+ let provider = new SyncProvider();
+});
+
+// Provider can initialize and de-initialize properly.
+add_task(function* test_init() {
+ let storage = yield Metrics.Storage("init");
+ let provider = new SyncProvider();
+ yield provider.init(storage);
+ yield provider.shutdown();
+ yield storage.close();
+});
+
+add_task(function* test_collect() {
+ let storage = yield Metrics.Storage("collect");
+ let provider = new SyncProvider();
+ yield provider.init(storage);
+
+ // Initially nothing should be configured.
+ let now = new Date();
+ yield provider.collectDailyData();
+
+ let m = provider.getMeasurement("sync", 1);
+ let values = yield m.getValues();
+ Assert.equal(values.days.size, 1);
+ Assert.ok(values.days.hasDay(now));
+ let day = values.days.getDay(now);
+ Assert.ok(day.has("enabled"));
+ Assert.ok(day.has("activeProtocol"));
+ Assert.ok(day.has("preferredProtocol"));
+ Assert.equal(day.get("enabled"), 0);
+ Assert.equal(day.get("preferredProtocol"), "1.5");
+ Assert.equal(day.get("activeProtocol"), "1.5",
+ "Protocol without setup should be FX Accounts version.");
+
+ // Now check for old Sync setup.
+ let branch = new Preferences("services.sync.");
+ branch.set("username", "foo");
+ branch.reset("fxaccounts.enabled");
+ yield provider.collectDailyData();
+ values = yield m.getValues();
+ Assert.equal(values.days.getDay(now).get("activeProtocol"), "1.1",
+ "Protocol with old Sync setup is correct.");
+
+ Assert.equal(Weave.Status.__authManager, undefined, "Detect code changes");
+
+ // Let's enable Sync so we can get more useful data.
+ // We need to do this because the FHR probe only records more info if Sync
+ // is configured properly.
+ Weave.Service.identity.account = "johndoe";
+ Weave.Service.identity.basicPassword = "ilovejane";
+ Weave.Service.identity.syncKey = Weave.Utils.generatePassphrase();
+ Weave.Service.clusterURL = "http://localhost/";
+ Assert.equal(Weave.Status.checkSetup(), Weave.STATUS_OK);
+
+ yield provider.collectDailyData();
+ values = yield m.getValues();
+ day = values.days.getDay(now);
+ Assert.equal(day.get("enabled"), 1);
+
+ // An empty account should have 1 device: us.
+ let dm = provider.getMeasurement("devices", 1);
+ values = yield dm.getValues();
+ Assert.ok(values.days.hasDay(now));
+ day = values.days.getDay(now);
+ Assert.equal(day.size, 1);
+ let engine = Weave.Service.clientsEngine;
+ Assert.ok(engine);
+ Assert.ok(day.has(engine.localType));
+ Assert.equal(day.get(engine.localType), 1);
+
+ // Add some devices and ensure they show up.
+ engine._store._remoteClients["id1"] = {type: "mobile"};
+ engine._store._remoteClients["id2"] = {type: "tablet"};
+ engine._store._remoteClients["id3"] = {type: "mobile"};
+
+ yield provider.collectDailyData();
+ values = yield dm.getValues();
+ day = values.days.getDay(now);
+
+ let expected = {
+ "foobar": 0,
+ "tablet": 1,
+ "mobile": 2,
+ "desktop": 0,
+ };
+
+ for (let type in expected) {
+ let count = expected[type];
+
+ if (engine.localType == type) {
+ count++;
+ }
+
+ if (!count) {
+ Assert.ok(!day.has(type));
+ } else {
+ Assert.ok(day.has(type));
+ Assert.equal(day.get(type), count);
+ }
+ }
+
+ engine._store._remoteClients = {};
+
+ yield provider.shutdown();
+ yield storage.close();
+});
+
+add_task(function* test_sync_events() {
+ let storage = yield Metrics.Storage("sync_events");
+ let provider = new SyncProvider();
+ yield provider.init(storage);
+
+ let m = provider.getMeasurement("sync", 1);
+
+ for (let i = 0; i < 5; i++) {
+ Services.obs.notifyObservers(null, "weave:service:sync:start", null);
+ }
+
+ for (let i = 0; i < 3; i++) {
+ Services.obs.notifyObservers(null, "weave:service:sync:finish", null);
+ }
+
+ for (let i = 0; i < 2; i++) {
+ Services.obs.notifyObservers(null, "weave:service:sync:error", null);
+ }
+
+ // Wait for storage to complete.
+ yield m.storage.enqueueOperation(() => {
+ return Promise.resolve();
+ });
+
+ let values = yield m.getValues();
+ let now = new Date();
+ Assert.ok(values.days.hasDay(now));
+ let day = values.days.getDay(now);
+
+ Assert.ok(day.has("syncStart"));
+ Assert.ok(day.has("syncSuccess"));
+ Assert.ok(day.has("syncError"));
+ Assert.equal(day.get("syncStart"), 5);
+ Assert.equal(day.get("syncSuccess"), 3);
+ Assert.equal(day.get("syncError"), 2);
+
+ yield provider.shutdown();
+ yield storage.close();
+});
+
+add_task(function* test_healthreporter_json() {
+ let reporter = yield getHealthReporter("healthreporter_json");
+ yield reporter.init();
+ try {
+ yield reporter._providerManager.registerProvider(new SyncProvider());
+ yield reporter.collectMeasurements();
+ let payload = yield reporter.getJSONPayload(true);
+ let now = new Date();
+ let today = reporter._formatDate(now);
+
+ Assert.ok(today in payload.data.days);
+ let day = payload.data.days[today];
+
+ Assert.ok("org.mozilla.sync.sync" in day);
+ Assert.ok("org.mozilla.sync.devices" in day);
+
+ let devices = day["org.mozilla.sync.devices"];
+ let engine = Weave.Service.clientsEngine;
+ Assert.ok(engine);
+ let type = engine.localType;
+ Assert.ok(type);
+ Assert.ok(type in devices);
+ Assert.equal(devices[type], 1);
+ } finally {
+ reporter._shutdown();
+ }
+});
diff --git a/services/sync/tests/unit/test_healthreport_migration.js b/services/sync/tests/unit/test_healthreport_migration.js
new file mode 100644
index 000000000..23f756748
--- /dev/null
+++ b/services/sync/tests/unit/test_healthreport_migration.js
@@ -0,0 +1,155 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+Cu.import("resource://gre/modules/Metrics.jsm", this);
+Cu.import("resource://gre/modules/Preferences.jsm", this);
+Cu.import("resource://gre/modules/Promise.jsm", this);
+Cu.import("resource://services-sync/healthreport.jsm", this);
+Cu.import("resource://services-sync/FxaMigrator.jsm", this);
+Cu.import("resource://testing-common/services/common/logging.js", this);
+Cu.import("resource://testing-common/services/healthreport/utils.jsm", this);
+
+
+function run_test() {
+ initTestLogging();
+
+ run_next_test();
+}
+
+add_task(function* test_no_data() {
+ let storage = yield Metrics.Storage("collect");
+ let provider = new SyncProvider();
+ yield provider.init(storage);
+
+ try {
+ // Initially nothing should be configured.
+ let now = new Date();
+ yield provider.collectDailyData();
+
+ let m = provider.getMeasurement("migration", 1);
+ let values = yield m.getValues();
+ Assert.equal(values.days.size, 0);
+ Assert.ok(!values.days.hasDay(now));
+ } finally {
+ yield provider.shutdown();
+ yield storage.close();
+ }
+});
+
+function checkCorrectStateRecorded(provider, state) {
+ // Wait for storage to complete.
+ yield m.storage.enqueueOperation(() => {
+ return Promise.resolve();
+ });
+
+ let m = provider.getMeasurement("migration", 1);
+ let values = yield m.getValues();
+ Assert.equal(values.days.size, 1);
+ Assert.ok(values.days.hasDay(now));
+ let day = values.days.getDay(now);
+
+ Assert.ok(day.has("state"));
+ Assert.equal(day.get("state"), state);
+}
+
+add_task(function* test_state() {
+ let storage = yield Metrics.Storage("collect");
+ let provider = new SyncProvider();
+ yield provider.init(storage);
+
+ try {
+ // Initially nothing should be configured.
+ let now = new Date();
+
+ // We record both a "user" and "internal" state in the same field.
+ // So simulate a "user" state first.
+ Services.obs.notifyObservers(null, "fxa-migration:state-changed",
+ fxaMigrator.STATE_USER_FXA_VERIFIED);
+ checkCorrectStateRecorded(provider, fxaMigrator.STATE_USER_FXA_VERIFIED);
+
+ // And an internal state.
+ Services.obs.notifyObservers(null, "fxa-migration:internal-state-changed",
+ fxaMigrator.STATE_INTERNAL_WAITING_SYNC_COMPLETE);
+ checkCorrectStateRecorded(provider, fxaMigrator.STATE_INTERNAL_WAITING_SYNC_COMPLETE);
+ } finally {
+ yield provider.shutdown();
+ yield storage.close();
+ }
+});
+
+add_task(function* test_flags() {
+ let storage = yield Metrics.Storage("collect");
+ let provider = new SyncProvider();
+ yield provider.init(storage);
+
+ try {
+ // Initially nothing should be configured.
+ let now = new Date();
+
+ let m = provider.getMeasurement("migration", 1);
+
+ let record = function*(what) {
+ Services.obs.notifyObservers(null, "fxa-migration:internal-telemetry", what);
+ // Wait for storage to complete.
+ yield m.storage.enqueueOperation(Promise.resolve);
+ let values = yield m.getValues();
+ Assert.equal(values.days.size, 1);
+ return values.days.getDay(now);
+ }
+
+ let values = yield m.getValues();
+ Assert.equal(values.days.size, 1);
+ let day = values.days.getDay(now);
+ Assert.ok(!day.has(fxaMigrator.TELEMETRY_ACCEPTED));
+ Assert.ok(!day.has(fxaMigrator.TELEMETRY_DECLINED));
+ Assert.ok(!day.has(fxaMigrator.TELEMETRY_UNLINKED));
+
+ // let's send an unknown value to ensure our error mitigation works.
+ day = yield record("unknown");
+ Assert.ok(!day.has(fxaMigrator.TELEMETRY_ACCEPTED));
+ Assert.ok(!day.has(fxaMigrator.TELEMETRY_DECLINED));
+ Assert.ok(!day.has(fxaMigrator.TELEMETRY_UNLINKED));
+
+ // record an fxaMigrator.TELEMETRY_ACCEPTED state.
+ day = yield record(fxaMigrator.TELEMETRY_ACCEPTED);
+ Assert.ok(day.has(fxaMigrator.TELEMETRY_ACCEPTED));
+ Assert.ok(!day.has(fxaMigrator.TELEMETRY_DECLINED));
+ Assert.ok(!day.has(fxaMigrator.TELEMETRY_UNLINKED));
+ Assert.equal(day.get(fxaMigrator.TELEMETRY_ACCEPTED), 1);
+
+ // and again - it should get 2.
+ day = yield record(fxaMigrator.TELEMETRY_ACCEPTED);
+ Assert.equal(day.get(fxaMigrator.TELEMETRY_ACCEPTED), 2);
+
+ // record fxaMigrator.TELEMETRY_DECLINED - also a counter.
+ day = yield record(fxaMigrator.TELEMETRY_DECLINED);
+ Assert.ok(day.has(fxaMigrator.TELEMETRY_ACCEPTED));
+ Assert.ok(day.has(fxaMigrator.TELEMETRY_DECLINED));
+ Assert.ok(!day.has(fxaMigrator.TELEMETRY_UNLINKED));
+ Assert.equal(day.get(fxaMigrator.TELEMETRY_ACCEPTED), 2);
+ Assert.equal(day.get(fxaMigrator.TELEMETRY_DECLINED), 1);
+
+ day = yield record(fxaMigrator.TELEMETRY_DECLINED);
+ Assert.ok(day.has(fxaMigrator.TELEMETRY_ACCEPTED));
+ Assert.ok(day.has(fxaMigrator.TELEMETRY_DECLINED));
+ Assert.ok(!day.has(fxaMigrator.TELEMETRY_UNLINKED));
+ Assert.equal(day.get(fxaMigrator.TELEMETRY_ACCEPTED), 2);
+ Assert.equal(day.get(fxaMigrator.TELEMETRY_DECLINED), 2);
+
+ // and fxaMigrator.TELEMETRY_UNLINKED - this is conceptually a "daily bool".
+ // (ie, it's DAILY_LAST_NUMERIC_FIELD and only ever has |1| written to it)
+ day = yield record(fxaMigrator.TELEMETRY_UNLINKED);
+ Assert.ok(day.has(fxaMigrator.TELEMETRY_ACCEPTED));
+ Assert.ok(day.has(fxaMigrator.TELEMETRY_DECLINED));
+ Assert.ok(day.has(fxaMigrator.TELEMETRY_UNLINKED));
+ Assert.equal(day.get(fxaMigrator.TELEMETRY_UNLINKED), 1);
+ // and doing it again still leaves us with |1|
+ day = yield record(fxaMigrator.TELEMETRY_UNLINKED);
+ Assert.equal(day.get(fxaMigrator.TELEMETRY_UNLINKED), 1);
+ } finally {
+ yield provider.shutdown();
+ yield storage.close();
+ }
+});
diff --git a/services/sync/tests/unit/test_history_engine.js b/services/sync/tests/unit/test_history_engine.js
index ea2e7de82..fd5067ce9 100644
--- a/services/sync/tests/unit/test_history_engine.js
+++ b/services/sync/tests/unit/test_history_engine.js
@@ -12,15 +12,16 @@ Cu.import("resource://testing-common/services/sync/utils.js");
Service.engineManager.clear();
+add_test(function test_setup() {
+ PlacesTestUtils.clearHistory().then(run_next_test);
+});
+
add_test(function test_processIncoming_mobile_history_batched() {
_("SyncEngine._processIncoming works on history engine.");
let FAKE_DOWNLOAD_LIMIT = 100;
- new SyncTestingInfrastructure();
-
Svc.Prefs.set("client.type", "mobile");
- PlacesUtils.history.removeAllPages();
Service.engineManager.register(HistoryEngine);
// A collection that logs each GET
@@ -32,6 +33,12 @@ add_test(function test_processIncoming_mobile_history_batched() {
return this._get(options);
};
+ let server = sync_httpd_setup({
+ "/1.1/foo/storage/history": collection.handler()
+ });
+
+ new SyncTestingInfrastructure(server);
+
// Let's create some 234 server side history records. They're all at least
// 10 minutes old.
let visitType = Ci.nsINavHistoryService.TRANSITION_LINK;
@@ -51,10 +58,6 @@ add_test(function test_processIncoming_mobile_history_batched() {
collection.insertWBO(wbo);
}
- let server = sync_httpd_setup({
- "/1.1/foo/storage/history": collection.handler()
- });
-
let engine = Service.engineManager.get("history");
let meta_global = Service.recordManager.set(engine.metaURL,
new WBORecord(engine.metaURL));
@@ -129,10 +132,11 @@ add_test(function test_processIncoming_mobile_history_batched() {
}
} finally {
- PlacesUtils.history.removeAllPages();
- server.stop(do_test_finished);
- Svc.Prefs.resetBranch("");
- Service.recordManager.clearCache();
+ PlacesTestUtils.clearHistory().then(() => {
+ server.stop(do_test_finished);
+ Svc.Prefs.resetBranch("");
+ Service.recordManager.clearCache();
+ });
}
});
diff --git a/services/sync/tests/unit/test_history_store.js b/services/sync/tests/unit/test_history_store.js
index 0839f6e22..2381f103d 100644
--- a/services/sync/tests/unit/test_history_store.js
+++ b/services/sync/tests/unit/test_history_store.js
@@ -1,6 +1,7 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
+Cu.import("resource://gre/modules/PlacesUtils.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://services-common/async.js");
Cu.import("resource://services-sync/engines/history.js");
@@ -61,7 +62,7 @@ function ensureThrows(func) {
try {
func.apply(this, arguments);
} catch (ex) {
- PlacesUtils.history.removeAllPages();
+ PlacesTestUtils.clearHistory();
do_throw(ex);
}
};
@@ -299,6 +300,5 @@ add_test(function test_remove() {
add_test(function cleanup() {
_("Clean up.");
- PlacesUtils.history.removeAllPages();
- run_next_test();
+ PlacesTestUtils.clearHistory().then(run_next_test);
});
diff --git a/services/sync/tests/unit/test_history_tracker.js b/services/sync/tests/unit/test_history_tracker.js
index 2eb9ea596..ca1090b79 100644
--- a/services/sync/tests/unit/test_history_tracker.js
+++ b/services/sync/tests/unit/test_history_tracker.js
@@ -1,8 +1,8 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
-Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/PlacesUtils.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://services-sync/engines.js");
Cu.import("resource://services-sync/constants.js");
Cu.import("resource://services-sync/engines/history.js");
@@ -64,7 +64,7 @@ function addVisit() {
function run_test() {
initTestLogging("Trace");
- Log4Moz.repository.getLogger("Sync.Tracker.History").level = Log4Moz.Level.Trace;
+ Log.repository.getLogger("Sync.Tracker.History").level = Log.Level.Trace;
run_next_test();
}
@@ -72,7 +72,7 @@ add_test(function test_empty() {
_("Verify we've got an empty, disabled tracker to work with.");
do_check_empty(tracker.changedIDs);
do_check_eq(tracker.score, 0);
- do_check_false(tracker._enabled);
+ do_check_false(tracker._isTracking);
run_next_test();
});
@@ -199,6 +199,5 @@ add_test(function test_stop_tracking_twice() {
add_test(function cleanup() {
_("Clean up.");
- PlacesUtils.history.removeAllPages();
- run_next_test();
+ PlacesTestUtils.clearHistory().then(run_next_test);
});
diff --git a/services/sync/tests/unit/test_hmac_error.js b/services/sync/tests/unit/test_hmac_error.js
index ca6b2507b..e41ff3797 100644
--- a/services/sync/tests/unit/test_hmac_error.js
+++ b/services/sync/tests/unit/test_hmac_error.js
@@ -21,9 +21,8 @@ function shared_setup() {
hmacErrorCount = 0;
// Do not instantiate SyncTestingInfrastructure; we need real crypto.
+ ensureLegacyIdentityManager();
setBasicCredentials("foo", "foo", "aabcdeabcdeabcdeabcdeabcde");
- Service.serverURL = TEST_SERVER_URL;
- Service.clusterURL = TEST_CLUSTER_URL;
// Make sure RotaryEngine is the only one we sync.
Service.engineManager._engines = {};
@@ -80,6 +79,7 @@ add_test(function hmac_error_during_404() {
};
let server = sync_httpd_setup(handlers);
+ Service.serverURL = server.baseURI;
try {
_("Syncing.");
@@ -155,6 +155,7 @@ add_test(function hmac_error_during_node_reassignment() {
};
let server = sync_httpd_setup(handlers);
+ Service.serverURL = server.baseURI;
_("Syncing.");
// First hit of clients will 401. This will happen after meta/global and
// keys -- i.e., in the middle of the sync, but before RotaryEngine.
diff --git a/services/sync/tests/unit/test_httpd_sync_server.js b/services/sync/tests/unit/test_httpd_sync_server.js
index 36121fd64..943dbfd73 100644
--- a/services/sync/tests/unit/test_httpd_sync_server.js
+++ b/services/sync/tests/unit/test_httpd_sync_server.js
@@ -4,7 +4,7 @@
Cu.import("resource://services-sync/util.js");
function run_test() {
- Log4Moz.repository.getLogger("Sync.Test.Server").level = Log4Moz.Level.Trace;
+ Log.repository.getLogger("Sync.Test.Server").level = Log.Level.Trace;
initTestLogging();
run_next_test();
}
@@ -36,7 +36,7 @@ add_test(function test_url_parsing() {
// Check that we can parse a collection URI.
parts = server.pathRE.exec("/1.1/johnsmith/storage/crypto");
- let [all, version, username, first, rest] = parts;
+ [all, version, username, first, rest] = parts;
do_check_eq(all, "/1.1/johnsmith/storage/crypto");
do_check_eq(version, "1.1");
do_check_eq(username, "johnsmith");
@@ -49,7 +49,7 @@ add_test(function test_url_parsing() {
// storage alone is a valid request.
parts = server.pathRE.exec("/1.1/johnsmith/storage");
- let [all, version, username, first, rest] = parts;
+ [all, version, username, first, rest] = parts;
do_check_eq(all, "/1.1/johnsmith/storage");
do_check_eq(version, "1.1");
do_check_eq(username, "johnsmith");
@@ -57,7 +57,8 @@ add_test(function test_url_parsing() {
do_check_eq(rest, undefined);
parts = server.storageRE.exec("storage");
- let [all, storage, collection, id] = parts;
+ let storage, collection, id;
+ [all, storage, collection, id] = parts;
do_check_eq(all, "storage");
do_check_eq(collection, undefined);
@@ -65,9 +66,9 @@ add_test(function test_url_parsing() {
});
Cu.import("resource://services-common/rest.js");
-function localRequest(path) {
+function localRequest(server, path) {
_("localRequest: " + path);
- let url = "http://127.0.0.1:8080" + path;
+ let url = server.baseURI.substr(0, server.baseURI.length - 1) + path;
_("url: " + url);
return new RESTRequest(url);
}
@@ -76,11 +77,10 @@ add_test(function test_basic_http() {
let server = new SyncServer();
server.registerUser("john", "password");
do_check_true(server.userExists("john"));
- server.start(8080, function () {
+ server.start(null, function () {
_("Started on " + server.port);
- do_check_eq(server.port, 8080);
Utils.nextTick(function () {
- let req = localRequest("/1.1/john/storage/crypto/keys");
+ let req = localRequest(server, "/1.1/john/storage/crypto/keys");
_("req is " + req);
req.get(function (err) {
do_check_eq(null, err);
@@ -103,10 +103,9 @@ add_test(function test_info_collections() {
}
server.registerUser("john", "password");
- server.start(8080, function () {
- do_check_eq(server.port, 8080);
+ server.start(null, function () {
Utils.nextTick(function () {
- let req = localRequest("/1.1/john/info/collections");
+ let req = localRequest(server, "/1.1/john/info/collections");
req.get(function (err) {
// Initial info/collections fetch is empty.
do_check_eq(null, err);
@@ -121,7 +120,7 @@ add_test(function test_info_collections() {
let putResponseBody = this.response.body;
_("PUT response body: " + JSON.stringify(putResponseBody));
- req = localRequest("/1.1/john/info/collections");
+ req = localRequest(server, "/1.1/john/info/collections");
req.get(function (err) {
do_check_eq(null, err);
responseHasCorrectHeaders(this.response);
@@ -137,7 +136,7 @@ add_test(function test_info_collections() {
});
}
let payload = JSON.stringify({foo: "bar"});
- localRequest("/1.1/john/storage/crypto/keys").put(payload, cb);
+ localRequest(server, "/1.1/john/storage/crypto/keys").put(payload, cb);
});
});
});
@@ -163,7 +162,7 @@ add_test(function test_storage_request() {
do_check_true(coll.timestamp >= creation);
function retrieveWBONotExists(next) {
- let req = localRequest(keysURL);
+ let req = localRequest(server, keysURL);
req.get(function (err) {
_("Body is " + this.response.body);
_("Modified is " + this.response.newModified);
@@ -174,7 +173,7 @@ add_test(function test_storage_request() {
});
}
function retrieveWBOExists(next) {
- let req = localRequest(foosURL);
+ let req = localRequest(server, foosURL);
req.get(function (err) {
_("Body is " + this.response.body);
_("Modified is " + this.response.newModified);
@@ -186,7 +185,7 @@ add_test(function test_storage_request() {
});
}
function deleteWBONotExists(next) {
- let req = localRequest(keysURL);
+ let req = localRequest(server, keysURL);
server.callback.onItemDeleted = function (username, collection, wboID) {
do_throw("onItemDeleted should not have been called.");
};
@@ -200,7 +199,7 @@ add_test(function test_storage_request() {
});
}
function deleteWBOExists(next) {
- let req = localRequest(foosURL);
+ let req = localRequest(server, foosURL);
server.callback.onItemDeleted = function (username, collection, wboID) {
_("onItemDeleted called for " + collection + "/" + wboID);
delete server.callback.onItemDeleted;
@@ -220,7 +219,7 @@ add_test(function test_storage_request() {
_("Testing DELETE on /storage.");
let now = server.timestamp();
_("Timestamp: " + now);
- let req = localRequest(storageURL);
+ let req = localRequest(server, storageURL);
req.delete(function (err) {
_("Body is " + this.response.body);
_("Modified is " + this.response.newModified);
@@ -232,7 +231,7 @@ add_test(function test_storage_request() {
}
function getStorageFails(next) {
_("Testing that GET on /storage fails.");
- let req = localRequest(storageURL);
+ let req = localRequest(server, storageURL);
req.get(function (err) {
do_check_eq(this.response.status, 405);
do_check_eq(this.response.headers["allow"], "DELETE");
@@ -241,14 +240,14 @@ add_test(function test_storage_request() {
}
function getMissingCollectionWBO(next) {
_("Testing that fetching a WBO from an on-existent collection 404s.");
- let req = localRequest(storageURL + "/foobar/baz");
+ let req = localRequest(server, storageURL + "/foobar/baz");
req.get(function (err) {
do_check_eq(this.response.status, 404);
Utils.nextTick(next);
});
}
- server.start(8080,
+ server.start(null,
Async.chain(
retrieveWBONotExists,
retrieveWBOExists,
@@ -270,12 +269,12 @@ add_test(function test_x_weave_records() {
crypto: {foos: {foo: "bar"},
bars: {foo: "baz"}}
});
- server.start(8080, function () {
- let wbo = localRequest("/1.1/john/storage/crypto/foos");
+ server.start(null, function () {
+ let wbo = localRequest(server, "/1.1/john/storage/crypto/foos");
wbo.get(function (err) {
// WBO fetches don't have one.
do_check_false("x-weave-records" in this.response.headers);
- let col = localRequest("/1.1/john/storage/crypto");
+ let col = localRequest(server, "/1.1/john/storage/crypto");
col.get(function (err) {
// Collection fetches do.
do_check_eq(this.response.headers["x-weave-records"], "2");
diff --git a/services/sync/tests/unit/test_identity_manager.js b/services/sync/tests/unit/test_identity_manager.js
index d377319f4..97dace95f 100644
--- a/services/sync/tests/unit/test_identity_manager.js
+++ b/services/sync/tests/unit/test_identity_manager.js
@@ -9,7 +9,7 @@ let identity = new IdentityManager();
function run_test() {
initTestLogging("Trace");
- Log4Moz.repository.getLogger("Sync.Identity").level = Log4Moz.Level.Trace;
+ Log.repository.getLogger("Sync.Identity").level = Log.Level.Trace;
run_next_test();
}
diff --git a/services/sync/tests/unit/test_interval_triggers.js b/services/sync/tests/unit/test_interval_triggers.js
index 65fdaf3f7..0f355e636 100644
--- a/services/sync/tests/unit/test_interval_triggers.js
+++ b/services/sync/tests/unit/test_interval_triggers.js
@@ -13,6 +13,12 @@ Cu.import("resource://services-sync/service.js");
let scheduler = Service.scheduler;
let clientsEngine = Service.clientsEngine;
+function promiseStopServer(server) {
+ let deferred = Promise.defer();
+ server.stop(deferred.resolve);
+ return deferred.promise;
+}
+
function sync_httpd_setup() {
let global = new ServerWBO("global", {
syncID: Service.syncID,
@@ -35,27 +41,26 @@ function sync_httpd_setup() {
});
}
-function setUp() {
- setBasicCredentials("johndoe", "ilovejane", "abcdeabcdeabcdeabcdeabcdea");
- Service.serverURL = TEST_SERVER_URL;
- Service.clusterURL = TEST_CLUSTER_URL;
-
+function setUp(server) {
+ yield configureIdentity({username: "johndoe"});
+ Service.serverURL = server.baseURI + "/";
+ Service.clusterURL = server.baseURI + "/";
generateNewKeys(Service.collectionKeys);
let serverKeys = Service.collectionKeys.asWBO("crypto", "keys");
serverKeys.encrypt(Service.identity.syncKeyBundle);
- return serverKeys.upload(Service.resource(Service.cryptoKeysURL));
+ serverKeys.upload(Service.resource(Service.cryptoKeysURL));
}
function run_test() {
initTestLogging("Trace");
- Log4Moz.repository.getLogger("Sync.Service").level = Log4Moz.Level.Trace;
- Log4Moz.repository.getLogger("Sync.SyncScheduler").level = Log4Moz.Level.Trace;
+ Log.repository.getLogger("Sync.Service").level = Log.Level.Trace;
+ Log.repository.getLogger("Sync.SyncScheduler").level = Log.Level.Trace;
run_next_test();
}
-add_test(function test_successful_sync_adjustSyncInterval() {
+add_identity_test(this, function test_successful_sync_adjustSyncInterval() {
_("Test successful sync calling adjustSyncInterval");
let syncSuccesses = 0;
function onSyncFinish() {
@@ -65,7 +70,7 @@ add_test(function test_successful_sync_adjustSyncInterval() {
Svc.Obs.add("weave:service:sync:finish", onSyncFinish);
let server = sync_httpd_setup();
- setUp();
+ yield setUp(server);
// Confirm defaults
do_check_false(scheduler.idle);
@@ -151,10 +156,10 @@ add_test(function test_successful_sync_adjustSyncInterval() {
Svc.Obs.remove("weave:service:sync:finish", onSyncFinish);
Service.startOver();
- server.stop(run_next_test);
+ yield promiseStopServer(server);
});
-add_test(function test_unsuccessful_sync_adjustSyncInterval() {
+add_identity_test(this, function test_unsuccessful_sync_adjustSyncInterval() {
_("Test unsuccessful sync calling adjustSyncInterval");
let syncFailures = 0;
@@ -169,7 +174,7 @@ add_test(function test_unsuccessful_sync_adjustSyncInterval() {
Svc.Prefs.set("firstSync", "notReady");
let server = sync_httpd_setup();
- setUp();
+ yield setUp(server);
// Confirm defaults
do_check_false(scheduler.idle);
@@ -256,22 +261,23 @@ add_test(function test_unsuccessful_sync_adjustSyncInterval() {
Service.startOver();
Svc.Obs.remove("weave:service:sync:error", onSyncError);
- server.stop(run_next_test);
+ yield promiseStopServer(server);
});
-add_test(function test_back_triggers_sync() {
+add_identity_test(this, function test_back_triggers_sync() {
let server = sync_httpd_setup();
- setUp();
+ yield setUp(server);
// Single device: no sync triggered.
scheduler.idle = true;
- scheduler.observe(null, "back", Svc.Prefs.get("scheduler.idleTime"));
+ scheduler.observe(null, "active", Svc.Prefs.get("scheduler.idleTime"));
do_check_false(scheduler.idle);
// Multiple devices: sync is triggered.
clientsEngine._store.create({id: "foo", cleartext: "bar"});
scheduler.updateClientMode();
+ let deferred = Promise.defer();
Svc.Obs.add("weave:service:sync:finish", function onSyncFinish() {
Svc.Obs.remove("weave:service:sync:finish", onSyncFinish);
@@ -281,17 +287,18 @@ add_test(function test_back_triggers_sync() {
clientsEngine.resetClient();
Service.startOver();
- server.stop(run_next_test);
+ server.stop(deferred.resolve);
});
scheduler.idle = true;
- scheduler.observe(null, "back", Svc.Prefs.get("scheduler.idleTime"));
+ scheduler.observe(null, "active", Svc.Prefs.get("scheduler.idleTime"));
do_check_false(scheduler.idle);
+ yield deferred.promise;
});
-add_test(function test_adjust_interval_on_sync_error() {
+add_identity_test(this, function test_adjust_interval_on_sync_error() {
let server = sync_httpd_setup();
- setUp();
+ yield setUp(server);
let syncFailures = 0;
function onSyncError() {
@@ -317,17 +324,17 @@ add_test(function test_adjust_interval_on_sync_error() {
Svc.Obs.remove("weave:service:sync:error", onSyncError);
Service.startOver();
- server.stop(run_next_test);
+ yield promiseStopServer(server);
});
-add_test(function test_bug671378_scenario() {
+add_identity_test(this, function test_bug671378_scenario() {
// Test scenario similar to bug 671378. This bug appeared when a score
// update occurred that wasn't large enough to trigger a sync so
// scheduleNextSync() was called without a time interval parameter,
// setting nextSync to a non-zero value and preventing the timer from
// being adjusted in the next call to scheduleNextSync().
let server = sync_httpd_setup();
- setUp();
+ yield setUp(server);
let syncSuccesses = 0;
function onSyncFinish() {
@@ -343,6 +350,7 @@ add_test(function test_bug671378_scenario() {
do_check_eq(scheduler.syncInterval, scheduler.singleDeviceInterval);
do_check_eq(scheduler.syncTimer.delay, scheduler.singleDeviceInterval);
+ let deferred = Promise.defer();
// Wrap scheduleNextSync so we are notified when it is finished.
scheduler._scheduleNextSync = scheduler.scheduleNextSync;
scheduler.scheduleNextSync = function() {
@@ -358,7 +366,7 @@ add_test(function test_bug671378_scenario() {
scheduler.scheduleNextSync = scheduler._scheduleNextSync;
Svc.Obs.remove("weave:service:sync:finish", onSyncFinish);
Service.startOver();
- server.stop(run_next_test);
+ server.stop(deferred.resolve);
}
};
@@ -381,6 +389,7 @@ add_test(function test_bug671378_scenario() {
clientsEngine._store.create({id: "foo", cleartext: "bar"});
Service.sync();
+ yield deferred.promise;
});
add_test(function test_adjust_timer_larger_syncInterval() {
diff --git a/services/sync/tests/unit/test_jpakeclient.js b/services/sync/tests/unit/test_jpakeclient.js
index fa705717b..ff13c5716 100644
--- a/services/sync/tests/unit/test_jpakeclient.js
+++ b/services/sync/tests/unit/test_jpakeclient.js
@@ -1,4 +1,4 @@
-Cu.import("resource://services-common/log4moz.js");
+Cu.import("resource://gre/modules/Log.jsm");
Cu.import("resource://services-sync/identity.js");
Cu.import("resource://services-sync/jpakeclient.js");
Cu.import("resource://services-sync/constants.js");
@@ -169,7 +169,9 @@ const DATA = {"msg": "eggstreamly sekrit"};
const POLLINTERVAL = 50;
function run_test() {
- Svc.Prefs.set("jpake.serverURL", TEST_SERVER_URL);
+ server = httpd_setup({"/new_channel": server_new_channel,
+ "/report": server_report});
+ Svc.Prefs.set("jpake.serverURL", server.baseURI + "/");
Svc.Prefs.set("jpake.pollInterval", POLLINTERVAL);
Svc.Prefs.set("jpake.maxTries", 2);
Svc.Prefs.set("jpake.firstMsgMaxTries", 5);
@@ -184,15 +186,13 @@ function run_test() {
// Simulate Sync setup with credentials in place. We want to make
// sure the J-PAKE requests don't include those data.
+ ensureLegacyIdentityManager();
setBasicCredentials("johndoe", "ilovejane");
- server = httpd_setup({"/new_channel": server_new_channel,
- "/report": server_report});
-
initTestLogging("Trace");
- Log4Moz.repository.getLogger("Sync.JPAKEClient").level = Log4Moz.Level.Trace;
- Log4Moz.repository.getLogger("Common.RESTRequest").level =
- Log4Moz.Level.Trace;
+ Log.repository.getLogger("Sync.JPAKEClient").level = Log.Level.Trace;
+ Log.repository.getLogger("Common.RESTRequest").level =
+ Log.Level.Trace;
run_next_test();
}
diff --git a/services/sync/tests/unit/test_keys.js b/services/sync/tests/unit/test_keys.js
index 9668d3f14..6a2fdd027 100644
--- a/services/sync/tests/unit/test_keys.js
+++ b/services/sync/tests/unit/test_keys.js
@@ -14,13 +14,6 @@ function sha256HMAC(message, key) {
return Utils.digestBytes(message, h);
}
-function do_check_array_eq(a1, a2) {
- do_check_eq(a1.length, a2.length);
- for (let i = 0; i < a1.length; ++i) {
- do_check_eq(a1[i], a2[i]);
- }
-}
-
function do_check_keypair_eq(a, b) {
do_check_eq(2, a.length);
do_check_eq(2, b.length);
@@ -172,8 +165,8 @@ add_test(function test_keymanager() {
});
add_test(function test_collections_manager() {
- let log = Log4Moz.repository.getLogger("Test");
- Log4Moz.repository.rootLogger.addAppender(new Log4Moz.DumpAppender());
+ let log = Log.repository.getLogger("Test");
+ Log.repository.rootLogger.addAppender(new Log.DumpAppender());
let identity = new IdentityManager();
diff --git a/services/sync/tests/unit/test_load_modules.js b/services/sync/tests/unit/test_load_modules.js
index 516948468..4f561bae6 100644
--- a/services/sync/tests/unit/test_load_modules.js
+++ b/services/sync/tests/unit/test_load_modules.js
@@ -4,6 +4,7 @@
const modules = [
"addonutils.js",
"addonsreconciler.js",
+ "browserid_identity.js",
"constants.js",
"engines/addons.js",
"engines/bookmarks.js",
@@ -25,6 +26,7 @@ const modules = [
"rest.js",
"service.js",
"stages/cluster.js",
+ "stages/declined.js",
"stages/enginesync.js",
"status.js",
"userapi.js",
@@ -35,6 +37,7 @@ const testingModules = [
"fakeservices.js",
"rotaryengine.js",
"utils.js",
+ "fxa_utils.js",
];
function run_test() {
@@ -50,4 +53,3 @@ function run_test() {
Cu.import(res, {});
}
}
-
diff --git a/services/sync/tests/unit/test_node_reassignment.js b/services/sync/tests/unit/test_node_reassignment.js
index b851c6549..7fe5ed7ed 100644
--- a/services/sync/tests/unit/test_node_reassignment.js
+++ b/services/sync/tests/unit/test_node_reassignment.js
@@ -4,7 +4,7 @@
_("Test that node reassignment responses are respected on all kinds of " +
"requests.");
-Cu.import("resource://services-common/log4moz.js");
+Cu.import("resource://gre/modules/Log.jsm");
Cu.import("resource://services-common/rest.js");
Cu.import("resource://services-sync/constants.js");
Cu.import("resource://services-sync/service.js");
@@ -16,14 +16,16 @@ Cu.import("resource://testing-common/services/sync/utils.js");
Service.engineManager.clear();
function run_test() {
- Log4Moz.repository.getLogger("Sync.AsyncResource").level = Log4Moz.Level.Trace;
- Log4Moz.repository.getLogger("Sync.ErrorHandler").level = Log4Moz.Level.Trace;
- Log4Moz.repository.getLogger("Sync.Resource").level = Log4Moz.Level.Trace;
- Log4Moz.repository.getLogger("Sync.RESTRequest").level = Log4Moz.Level.Trace;
- Log4Moz.repository.getLogger("Sync.Service").level = Log4Moz.Level.Trace;
- Log4Moz.repository.getLogger("Sync.SyncScheduler").level = Log4Moz.Level.Trace;
+ Log.repository.getLogger("Sync.AsyncResource").level = Log.Level.Trace;
+ Log.repository.getLogger("Sync.ErrorHandler").level = Log.Level.Trace;
+ Log.repository.getLogger("Sync.Resource").level = Log.Level.Trace;
+ Log.repository.getLogger("Sync.RESTRequest").level = Log.Level.Trace;
+ Log.repository.getLogger("Sync.Service").level = Log.Level.Trace;
+ Log.repository.getLogger("Sync.SyncScheduler").level = Log.Level.Trace;
initTestLogging();
+ ensureLegacyIdentityManager();
+
Service.engineManager.register(RotaryEngine);
// None of the failures in this file should result in a UI error.
@@ -59,8 +61,8 @@ function handleReassign(handler, req, resp) {
/**
* A node assignment handler.
*/
-const newNodeBody = "http://localhost:8080/";
function installNodeHandler(server, next) {
+ let newNodeBody = server.baseURI;
function handleNodeRequest(req, resp) {
_("Client made a request for a node reassignment.");
resp.setStatusLine(req.httpVersion, 200, "OK");
@@ -74,15 +76,17 @@ function installNodeHandler(server, next) {
}
function prepareServer() {
- setBasicCredentials("johndoe", "ilovejane", "abcdeabcdeabcdeabcdeabcdea");
- Service.serverURL = TEST_SERVER_URL;
- Service.clusterURL = TEST_CLUSTER_URL;
-
- do_check_eq(Service.userAPIURI, "http://localhost:8080/user/1.0/");
- let server = new SyncServer();
- server.registerUser("johndoe");
- server.start();
- return server;
+ let deferred = Promise.defer();
+ configureIdentity({username: "johndoe"}).then(() => {
+ let server = new SyncServer();
+ server.registerUser("johndoe");
+ server.start();
+ Service.serverURL = server.baseURI;
+ Service.clusterURL = server.baseURI;
+ do_check_eq(Service.userAPIURI, server.baseURI + "user/1.0/");
+ deferred.resolve(server);
+ });
+ return deferred.promise;
}
function getReassigned() {
@@ -104,6 +108,7 @@ function getReassigned() {
*/
function syncAndExpectNodeReassignment(server, firstNotification, between,
secondNotification, url) {
+ let deferred = Promise.defer();
function onwards() {
let nodeFetched = false;
function onFirstSync() {
@@ -138,7 +143,7 @@ function syncAndExpectNodeReassignment(server, firstNotification, between,
_("Second sync nextTick.");
do_check_true(nodeFetched);
Service.startOver();
- server.stop(run_next_test);
+ server.stop(deferred.resolve);
});
}
@@ -152,11 +157,12 @@ function syncAndExpectNodeReassignment(server, firstNotification, between,
do_check_eq(request.response.status, 401);
Utils.nextTick(onwards);
});
+ yield deferred.promise;
}
-add_test(function test_momentary_401_engine() {
+add_task(function test_momentary_401_engine() {
_("Test a failure for engine URLs that's resolved by reassignment.");
- let server = prepareServer();
+ let server = yield prepareServer();
let john = server.user("johndoe");
_("Enabling the Rotary engine.");
@@ -198,17 +204,17 @@ add_test(function test_momentary_401_engine() {
Svc.Obs.add("weave:service:login:start", onLoginStart);
}
- syncAndExpectNodeReassignment(server,
- "weave:service:sync:finish",
- between,
- "weave:service:sync:finish",
- Service.storageURL + "rotary");
+ yield syncAndExpectNodeReassignment(server,
+ "weave:service:sync:finish",
+ between,
+ "weave:service:sync:finish",
+ Service.storageURL + "rotary");
});
// This test ends up being a failing fetch *after we're already logged in*.
-add_test(function test_momentary_401_info_collections() {
+add_task(function test_momentary_401_info_collections() {
_("Test a failure for info/collections that's resolved by reassignment.");
- let server = prepareServer();
+ let server = yield prepareServer();
_("First sync to prepare server contents.");
Service.sync();
@@ -222,17 +228,42 @@ add_test(function test_momentary_401_info_collections() {
server.toplevelHandlers.info = oldHandler;
}
- syncAndExpectNodeReassignment(server,
- "weave:service:sync:error",
- undo,
- "weave:service:sync:finish",
- Service.infoURL);
+ yield syncAndExpectNodeReassignment(server,
+ "weave:service:sync:error",
+ undo,
+ "weave:service:sync:finish",
+ Service.infoURL);
+});
+
+add_task(function test_momentary_401_storage_loggedin() {
+ _("Test a failure for any storage URL, not just engine parts. " +
+ "Resolved by reassignment.");
+ let server = yield prepareServer();
+
+ _("Performing initial sync to ensure we are logged in.")
+ Service.sync();
+
+ // Return a 401 for all storage requests.
+ let oldHandler = server.toplevelHandlers.storage;
+ server.toplevelHandlers.storage = handleReassign;
+
+ function undo() {
+ _("Undoing test changes.");
+ server.toplevelHandlers.storage = oldHandler;
+ }
+
+ do_check_true(Service.isLoggedIn, "already logged in");
+ yield syncAndExpectNodeReassignment(server,
+ "weave:service:sync:error",
+ undo,
+ "weave:service:sync:finish",
+ Service.storageURL + "meta/global");
});
-add_test(function test_momentary_401_storage() {
+add_task(function test_momentary_401_storage_loggedout() {
_("Test a failure for any storage URL, not just engine parts. " +
"Resolved by reassignment.");
- let server = prepareServer();
+ let server = yield prepareServer();
// Return a 401 for all storage requests.
let oldHandler = server.toplevelHandlers.storage;
@@ -243,18 +274,19 @@ add_test(function test_momentary_401_storage() {
server.toplevelHandlers.storage = oldHandler;
}
- syncAndExpectNodeReassignment(server,
- "weave:service:login:error",
- undo,
- "weave:service:sync:finish",
- Service.storageURL + "meta/global");
+ do_check_false(Service.isLoggedIn, "not already logged in");
+ yield syncAndExpectNodeReassignment(server,
+ "weave:service:login:error",
+ undo,
+ "weave:service:sync:finish",
+ Service.storageURL + "meta/global");
});
-add_test(function test_loop_avoidance_storage() {
+add_task(function test_loop_avoidance_storage() {
_("Test that a repeated failure doesn't result in a sync loop " +
"if node reassignment cannot resolve the failure.");
- let server = prepareServer();
+ let server = yield prepareServer();
// Return a 401 for all storage requests.
let oldHandler = server.toplevelHandlers.storage;
@@ -265,6 +297,7 @@ add_test(function test_loop_avoidance_storage() {
let thirdNotification = "weave:service:sync:finish";
let nodeFetched = false;
+ let deferred = Promise.defer();
// Track the time. We want to make sure the duration between the first and
// second sync is small, and then that the duration between second and third
@@ -338,7 +371,7 @@ add_test(function test_loop_avoidance_storage() {
do_check_false(getReassigned());
do_check_true(nodeFetched);
Service.startOver();
- server.stop(run_next_test);
+ server.stop(deferred.resolve);
});
}
@@ -346,17 +379,19 @@ add_test(function test_loop_avoidance_storage() {
now = Date.now();
Service.sync();
+ yield deferred.promise;
});
-add_test(function test_loop_avoidance_engine() {
+add_task(function test_loop_avoidance_engine() {
_("Test that a repeated 401 in an engine doesn't result in a sync loop " +
"if node reassignment cannot resolve the failure.");
- let server = prepareServer();
+ let server = yield prepareServer();
let john = server.user("johndoe");
_("Enabling the Rotary engine.");
let engine = Service.engineManager.get("rotary");
engine.enabled = true;
+ let deferred = Promise.defer();
// We need the server to be correctly set up prior to experimenting. Do this
// through a sync.
@@ -391,7 +426,7 @@ add_test(function test_loop_avoidance_engine() {
function afterSuccessfulSync() {
Svc.Obs.remove("weave:service:login:start", onLoginStart);
Service.startOver();
- server.stop(run_next_test);
+ server.stop(deferred.resolve);
}
let firstNotification = "weave:service:sync:finish";
@@ -483,4 +518,5 @@ add_test(function test_loop_avoidance_engine() {
now = Date.now();
Service.sync();
+ yield deferred.promise;
});
diff --git a/services/sync/tests/unit/test_password_store.js b/services/sync/tests/unit/test_password_store.js
index a40fb0aa3..c56901d79 100644
--- a/services/sync/tests/unit/test_password_store.js
+++ b/services/sync/tests/unit/test_password_store.js
@@ -7,8 +7,8 @@ Cu.import("resource://services-sync/util.js");
function run_test() {
initTestLogging("Trace");
- Log4Moz.repository.getLogger("Sync.Engine.Passwords").level = Log4Moz.Level.Trace;
- Log4Moz.repository.getLogger("Sync.Store.Passwords").level = Log4Moz.Level.Trace;
+ Log.repository.getLogger("Sync.Engine.Passwords").level = Log.Level.Trace;
+ Log.repository.getLogger("Sync.Store.Passwords").level = Log.Level.Trace;
const BOGUS_GUID_A = "zzzzzzzzzzzz";
const BOGUS_GUID_B = "yyyyyyyyyyyy";
diff --git a/services/sync/tests/unit/test_places_guid_downgrade.js b/services/sync/tests/unit/test_places_guid_downgrade.js
index 4edc4b3aa..2f99c4a93 100644
--- a/services/sync/tests/unit/test_places_guid_downgrade.js
+++ b/services/sync/tests/unit/test_places_guid_downgrade.js
@@ -1,6 +1,7 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
+Cu.import("resource://gre/modules/PlacesUtils.jsm");
Cu.import("resource://services-common/async.js");
Cu.import("resource://services-sync/util.js");
Cu.import("resource://services-sync/engines.js");
diff --git a/services/sync/tests/unit/test_records_crypto.js b/services/sync/tests/unit/test_records_crypto.js
index e40a4da76..4d623c917 100644
--- a/services/sync/tests/unit/test_records_crypto.js
+++ b/services/sync/tests/unit/test_records_crypto.js
@@ -1,13 +1,14 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
-Cu.import("resource://services-common/log4moz.js");
+Cu.import("resource://gre/modules/Log.jsm");
Cu.import("resource://services-sync/constants.js");
Cu.import("resource://services-sync/keys.js");
Cu.import("resource://services-sync/record.js");
Cu.import("resource://services-sync/resource.js");
Cu.import("resource://services-sync/service.js");
Cu.import("resource://services-sync/util.js");
+Cu.import("resource://testing-common/services/sync/utils.js");
let cryptoWrap;
@@ -30,13 +31,14 @@ function run_test() {
let server;
do_test_pending();
+ ensureLegacyIdentityManager();
Service.identity.username = "john@example.com";
Service.identity.syncKey = "a-abcde-abcde-abcde-abcde-abcde";
let keyBundle = Service.identity.syncKeyBundle;
try {
- let log = Log4Moz.repository.getLogger("Test");
- Log4Moz.repository.rootLogger.addAppender(new Log4Moz.DumpAppender());
+ let log = Log.repository.getLogger("Test");
+ Log.repository.rootLogger.addAppender(new Log.DumpAppender());
log.info("Setting up server and authenticator");
diff --git a/services/sync/tests/unit/test_records_wbo.js b/services/sync/tests/unit/test_records_wbo.js
index f397f638b..e3277b0a7 100644
--- a/services/sync/tests/unit/test_records_wbo.js
+++ b/services/sync/tests/unit/test_records_wbo.js
@@ -6,6 +6,7 @@ Cu.import("resource://services-sync/identity.js");
Cu.import("resource://services-sync/resource.js");
Cu.import("resource://services-sync/service.js");
Cu.import("resource://services-sync/util.js");
+Cu.import("resource://testing-common/services/sync/utils.js");
function test_toJSON() {
@@ -51,7 +52,7 @@ function test_fetch() {
try {
_("Fetching a WBO record");
let rec = new WBORecord("coll", "record");
- rec.fetch(Service.resource("http://localhost:8080/record"));
+ rec.fetch(Service.resource(server.baseURI + "/record"));
do_check_eq(rec.id, "asdf-1234-asdf-1234"); // NOT "record"!
do_check_eq(rec.modified, 2454725.98283);
@@ -59,7 +60,7 @@ function test_fetch() {
do_check_eq(rec.payload.cheese, "roquefort");
_("Fetching a WBO record using the record manager");
- let rec2 = Service.recordManager.get("http://localhost:8080/record2");
+ let rec2 = Service.recordManager.get(server.baseURI + "/record2");
do_check_eq(rec2.id, "record2");
do_check_eq(rec2.modified, 2454725.98284);
do_check_eq(typeof(rec2.payload), "object");
@@ -78,6 +79,7 @@ function test_fetch() {
function run_test() {
initTestLogging("Trace");
+ ensureLegacyIdentityManager();
test_toJSON();
test_fetch();
diff --git a/services/sync/tests/unit/test_resource.js b/services/sync/tests/unit/test_resource.js
index 3cb8775ab..027d662b4 100644
--- a/services/sync/tests/unit/test_resource.js
+++ b/services/sync/tests/unit/test_resource.js
@@ -1,7 +1,7 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
-Cu.import("resource://services-common/log4moz.js");
+Cu.import("resource://gre/modules/Log.jsm");
Cu.import("resource://services-common/observers.js");
Cu.import("resource://services-sync/identity.js");
Cu.import("resource://services-sync/resource.js");
@@ -153,8 +153,8 @@ function run_test() {
do_test_pending();
- logger = Log4Moz.repository.getLogger('Test');
- Log4Moz.repository.rootLogger.addAppender(new Log4Moz.DumpAppender());
+ let logger = Log.repository.getLogger('Test');
+ Log.repository.rootLogger.addAppender(new Log.DumpAppender());
let server = httpd_setup({
"/open": server_open,
@@ -176,9 +176,9 @@ function run_test() {
// This apparently has to come first in order for our PAC URL to be hit.
// Don't put any other HTTP requests earlier in the file!
_("Testing handling of proxy auth redirection.");
- PACSystemSettings.PACURI = "http://localhost:8080/pac1";
+ PACSystemSettings.PACURI = server.baseURI + "/pac1";
installFakePAC();
- let proxiedRes = new Resource("http://localhost:8080/open");
+ let proxiedRes = new Resource(server.baseURI + "/open");
let content = proxiedRes.get();
do_check_true(pacFetched);
do_check_true(fetched);
@@ -187,10 +187,10 @@ function run_test() {
uninstallFakePAC();
_("Resource object members");
- let res = new Resource("http://localhost:8080/open");
+ let res = new Resource(server.baseURI + "/open");
do_check_true(res.uri instanceof Ci.nsIURI);
- do_check_eq(res.uri.spec, "http://localhost:8080/open");
- do_check_eq(res.spec, "http://localhost:8080/open");
+ do_check_eq(res.uri.spec, server.baseURI + "/open");
+ do_check_eq(res.spec, server.baseURI + "/open");
do_check_eq(typeof res.headers, "object");
do_check_eq(typeof res.authenticator, "object");
// Initially res.data is null since we haven't performed a GET or
@@ -206,7 +206,7 @@ function run_test() {
do_check_eq(res.data, content);
// Observe logging messages.
- let logger = res._log;
+ logger = res._log;
let dbg = logger.debug;
let debugMessages = [];
logger.debug = function (msg) {
@@ -230,19 +230,19 @@ function run_test() {
logger.debug = dbg;
_("Test that the BasicAuthenticator doesn't screw up header case.");
- let res1 = new Resource("http://localhost:8080/foo");
+ let res1 = new Resource(server.baseURI + "/foo");
res1.setHeader("Authorization", "Basic foobar");
do_check_eq(res1.headers["authorization"], "Basic foobar");
_("GET a password protected resource (test that it'll fail w/o pass, no throw)");
- let res2 = new Resource("http://localhost:8080/protected");
+ let res2 = new Resource(server.baseURI + "/protected");
content = res2.get();
do_check_eq(content, "This path exists and is protected - failed");
do_check_eq(content.status, 401);
do_check_false(content.success);
_("GET a password protected resource");
- let res3 = new Resource("http://localhost:8080/protected");
+ let res3 = new Resource(server.baseURI + "/protected");
let identity = new IdentityManager();
let auth = identity.getBasicResourceAuthenticator("guest", "guest");
res3.authenticator = auth;
@@ -253,7 +253,7 @@ function run_test() {
do_check_true(content.success);
_("GET a non-existent resource (test that it'll fail, but not throw)");
- let res4 = new Resource("http://localhost:8080/404");
+ let res4 = new Resource(server.baseURI + "/404");
content = res4.get();
do_check_eq(content, "File not found");
do_check_eq(content.status, 404);
@@ -265,7 +265,7 @@ function run_test() {
do_check_eq(content.headers["content-length"], 14);
_("PUT to a resource (string)");
- let res5 = new Resource("http://localhost:8080/upload");
+ let res5 = new Resource(server.baseURI + "/upload");
content = res5.put(JSON.stringify(sample_data));
do_check_eq(content, "Valid data upload via PUT");
do_check_eq(content.status, 200);
@@ -318,13 +318,13 @@ function run_test() {
do_check_eq(res5.data, content);
_("DELETE a resource");
- let res6 = new Resource("http://localhost:8080/delete");
+ let res6 = new Resource(server.baseURI + "/delete");
content = res6.delete();
do_check_eq(content, "This resource has been deleted")
do_check_eq(content.status, 200);
_("JSON conversion of response body");
- let res7 = new Resource("http://localhost:8080/json");
+ let res7 = new Resource(server.baseURI + "/json");
content = res7.get();
do_check_eq(content, JSON.stringify(sample_data));
do_check_eq(content.status, 200);
@@ -334,12 +334,12 @@ function run_test() {
// Before having received any response containing the
// X-Weave-Timestamp header, AsyncResource.serverTime is null.
do_check_eq(AsyncResource.serverTime, null);
- let res8 = new Resource("http://localhost:8080/timestamp");
+ let res8 = new Resource(server.baseURI + "/timestamp");
content = res8.get();
do_check_eq(AsyncResource.serverTime, TIMESTAMP);
_("GET: no special request headers");
- let res9 = new Resource("http://localhost:8080/headers");
+ let res9 = new Resource(server.baseURI + "/headers");
content = res9.get();
do_check_eq(content, '{}');
@@ -387,7 +387,7 @@ function run_test() {
}
Observers.add("weave:service:backoff:interval", onBackoff);
- let res10 = new Resource("http://localhost:8080/backoff");
+ let res10 = new Resource(server.baseURI + "/backoff");
content = res10.get();
do_check_eq(backoffInterval, 600);
@@ -399,12 +399,12 @@ function run_test() {
}
Observers.add("weave:service:quota:remaining", onQuota);
- res10 = new Resource("http://localhost:8080/quota-error");
+ res10 = new Resource(server.baseURI + "/quota-error");
content = res10.get();
do_check_eq(content.status, 400);
do_check_eq(quotaValue, undefined); // HTTP 400, so no observer notification.
- res10 = new Resource("http://localhost:8080/quota-notice");
+ res10 = new Resource(server.baseURI + "/quota-notice");
content = res10.get();
do_check_eq(content.status, 200);
do_check_eq(quotaValue, 1048576);
@@ -423,7 +423,7 @@ function run_test() {
do_check_eq(typeof error.stack, "string");
_("Checking handling of errors in onProgress.");
- let res18 = new Resource("http://localhost:8080/json");
+ let res18 = new Resource(server.baseURI + "/json");
let onProgress = function(rec) {
// Provoke an XPC exception without a Javascript wrapper.
Services.io.newURI("::::::::", null, null);
@@ -444,10 +444,10 @@ function run_test() {
do_check_eq(error, "Error: NS_ERROR_MALFORMED_URI");
do_check_eq(warnings.pop(),
"Got exception calling onProgress handler during fetch of " +
- "http://localhost:8080/json");
+ server.baseURI + "/json");
// And this is what happens if JS throws an exception.
- res18 = new Resource("http://localhost:8080/json");
+ res18 = new Resource(server.baseURI + "/json");
onProgress = function(rec) {
throw "BOO!";
};
@@ -467,11 +467,11 @@ function run_test() {
do_check_eq(error, "Error: NS_ERROR_XPC_JS_THREW_STRING");
do_check_eq(warnings.pop(),
"Got exception calling onProgress handler during fetch of " +
- "http://localhost:8080/json");
+ server.baseURI + "/json");
_("Ensure channel timeouts are thrown appropriately.");
- let res19 = new Resource("http://localhost:8080/json");
+ let res19 = new Resource(server.baseURI + "/json");
res19.ABORT_TIMEOUT = 0;
error = undefined;
try {
diff --git a/services/sync/tests/unit/test_resource_async.js b/services/sync/tests/unit/test_resource_async.js
index 9609dbf7a..c4b9a3804 100644
--- a/services/sync/tests/unit/test_resource_async.js
+++ b/services/sync/tests/unit/test_resource_async.js
@@ -1,15 +1,12 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
-Cu.import("resource://services-common/log4moz.js");
+Cu.import("resource://gre/modules/Log.jsm");
Cu.import("resource://services-common/observers.js");
Cu.import("resource://services-sync/identity.js");
Cu.import("resource://services-sync/resource.js");
Cu.import("resource://services-sync/util.js");
-const RES_UPLOAD_URL = "http://localhost:8080/upload";
-const RES_HEADERS_URL = "http://localhost:8080/headers";
-
let logger;
let fetched = false;
@@ -156,8 +153,8 @@ Observers.add("weave:service:quota:remaining",
function (subject) { quotaValue = subject; });
function run_test() {
- logger = Log4Moz.repository.getLogger('Test');
- Log4Moz.repository.rootLogger.addAppender(new Log4Moz.DumpAppender());
+ logger = Log.repository.getLogger('Test');
+ Log.repository.rootLogger.addAppender(new Log.DumpAppender());
Svc.Prefs.set("network.numRetries", 1); // speed up test
run_next_test();
@@ -173,9 +170,9 @@ add_test(function test_proxy_auth_redirect() {
"/pac2": server_pac
});
- PACSystemSettings.PACURI = "http://localhost:8080/pac2";
+ PACSystemSettings.PACURI = server.baseURI + "/pac2";
installFakePAC();
- let res = new AsyncResource("http://localhost:8080/open");
+ let res = new AsyncResource(server.baseURI + "/open");
res.get(function (error, result) {
do_check_true(!error);
do_check_true(pacFetched);
@@ -199,18 +196,19 @@ add_test(function test_new_channel() {
response.bodyOutputStream.write(body, body.length);
}
+ let locationURL;
function redirectHandler(metadata, response) {
let body = "Redirecting";
response.setStatusLine(metadata.httpVersion, 307, "TEMPORARY REDIRECT");
- response.setHeader("Location", "http://localhost:8080/resource");
+ response.setHeader("Location", locationURL);
response.bodyOutputStream.write(body, body.length);
}
let server = httpd_setup({"/resource": resourceHandler,
- "/redirect": redirectHandler},
- 8080);
+ "/redirect": redirectHandler});
+ locationURL = server.baseURI + "/resource";
- let request = new AsyncResource("http://localhost:8080/redirect");
+ let request = new AsyncResource(server.baseURI + "/redirect");
request.get(function onRequest(error, content) {
do_check_null(error);
do_check_true(resourceRequested);
@@ -246,10 +244,11 @@ add_test(function setup() {
add_test(function test_members() {
_("Resource object members");
- let res = new AsyncResource("http://localhost:8080/open");
+ let uri = server.baseURI + "/open";
+ let res = new AsyncResource(uri);
do_check_true(res.uri instanceof Ci.nsIURI);
- do_check_eq(res.uri.spec, "http://localhost:8080/open");
- do_check_eq(res.spec, "http://localhost:8080/open");
+ do_check_eq(res.uri.spec, uri);
+ do_check_eq(res.spec, uri);
do_check_eq(typeof res.headers, "object");
do_check_eq(typeof res.authenticator, "object");
// Initially res.data is null since we haven't performed a GET or
@@ -261,7 +260,7 @@ add_test(function test_members() {
add_test(function test_get() {
_("GET a non-password-protected resource");
- let res = new AsyncResource("http://localhost:8080/open");
+ let res = new AsyncResource(server.baseURI + "/open");
res.get(function (error, content) {
do_check_eq(error, null);
do_check_eq(content, "This path exists");
@@ -299,7 +298,7 @@ add_test(function test_get() {
add_test(function test_basicauth() {
_("Test that the BasicAuthenticator doesn't screw up header case.");
- let res1 = new AsyncResource("http://localhost:8080/foo");
+ let res1 = new AsyncResource(server.baseURI + "/foo");
res1.setHeader("Authorization", "Basic foobar");
do_check_eq(res1._headers["authorization"], "Basic foobar");
do_check_eq(res1.headers["authorization"], "Basic foobar");
@@ -309,7 +308,7 @@ add_test(function test_basicauth() {
add_test(function test_get_protected_fail() {
_("GET a password protected resource (test that it'll fail w/o pass, no throw)");
- let res2 = new AsyncResource("http://localhost:8080/protected");
+ let res2 = new AsyncResource(server.baseURI + "/protected");
res2.get(function (error, content) {
do_check_eq(error, null);
do_check_eq(content, "This path exists and is protected - failed");
@@ -323,7 +322,7 @@ add_test(function test_get_protected_success() {
_("GET a password protected resource");
let identity = new IdentityManager();
let auth = identity.getBasicResourceAuthenticator("guest", "guest");
- let res3 = new AsyncResource("http://localhost:8080/protected");
+ let res3 = new AsyncResource(server.baseURI + "/protected");
res3.authenticator = auth;
do_check_eq(res3.authenticator, auth);
res3.get(function (error, content) {
@@ -337,7 +336,7 @@ add_test(function test_get_protected_success() {
add_test(function test_get_404() {
_("GET a non-existent resource (test that it'll fail, but not throw)");
- let res4 = new AsyncResource("http://localhost:8080/404");
+ let res4 = new AsyncResource(server.baseURI + "/404");
res4.get(function (error, content) {
do_check_eq(error, null);
do_check_eq(content, "File not found");
@@ -355,7 +354,7 @@ add_test(function test_get_404() {
add_test(function test_put_string() {
_("PUT to a resource (string)");
- let res_upload = new AsyncResource(RES_UPLOAD_URL);
+ let res_upload = new AsyncResource(server.baseURI + "/upload");
res_upload.put(JSON.stringify(sample_data), function(error, content) {
do_check_eq(error, null);
do_check_eq(content, "Valid data upload via PUT");
@@ -367,7 +366,7 @@ add_test(function test_put_string() {
add_test(function test_put_object() {
_("PUT to a resource (object)");
- let res_upload = new AsyncResource(RES_UPLOAD_URL);
+ let res_upload = new AsyncResource(server.baseURI + "/upload");
res_upload.put(sample_data, function (error, content) {
do_check_eq(error, null);
do_check_eq(content, "Valid data upload via PUT");
@@ -379,7 +378,7 @@ add_test(function test_put_object() {
add_test(function test_put_data_string() {
_("PUT without data arg (uses resource.data) (string)");
- let res_upload = new AsyncResource(RES_UPLOAD_URL);
+ let res_upload = new AsyncResource(server.baseURI + "/upload");
res_upload.data = JSON.stringify(sample_data);
res_upload.put(function (error, content) {
do_check_eq(error, null);
@@ -392,7 +391,7 @@ add_test(function test_put_data_string() {
add_test(function test_put_data_object() {
_("PUT without data arg (uses resource.data) (object)");
- let res_upload = new AsyncResource(RES_UPLOAD_URL);
+ let res_upload = new AsyncResource(server.baseURI + "/upload");
res_upload.data = sample_data;
res_upload.put(function (error, content) {
do_check_eq(error, null);
@@ -405,7 +404,7 @@ add_test(function test_put_data_object() {
add_test(function test_post_string() {
_("POST to a resource (string)");
- let res_upload = new AsyncResource(RES_UPLOAD_URL);
+ let res_upload = new AsyncResource(server.baseURI + "/upload");
res_upload.post(JSON.stringify(sample_data), function (error, content) {
do_check_eq(error, null);
do_check_eq(content, "Valid data upload via POST");
@@ -417,7 +416,7 @@ add_test(function test_post_string() {
add_test(function test_post_object() {
_("POST to a resource (object)");
- let res_upload = new AsyncResource(RES_UPLOAD_URL);
+ let res_upload = new AsyncResource(server.baseURI + "/upload");
res_upload.post(sample_data, function (error, content) {
do_check_eq(error, null);
do_check_eq(content, "Valid data upload via POST");
@@ -429,7 +428,7 @@ add_test(function test_post_object() {
add_test(function test_post_data_string() {
_("POST without data arg (uses resource.data) (string)");
- let res_upload = new AsyncResource(RES_UPLOAD_URL);
+ let res_upload = new AsyncResource(server.baseURI + "/upload");
res_upload.data = JSON.stringify(sample_data);
res_upload.post(function (error, content) {
do_check_eq(error, null);
@@ -442,7 +441,7 @@ add_test(function test_post_data_string() {
add_test(function test_post_data_object() {
_("POST without data arg (uses resource.data) (object)");
- let res_upload = new AsyncResource(RES_UPLOAD_URL);
+ let res_upload = new AsyncResource(server.baseURI + "/upload");
res_upload.data = sample_data;
res_upload.post(function (error, content) {
do_check_eq(error, null);
@@ -455,7 +454,7 @@ add_test(function test_post_data_object() {
add_test(function test_delete() {
_("DELETE a resource");
- let res6 = new AsyncResource("http://localhost:8080/delete");
+ let res6 = new AsyncResource(server.baseURI + "/delete");
res6.delete(function (error, content) {
do_check_eq(error, null);
do_check_eq(content, "This resource has been deleted");
@@ -466,7 +465,7 @@ add_test(function test_delete() {
add_test(function test_json_body() {
_("JSON conversion of response body");
- let res7 = new AsyncResource("http://localhost:8080/json");
+ let res7 = new AsyncResource(server.baseURI + "/json");
res7.get(function (error, content) {
do_check_eq(error, null);
do_check_eq(content, JSON.stringify(sample_data));
@@ -481,7 +480,7 @@ add_test(function test_weave_timestamp() {
// Before having received any response containing the
// X-Weave-Timestamp header, AsyncResource.serverTime is null.
do_check_eq(AsyncResource.serverTime, null);
- let res8 = new AsyncResource("http://localhost:8080/timestamp");
+ let res8 = new AsyncResource(server.baseURI + "/timestamp");
res8.get(function (error, content) {
do_check_eq(error, null);
do_check_eq(AsyncResource.serverTime, TIMESTAMP);
@@ -491,7 +490,7 @@ add_test(function test_weave_timestamp() {
add_test(function test_get_no_headers() {
_("GET: no special request headers");
- let res_headers = new AsyncResource(RES_HEADERS_URL);
+ let res_headers = new AsyncResource(server.baseURI + "/headers");
res_headers.get(function (error, content) {
do_check_eq(error, null);
do_check_eq(content, '{}');
@@ -501,7 +500,7 @@ add_test(function test_get_no_headers() {
add_test(function test_put_default_content_type() {
_("PUT: Content-Type defaults to text/plain");
- let res_headers = new AsyncResource(RES_HEADERS_URL);
+ let res_headers = new AsyncResource(server.baseURI + "/headers");
res_headers.put('data', function (error, content) {
do_check_eq(error, null);
do_check_eq(content, JSON.stringify({"content-type": "text/plain"}));
@@ -511,7 +510,7 @@ add_test(function test_put_default_content_type() {
add_test(function test_post_default_content_type() {
_("POST: Content-Type defaults to text/plain");
- let res_headers = new AsyncResource(RES_HEADERS_URL);
+ let res_headers = new AsyncResource(server.baseURI + "/headers");
res_headers.post('data', function (error, content) {
do_check_eq(error, null);
do_check_eq(content, JSON.stringify({"content-type": "text/plain"}));
@@ -521,7 +520,7 @@ add_test(function test_post_default_content_type() {
add_test(function test_setHeader() {
_("setHeader(): setting simple header");
- let res_headers = new AsyncResource(RES_HEADERS_URL);
+ let res_headers = new AsyncResource(server.baseURI + "/headers");
res_headers.setHeader('X-What-Is-Weave', 'awesome');
do_check_eq(res_headers.headers['x-what-is-weave'], 'awesome');
res_headers.get(function (error, content) {
@@ -533,7 +532,7 @@ add_test(function test_setHeader() {
add_test(function test_setHeader_overwrite() {
_("setHeader(): setting multiple headers, overwriting existing header");
- let res_headers = new AsyncResource(RES_HEADERS_URL);
+ let res_headers = new AsyncResource(server.baseURI + "/headers");
res_headers.setHeader('X-WHAT-is-Weave', 'more awesomer');
res_headers.setHeader('X-Another-Header', 'hello world');
do_check_eq(res_headers.headers['x-what-is-weave'], 'more awesomer');
@@ -549,7 +548,7 @@ add_test(function test_setHeader_overwrite() {
add_test(function test_headers_object() {
_("Setting headers object");
- let res_headers = new AsyncResource(RES_HEADERS_URL);
+ let res_headers = new AsyncResource(server.baseURI + "/headers");
res_headers.headers = {};
res_headers.get(function (error, content) {
do_check_eq(error, null);
@@ -560,7 +559,7 @@ add_test(function test_headers_object() {
add_test(function test_put_override_content_type() {
_("PUT: override default Content-Type");
- let res_headers = new AsyncResource(RES_HEADERS_URL);
+ let res_headers = new AsyncResource(server.baseURI + "/headers");
res_headers.setHeader('Content-Type', 'application/foobar');
do_check_eq(res_headers.headers['content-type'], 'application/foobar');
res_headers.put('data', function (error, content) {
@@ -572,7 +571,7 @@ add_test(function test_put_override_content_type() {
add_test(function test_post_override_content_type() {
_("POST: override default Content-Type");
- let res_headers = new AsyncResource(RES_HEADERS_URL);
+ let res_headers = new AsyncResource(server.baseURI + "/headers");
res_headers.setHeader('Content-Type', 'application/foobar');
res_headers.post('data', function (error, content) {
do_check_eq(error, null);
@@ -589,7 +588,7 @@ add_test(function test_weave_backoff() {
}
Observers.add("weave:service:backoff:interval", onBackoff);
- let res10 = new AsyncResource("http://localhost:8080/backoff");
+ let res10 = new AsyncResource(server.baseURI + "/backoff");
res10.get(function (error, content) {
do_check_eq(error, null);
do_check_eq(backoffInterval, 600);
@@ -599,7 +598,7 @@ add_test(function test_weave_backoff() {
add_test(function test_quota_error() {
_("X-Weave-Quota-Remaining header notifies observer on successful requests.");
- let res10 = new AsyncResource("http://localhost:8080/quota-error");
+ let res10 = new AsyncResource(server.baseURI + "/quota-error");
res10.get(function (error, content) {
do_check_eq(error, null);
do_check_eq(content.status, 400);
@@ -609,7 +608,7 @@ add_test(function test_quota_error() {
});
add_test(function test_quota_notice() {
- let res10 = new AsyncResource("http://localhost:8080/quota-notice");
+ let res10 = new AsyncResource(server.baseURI + "/quota-notice");
res10.get(function (error, content) {
do_check_eq(error, null);
do_check_eq(content.status, 200);
@@ -631,7 +630,7 @@ add_test(function test_preserve_exceptions() {
add_test(function test_xpc_exception_handling() {
_("Exception handling inside fetches.");
- let res14 = new AsyncResource("http://localhost:8080/json");
+ let res14 = new AsyncResource(server.baseURI + "/json");
res14._onProgress = function(rec) {
// Provoke an XPC exception without a Javascript wrapper.
Services.io.newURI("::::::::", null, null);
@@ -645,7 +644,7 @@ add_test(function test_xpc_exception_handling() {
do_check_eq(content, null);
do_check_eq(warnings.pop(),
"Got exception calling onProgress handler during fetch of " +
- "http://localhost:8080/json");
+ server.baseURI + "/json");
run_next_test();
});
@@ -653,7 +652,7 @@ add_test(function test_xpc_exception_handling() {
add_test(function test_js_exception_handling() {
_("JS exception handling inside fetches.");
- let res15 = new AsyncResource("http://localhost:8080/json");
+ let res15 = new AsyncResource(server.baseURI + "/json");
res15._onProgress = function(rec) {
throw "BOO!";
};
@@ -666,7 +665,7 @@ add_test(function test_js_exception_handling() {
do_check_eq(content, null);
do_check_eq(warnings.pop(),
"Got exception calling onProgress handler during fetch of " +
- "http://localhost:8080/json");
+ server.baseURI + "/json");
run_next_test();
});
@@ -674,7 +673,7 @@ add_test(function test_js_exception_handling() {
add_test(function test_timeout() {
_("Ensure channel timeouts are thrown appropriately.");
- let res19 = new AsyncResource("http://localhost:8080/json");
+ let res19 = new AsyncResource(server.baseURI + "/json");
res19.ABORT_TIMEOUT = 0;
res19.get(function (error, content) {
do_check_eq(error.result, Cr.NS_ERROR_NET_TIMEOUT);
@@ -710,10 +709,10 @@ add_test(function test_not_sending_cookie() {
}
let cookieSer = Cc["@mozilla.org/cookieService;1"]
.getService(Ci.nsICookieService);
- let uri = CommonUtils.makeURI("http://localhost:8080");
+ let uri = CommonUtils.makeURI(server.baseURI);
cookieSer.setCookieString(uri, null, "test=test; path=/;", null);
- let res = new AsyncResource("http://localhost:8080/test");
+ let res = new AsyncResource(server.baseURI + "/test");
res.get(function (error) {
do_check_null(error);
do_check_true(this.response.success);
diff --git a/services/sync/tests/unit/test_resource_header.js b/services/sync/tests/unit/test_resource_header.js
index 0c65f3c52..1835cc0e0 100644
--- a/services/sync/tests/unit/test_resource_header.js
+++ b/services/sync/tests/unit/test_resource_header.js
@@ -11,8 +11,12 @@ function run_test() {
run_next_test();
}
-const TEST_URL = "http://localhost:4444/content";
-const HTTP_PORT = 4444;
+let httpServer = new HttpServer();
+httpServer.registerPathHandler("/content", contentHandler);
+httpServer.start(-1);
+
+const HTTP_PORT = httpServer.identity.primaryPort;
+const TEST_URL = "http://localhost:" + HTTP_PORT + "/content";
const BODY = "response body";
// Keep headers for later inspection.
@@ -29,18 +33,11 @@ function contentHandler(metadata, response) {
response.bodyOutputStream.write(BODY, BODY.length);
}
-function makeServer() {
- let httpServer = new HttpServer();
- httpServer.registerPathHandler("/content", contentHandler);
- httpServer.start(4444);
- return httpServer;
-}
-
// Set a proxy function to cause an internal redirect.
function triggerRedirect() {
const PROXY_FUNCTION = "function FindProxyForURL(url, host) {" +
" return 'PROXY a_non_existent_domain_x7x6c572v:80; " +
- "PROXY localhost:4444';" +
+ "PROXY localhost:" + HTTP_PORT + "';" +
"}";
let prefsService = Cc["@mozilla.org/preferences-service;1"].getService(Ci.nsIPrefService);
@@ -50,7 +47,6 @@ function triggerRedirect() {
}
add_test(function test_headers_copied() {
- let server = makeServer();
triggerRedirect();
_("Issuing request.");
@@ -65,5 +61,5 @@ add_test(function test_headers_copied() {
do_check_eq(auth, "Basic foobar");
do_check_eq(foo, "foofoo");
- server.stop(run_next_test);
+ httpServer.stop(run_next_test);
});
diff --git a/services/sync/tests/unit/test_resource_ua.js b/services/sync/tests/unit/test_resource_ua.js
index 3211c8ef5..279a2b3e6 100644
--- a/services/sync/tests/unit/test_resource_ua.js
+++ b/services/sync/tests/unit/test_resource_ua.js
@@ -7,8 +7,6 @@ Cu.import("resource://services-sync/service.js");
Cu.import("resource://services-sync/util.js");
Cu.import("resource://testing-common/services/sync/utils.js");
-const TEST_GET_URL = "http://localhost:8080/1.1/johndoe/storage/meta/global";
-
// Tracking info/collections.
let collectionsHelper = track_collections_helper();
let collections = collectionsHelper.collections;
@@ -26,15 +24,18 @@ function uaHandler(f) {
}
function run_test() {
+ Log.repository.rootLogger.addAppender(new Log.DumpAppender());
meta_global = new ServerWBO('global');
server = httpd_setup({
"/1.1/johndoe/info/collections": uaHandler(collectionsHelper.handler),
"/1.1/johndoe/storage/meta/global": uaHandler(meta_global.handler()),
});
+ ensureLegacyIdentityManager();
setBasicCredentials("johndoe", "ilovejane");
- Service.serverURL = TEST_SERVER_URL;
- Service.clusterURL = TEST_CLUSTER_URL;
+ Service.serverURL = server.baseURI + "/";
+ Service.clusterURL = server.baseURI + "/";
+ _("Server URL: " + server.baseURI);
expectedUA = Services.appinfo.name + "/" + Services.appinfo.version +
" FxSync/" + WEAVE_VERSION + "." +
@@ -54,7 +55,7 @@ add_test(function test_fetchInfo() {
add_test(function test_desktop_post() {
_("Testing direct Resource POST.");
- let r = new AsyncResource(TEST_GET_URL);
+ let r = new AsyncResource(server.baseURI + "/1.1/johndoe/storage/meta/global");
r.post("foo=bar", function (error, content) {
_("User-Agent: " + ua);
do_check_eq(ua, expectedUA + ".desktop");
@@ -66,7 +67,7 @@ add_test(function test_desktop_post() {
add_test(function test_desktop_get() {
_("Testing async.");
Svc.Prefs.set("client.type", "desktop");
- let r = new AsyncResource(TEST_GET_URL);
+ let r = new AsyncResource(server.baseURI + "/1.1/johndoe/storage/meta/global");
r.get(function(error, content) {
_("User-Agent: " + ua);
do_check_eq(ua, expectedUA + ".desktop");
@@ -78,7 +79,7 @@ add_test(function test_desktop_get() {
add_test(function test_mobile_get() {
_("Testing mobile.");
Svc.Prefs.set("client.type", "mobile");
- let r = new AsyncResource(TEST_GET_URL);
+ let r = new AsyncResource(server.baseURI + "/1.1/johndoe/storage/meta/global");
r.get(function (error, content) {
_("User-Agent: " + ua);
do_check_eq(ua, expectedUA + ".mobile");
diff --git a/services/sync/tests/unit/test_score_triggers.js b/services/sync/tests/unit/test_score_triggers.js
index de08f8f08..98d3e094a 100644
--- a/services/sync/tests/unit/test_score_triggers.js
+++ b/services/sync/tests/unit/test_score_triggers.js
@@ -43,14 +43,14 @@ function sync_httpd_setup() {
return httpd_setup(handlers);
}
-function setUp() {
- new SyncTestingInfrastructure("johndoe", "ilovejane", "sekrit");
+function setUp(server) {
+ new SyncTestingInfrastructure(server, "johndoe", "ilovejane", "sekrit");
}
function run_test() {
initTestLogging("Trace");
- Log4Moz.repository.getLogger("Sync.Service").level = Log4Moz.Level.Trace;
+ Log.repository.getLogger("Sync.Service").level = Log.Level.Trace;
run_next_test();
}
@@ -80,7 +80,7 @@ add_test(function test_tracker_score_updated() {
add_test(function test_sync_triggered() {
let server = sync_httpd_setup();
- setUp();
+ setUp(server);
Service.login();
@@ -103,7 +103,7 @@ add_test(function test_clients_engine_sync_triggered() {
// global score tracker gives it that treatment. See bug 676042 for more.
let server = sync_httpd_setup();
- setUp();
+ setUp(server);
Service.login();
const TOPIC = "weave:service:sync:finish";
@@ -121,7 +121,7 @@ add_test(function test_clients_engine_sync_triggered() {
add_test(function test_incorrect_credentials_sync_not_triggered() {
_("Ensure that score changes don't trigger a sync if Status.login != LOGIN_SUCCEEDED.");
let server = sync_httpd_setup();
- setUp();
+ setUp(server);
// Ensure we don't actually try to sync.
function onSyncStart() {
diff --git a/services/sync/tests/unit/test_sendcredentials_controller.js b/services/sync/tests/unit/test_sendcredentials_controller.js
index 7a1770e07..42e5ec8e8 100644
--- a/services/sync/tests/unit/test_sendcredentials_controller.js
+++ b/services/sync/tests/unit/test_sendcredentials_controller.js
@@ -8,12 +8,13 @@ Cu.import("resource://services-sync/util.js");
Cu.import("resource://testing-common/services/sync/utils.js");
function run_test() {
+ ensureLegacyIdentityManager();
setBasicCredentials("johndoe", "ilovejane", Utils.generatePassphrase());
Service.serverURL = "http://weave.server/";
initTestLogging("Trace");
- Log4Moz.repository.getLogger("Sync.SendCredentialsController").level = Log4Moz.Level.Trace;
- Log4Moz.repository.getLogger("Sync.SyncScheduler").level = Log4Moz.Level.Trace;
+ Log.repository.getLogger("Sync.SendCredentialsController").level = Log.Level.Trace;
+ Log.repository.getLogger("Sync.SyncScheduler").level = Log.Level.Trace;
run_next_test();
}
diff --git a/services/sync/tests/unit/test_service_attributes.js b/services/sync/tests/unit/test_service_attributes.js
index d8a20eb2a..dc82f5edb 100644
--- a/services/sync/tests/unit/test_service_attributes.js
+++ b/services/sync/tests/unit/test_service_attributes.js
@@ -5,10 +5,12 @@ Cu.import("resource://services-sync/constants.js");
Cu.import("resource://services-sync/service.js");
Cu.import("resource://services-sync/util.js");
Cu.import("resource://testing-common/services/sync/fakeservices.js");
+Cu.import("resource://testing-common/services/sync/utils.js");
function test_urls() {
- _("URL related Service properties corresopnd to preference settings.");
+ _("URL related Service properties correspond to preference settings.");
try {
+ ensureLegacyIdentityManager();
do_check_true(!!Service.serverURL); // actual value may change
do_check_eq(Service.clusterURL, "");
do_check_eq(Service.userBaseURL, undefined);
diff --git a/services/sync/tests/unit/test_service_changePassword.js b/services/sync/tests/unit/test_service_changePassword.js
index d7b65df80..12b0ad00e 100644
--- a/services/sync/tests/unit/test_service_changePassword.js
+++ b/services/sync/tests/unit/test_service_changePassword.js
@@ -1,7 +1,7 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
-Cu.import("resource://services-common/log4moz.js");
+Cu.import("resource://gre/modules/Log.jsm");
Cu.import("resource://services-sync/constants.js");
Cu.import("resource://services-sync/service.js");
Cu.import("resource://services-sync/util.js");
@@ -9,9 +9,11 @@ Cu.import("resource://testing-common/services/sync/utils.js");
function run_test() {
initTestLogging("Trace");
- Log4Moz.repository.getLogger("Sync.AsyncResource").level = Log4Moz.Level.Trace;
- Log4Moz.repository.getLogger("Sync.Resource").level = Log4Moz.Level.Trace;
- Log4Moz.repository.getLogger("Sync.Service").level = Log4Moz.Level.Trace;
+ Log.repository.getLogger("Sync.AsyncResource").level = Log.Level.Trace;
+ Log.repository.getLogger("Sync.Resource").level = Log.Level.Trace;
+ Log.repository.getLogger("Sync.Service").level = Log.Level.Trace;
+
+ ensureLegacyIdentityManager();
run_next_test();
}
@@ -29,8 +31,8 @@ add_test(function test_change_password() {
}
try {
- Service.serverURL = TEST_SERVER_URL;
- Service.clusterURL = TEST_CLUSTER_URL;
+ Service.baseURI = "http://localhost:9999/";
+ Service.serverURL = "http://localhost:9999/";
setBasicCredentials("johndoe", "ilovejane");
_("changePassword() returns false for a network error, the password won't change.");
@@ -44,6 +46,7 @@ add_test(function test_change_password() {
"/user/1.0/janedoe/password": send(401, "Unauthorized", "Forbidden!")
});
+ Service.serverURL = server.baseURI;
res = Service.changePassword("ILoveJane83");
do_check_true(res);
do_check_eq(Service.identity.basicPassword, "ILoveJane83");
diff --git a/services/sync/tests/unit/test_service_checkAccount.js b/services/sync/tests/unit/test_service_checkAccount.js
index b5c536ec9..618348d1a 100644
--- a/services/sync/tests/unit/test_service_checkAccount.js
+++ b/services/sync/tests/unit/test_service_checkAccount.js
@@ -7,6 +7,7 @@ Cu.import("resource://testing-common/services/sync/utils.js");
function run_test() {
do_test_pending();
+ ensureLegacyIdentityManager();
let server = httpd_setup({
"/user/1.0/johndoe": httpd_handler(200, "OK", "1"),
"/user/1.0/janedoe": httpd_handler(200, "OK", "0"),
@@ -16,7 +17,7 @@ function run_test() {
"/user/1.0/vuuf3eqgloxpxmzph27f5a6ve7gzlrms": httpd_handler(200, "OK", "1")
});
try {
- Service.serverURL = TEST_SERVER_URL;
+ Service.serverURL = server.baseURI;
_("A 404 will be recorded as 'generic-server-error'");
do_check_eq(Service.checkAccount("jimdoe"), "generic-server-error");
diff --git a/services/sync/tests/unit/test_service_cluster.js b/services/sync/tests/unit/test_service_cluster.js
index 6c4dddbed..65f0c3a95 100644
--- a/services/sync/tests/unit/test_service_cluster.js
+++ b/services/sync/tests/unit/test_service_cluster.js
@@ -18,12 +18,12 @@ function do_check_throws(func) {
add_test(function test_findCluster() {
_("Test Service._findCluster()");
let server;
+ ensureLegacyIdentityManager();
try {
- Service.serverURL = TEST_SERVER_URL;
- Service.identity.account = "johndoe";
-
_("_findCluster() throws on network errors (e.g. connection refused).");
do_check_throws(function() {
+ Service.serverURL = "http://dummy:9000/";
+ Service.identity.account = "johndoe";
Service._clusterManager._findCluster();
});
@@ -35,6 +35,9 @@ add_test(function test_findCluster() {
"/user/1.0/joedoe/node/weave": httpd_handler(500, "Server Error", "Server Error")
});
+ Service.serverURL = server.baseURI;
+ Service.identity.account = "johndoe";
+
_("_findCluster() returns the user's cluster node");
let cluster = Service._clusterManager._findCluster();
do_check_eq(cluster, "http://weave.user.node/");
@@ -76,7 +79,7 @@ add_test(function test_setCluster() {
"/user/1.0/jimdoe/node/weave": httpd_handler(200, "OK", "null")
});
try {
- Service.serverURL = TEST_SERVER_URL;
+ Service.serverURL = server.baseURI;
Service.identity.account = "johndoe";
_("Check initial state.");
diff --git a/services/sync/tests/unit/test_service_createAccount.js b/services/sync/tests/unit/test_service_createAccount.js
index 976ec98d0..93c6f78e3 100644
--- a/services/sync/tests/unit/test_service_createAccount.js
+++ b/services/sync/tests/unit/test_service_createAccount.js
@@ -32,7 +32,7 @@ function run_test() {
"/user/1.0/vz6fhecgw5t3sgx3a4cektoiokyczkqd": send(500, "Server Error", "Server Error")
});
try {
- Service.serverURL = TEST_SERVER_URL;
+ Service.serverURL = server.baseURI;
_("Create an account.");
let res = Service.createAccount("john@doe.com", "mysecretpw",
diff --git a/services/sync/tests/unit/test_service_detect_upgrade.js b/services/sync/tests/unit/test_service_detect_upgrade.js
index baff1bc33..528bd751b 100644
--- a/services/sync/tests/unit/test_service_detect_upgrade.js
+++ b/services/sync/tests/unit/test_service_detect_upgrade.js
@@ -1,7 +1,7 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
-Cu.import("resource://services-common/log4moz.js");
+Cu.import("resource://gre/modules/Log.jsm");
Cu.import("resource://services-sync/constants.js");
Cu.import("resource://services-sync/keys.js");
Cu.import("resource://services-sync/engines/tabs.js");
@@ -43,6 +43,8 @@ add_test(function v4_upgrade() {
"/1.1/johndoe/storage/prefs": new ServerCollection().handler()
});
+ ensureLegacyIdentityManager();
+
try {
_("Set up some tabs.");
@@ -54,10 +56,8 @@ add_test(function v4_upgrade() {
}],
attributes: {
image: "image"
- },
- extData: {
- weaveLastUsed: 1
- }}]}]};
+ }
+ }]}]};
delete Svc.Session;
Svc.Session = {
getBrowserState: function () JSON.stringify(myTabs)
@@ -66,8 +66,7 @@ add_test(function v4_upgrade() {
Service.status.resetSync();
_("Logging in.");
- Service.serverURL = TEST_SERVER_URL;
- Service.clusterURL = TEST_CLUSTER_URL;
+ Service.serverURL = server.baseURI;
Service.login("johndoe", "ilovejane", passphrase);
do_check_true(Service.isLoggedIn);
@@ -102,8 +101,7 @@ add_test(function v4_upgrade() {
_("Syncing afresh...");
Service.logout();
Service.collectionKeys.clear();
- Service.serverURL = TEST_SERVER_URL;
- Service.clusterURL = TEST_CLUSTER_URL;
+ Service.serverURL = server.baseURI;
meta_global.payload = JSON.stringify({"syncID": "foooooooooooooobbbbbbbbbbbb",
"storageVersion": STORAGE_VERSION});
collections.meta = Date.now() / 1000;
@@ -227,10 +225,8 @@ add_test(function v5_upgrade() {
}],
attributes: {
image: "image"
- },
- extData: {
- weaveLastUsed: 1
- }}]}]};
+ }
+ }]}]};
delete Svc.Session;
Svc.Session = {
getBrowserState: function () JSON.stringify(myTabs)
@@ -239,8 +235,8 @@ add_test(function v5_upgrade() {
Service.status.resetSync();
setBasicCredentials("johndoe", "ilovejane", passphrase);
- Service.serverURL = TEST_SERVER_URL;
- Service.clusterURL = TEST_CLUSTER_URL;
+ Service.serverURL = server.baseURI + "/";
+ Service.clusterURL = server.baseURI + "/";
// Test an upgrade where the contents of the server would cause us to error
// -- keys decrypted with a different sync key, for example.
@@ -294,8 +290,8 @@ add_test(function v5_upgrade() {
});
function run_test() {
- let logger = Log4Moz.repository.rootLogger;
- Log4Moz.repository.rootLogger.addAppender(new Log4Moz.DumpAppender());
+ let logger = Log.repository.rootLogger;
+ Log.repository.rootLogger.addAppender(new Log.DumpAppender());
run_next_test();
}
diff --git a/services/sync/tests/unit/test_service_getStorageInfo.js b/services/sync/tests/unit/test_service_getStorageInfo.js
index be40feae8..4d463044b 100644
--- a/services/sync/tests/unit/test_service_getStorageInfo.js
+++ b/services/sync/tests/unit/test_service_getStorageInfo.js
@@ -12,20 +12,21 @@ let collections = {steam: 65.11328,
diesel: 2.25488281};
function run_test() {
- setBasicCredentials("johndoe", "ilovejane");
- Service.serverURL = TEST_SERVER_URL;
- Service.clusterURL = TEST_CLUSTER_URL;
-
- Log4Moz.repository.getLogger("Sync.Service").level = Log4Moz.Level.Trace;
- Log4Moz.repository.getLogger("Sync.StorageRequest").level = Log4Moz.Level.Trace;
+ Log.repository.getLogger("Sync.Service").level = Log.Level.Trace;
+ Log.repository.getLogger("Sync.StorageRequest").level = Log.Level.Trace;
initTestLogging();
+ ensureLegacyIdentityManager();
+ setBasicCredentials("johndoe", "ilovejane");
+
run_next_test();
}
add_test(function test_success() {
let handler = httpd_handler(200, "OK", JSON.stringify(collections));
let server = httpd_setup({"/1.1/johndoe/info/collections": handler});
+ Service.serverURL = server.baseURI + "/";
+ Service.clusterURL = server.baseURI + "/";
let request = Service.getStorageInfo("collections", function (error, info) {
do_check_eq(error, null);
@@ -65,6 +66,8 @@ add_test(function test_network_error() {
add_test(function test_http_error() {
let handler = httpd_handler(500, "Oh noez", "Something went wrong!");
let server = httpd_setup({"/1.1/johndoe/info/collections": handler});
+ Service.serverURL = server.baseURI + "/";
+ Service.clusterURL = server.baseURI + "/";
let request = Service.getStorageInfo(INFO_COLLECTIONS, function (error, info) {
do_check_eq(error.status, 500);
@@ -76,10 +79,11 @@ add_test(function test_http_error() {
add_test(function test_invalid_json() {
let handler = httpd_handler(200, "OK", "Invalid JSON");
let server = httpd_setup({"/1.1/johndoe/info/collections": handler});
+ Service.serverURL = server.baseURI + "/";
+ Service.clusterURL = server.baseURI + "/";
let request = Service.getStorageInfo(INFO_COLLECTIONS, function (error, info) {
do_check_eq(error.name, "SyntaxError");
- do_check_eq(error.message, "JSON.parse: unexpected character");
do_check_eq(info, null);
server.stop(run_next_test);
});
diff --git a/services/sync/tests/unit/test_service_login.js b/services/sync/tests/unit/test_service_login.js
index 47295bc59..62f406fe3 100644
--- a/services/sync/tests/unit/test_service_login.js
+++ b/services/sync/tests/unit/test_service_login.js
@@ -1,7 +1,7 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
-Cu.import("resource://services-common/log4moz.js");
+Cu.import("resource://gre/modules/Log.jsm");
Cu.import("resource://services-sync/constants.js");
Cu.import("resource://services-sync/service.js");
Cu.import("resource://services-sync/policies.js");
@@ -23,8 +23,8 @@ function login_handling(handler) {
}
function run_test() {
- let logger = Log4Moz.repository.rootLogger;
- Log4Moz.repository.rootLogger.addAppender(new Log4Moz.DumpAppender());
+ let logger = Log.repository.rootLogger;
+ Log.repository.rootLogger.addAppender(new Log.DumpAppender());
run_next_test();
}
@@ -43,9 +43,6 @@ add_test(function test_offline() {
});
function setup() {
- Service.serverURL = TEST_SERVER_URL;
- Service.clusterURL = TEST_CLUSTER_URL;
-
let janeHelper = track_collections_helper();
let janeU = janeHelper.with_updated_collection;
let janeColls = janeHelper.collections;
@@ -53,7 +50,7 @@ function setup() {
let johnU = johnHelper.with_updated_collection;
let johnColls = johnHelper.collections;
- return httpd_setup({
+ let server = httpd_setup({
"/1.1/johndoe/info/collections": login_handling(johnHelper.handler),
"/1.1/janedoe/info/collections": login_handling(janeHelper.handler),
@@ -65,6 +62,9 @@ function setup() {
"/1.1/janedoe/storage/crypto/keys": janeU("crypto", new ServerWBO("keys").handler()),
"/1.1/janedoe/storage/meta/global": janeU("meta", new ServerWBO("global").handler())
});
+
+ Service.serverURL = server.baseURI;
+ return server;
}
add_test(function test_login_logout() {
@@ -72,6 +72,7 @@ add_test(function test_login_logout() {
try {
_("Force the initial state.");
+ ensureLegacyIdentityManager();
Service.status.service = STATUS_OK;
do_check_eq(Service.status.service, STATUS_OK);
@@ -158,7 +159,7 @@ add_test(function test_login_on_sync() {
// Stub mpLocked.
let mpLockedF = Utils.mpLocked;
let mpLocked = true;
- Utils.mpLocked = function() mpLocked;
+ Utils.mpLocked = () => mpLocked;
// Stub scheduleNextSync. This gets called within checkSyncStatus if we're
// ready to sync, so use it as an indicator.
@@ -182,7 +183,7 @@ add_test(function test_login_on_sync() {
// This test exercises these two branches.
_("We're ready to sync if locked.");
- Service.enabled = true;
+ Svc.Prefs.set("enabled", true);
Services.io.offline = false;
Service.scheduler.checkSyncStatus();
do_check_true(scheduleCalled);
diff --git a/services/sync/tests/unit/test_service_passwordUTF8.js b/services/sync/tests/unit/test_service_passwordUTF8.js
index c7b52018d..733911291 100644
--- a/services/sync/tests/unit/test_service_passwordUTF8.js
+++ b/services/sync/tests/unit/test_service_passwordUTF8.js
@@ -58,6 +58,8 @@ function run_test() {
let upd = collectionsHelper.with_updated_collection;
let collections = collectionsHelper.collections;
+ ensureLegacyIdentityManager();
+
do_test_pending();
let server = httpd_setup({
"/1.1/johndoe/info/collections": login_handling(collectionsHelper.handler),
@@ -67,7 +69,7 @@ function run_test() {
});
setBasicCredentials("johndoe", JAPANESE, "irrelevant");
- Service.serverURL = TEST_SERVER_URL;
+ Service.serverURL = server.baseURI;
try {
_("Try to log in with the password.");
diff --git a/services/sync/tests/unit/test_service_persistLogin.js b/services/sync/tests/unit/test_service_persistLogin.js
index c0d78e525..9d4a1e51a 100644
--- a/services/sync/tests/unit/test_service_persistLogin.js
+++ b/services/sync/tests/unit/test_service_persistLogin.js
@@ -9,6 +9,7 @@ Cu.import("resource://testing-common/services/sync/utils.js");
function run_test() {
try {
// Ensure we have a blank slate to start.
+ ensureLegacyIdentityManager();
Services.logins.removeAllLogins();
setBasicCredentials("johndoe", "ilovejane", "abbbbbcccccdddddeeeeefffff");
diff --git a/services/sync/tests/unit/test_service_startOver.js b/services/sync/tests/unit/test_service_startOver.js
index aa2e53dac..6fb0a66d7 100644
--- a/services/sync/tests/unit/test_service_startOver.js
+++ b/services/sync/tests/unit/test_service_startOver.js
@@ -28,10 +28,8 @@ function run_test() {
run_next_test();
}
-add_test(function test_resetLocalData() {
- // Set up.
- setBasicCredentials("foobar", "blablabla", // Law Blog
- "abcdeabcdeabcdeabcdeabcdea");
+add_identity_test(this, function test_resetLocalData() {
+ yield configureIdentity();
Service.status.enforceBackoff = true;
Service.status.backoffInterval = 42;
Service.status.minimumNextSync = 23;
@@ -61,8 +59,6 @@ add_test(function test_resetLocalData() {
do_check_false(Service.status.enforceBackoff);
do_check_eq(Service.status.backoffInterval, 0);
do_check_eq(Service.status.minimumNextSync, 0);
-
- run_next_test();
});
add_test(function test_removeClientData() {
@@ -73,8 +69,8 @@ add_test(function test_removeClientData() {
Service.startOver();
do_check_false(engine.removed);
- Service.serverURL = TEST_SERVER_URL;
- Service.clusterURL = TEST_CLUSTER_URL;
+ Service.serverURL = "https://localhost/";
+ Service.clusterURL = Service.serverURL;
do_check_false(engine.removed);
Service.startOver();
diff --git a/services/sync/tests/unit/test_service_startup.js b/services/sync/tests/unit/test_service_startup.js
index ce9bb2431..6ced39da9 100644
--- a/services/sync/tests/unit/test_service_startup.js
+++ b/services/sync/tests/unit/test_service_startup.js
@@ -13,10 +13,14 @@ function run_test() {
_("When imported, Service.onStartup is called");
initTestLogging("Trace");
- new SyncTestingInfrastructure();
+ let xps = Cc["@mozilla.org/weave/service;1"]
+ .getService(Ci.nsISupports)
+ .wrappedJSObject;
+ do_check_false(xps.enabled);
// Test fixtures
Service.identity.username = "johndoe";
+ do_check_false(xps.enabled);
Cu.import("resource://services-sync/service.js");
@@ -31,10 +35,6 @@ function run_test() {
_("Observers are notified of startup");
do_test_pending();
- let xps = Cc["@mozilla.org/weave/service;1"]
- .getService(Ci.nsISupports)
- .wrappedJSObject;
-
do_check_false(Service.status.ready);
do_check_false(xps.ready);
Observers.add("weave:service:ready", function (subject, data) {
@@ -45,4 +45,10 @@ function run_test() {
Svc.Prefs.resetBranch("");
do_test_finished();
});
+
+ do_check_false(xps.enabled);
+
+ Service.identity.account = "johndoe";
+ Service.clusterURL = "http://localhost/";
+ do_check_true(xps.enabled);
}
diff --git a/services/sync/tests/unit/test_service_sync_401.js b/services/sync/tests/unit/test_service_sync_401.js
index 5bbc8324f..9e9db8137 100644
--- a/services/sync/tests/unit/test_service_sync_401.js
+++ b/services/sync/tests/unit/test_service_sync_401.js
@@ -20,8 +20,8 @@ function login_handling(handler) {
}
function run_test() {
- let logger = Log4Moz.repository.rootLogger;
- Log4Moz.repository.rootLogger.addAppender(new Log4Moz.DumpAppender());
+ let logger = Log.repository.rootLogger;
+ Log.repository.rootLogger.addAppender(new Log.DumpAppender());
let collectionsHelper = track_collections_helper();
let upd = collectionsHelper.with_updated_collection;
@@ -38,7 +38,7 @@ function run_test() {
try {
_("Set up test fixtures.");
- new SyncTestingInfrastructure("johndoe", "ilovejane", "foo");
+ new SyncTestingInfrastructure(server, "johndoe", "ilovejane", "foo");
Service.scheduler.globalScore = GLOBAL_SCORE;
// Avoid daily ping
Svc.Prefs.set("lastPing", Math.floor(Date.now() / 1000));
diff --git a/services/sync/tests/unit/test_service_sync_locked.js b/services/sync/tests/unit/test_service_sync_locked.js
index f66a37efd..e2cbbfa92 100644
--- a/services/sync/tests/unit/test_service_sync_locked.js
+++ b/services/sync/tests/unit/test_service_sync_locked.js
@@ -16,7 +16,7 @@ function run_test() {
return old;
}
- Log4Moz.repository.rootLogger.addAppender(new Log4Moz.DumpAppender());
+ Log.repository.rootLogger.addAppender(new Log.DumpAppender());
augmentLogger(Service._log);
diff --git a/services/sync/tests/unit/test_service_sync_remoteSetup.js b/services/sync/tests/unit/test_service_sync_remoteSetup.js
index 98bb3dabe..852ba64d5 100644
--- a/services/sync/tests/unit/test_service_sync_remoteSetup.js
+++ b/services/sync/tests/unit/test_service_sync_remoteSetup.js
@@ -1,7 +1,7 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
-Cu.import("resource://services-common/log4moz.js");
+Cu.import("resource://gre/modules/Log.jsm");
Cu.import("resource://services-sync/constants.js");
Cu.import("resource://services-sync/keys.js");
Cu.import("resource://services-sync/service.js");
@@ -10,8 +10,8 @@ Cu.import("resource://testing-common/services/sync/fakeservices.js");
Cu.import("resource://testing-common/services/sync/utils.js");
function run_test() {
- let logger = Log4Moz.repository.rootLogger;
- Log4Moz.repository.rootLogger.addAppender(new Log4Moz.DumpAppender());
+ let logger = Log.repository.rootLogger;
+ Log.repository.rootLogger.addAppender(new Log.DumpAppender());
let guidSvc = new FakeGUIDService();
let clients = new ServerCollection();
@@ -65,8 +65,8 @@ function run_test() {
try {
_("Log in.");
- Service.serverURL = TEST_SERVER_URL;
- Service.clusterURL = TEST_CLUSTER_URL;
+ ensureLegacyIdentityManager();
+ Service.serverURL = server.baseURI;
_("Checking Status.sync with no credentials.");
Service.verifyAndFetchSymmetricKeys();
@@ -81,8 +81,7 @@ function run_test() {
let syncKey = Service.identity.syncKey;
Service.startOver();
- Service.serverURL = TEST_SERVER_URL;
- Service.clusterURL = TEST_CLUSTER_URL;
+ Service.serverURL = server.baseURI;
Service.login("johndoe", "ilovejane", syncKey);
do_check_true(Service.isLoggedIn);
diff --git a/services/sync/tests/unit/test_service_sync_updateEnabledEngines.js b/services/sync/tests/unit/test_service_sync_updateEnabledEngines.js
index 621b114d0..c945cb6c2 100644
--- a/services/sync/tests/unit/test_service_sync_updateEnabledEngines.js
+++ b/services/sync/tests/unit/test_service_sync_updateEnabledEngines.js
@@ -66,8 +66,8 @@ function sync_httpd_setup(handlers) {
return httpd_setup(handlers);
}
-function setUp() {
- new SyncTestingInfrastructure("johndoe", "ilovejane",
+function setUp(server) {
+ new SyncTestingInfrastructure(server, "johndoe", "ilovejane",
"abcdeabcdeabcdeabcdeabcdea");
// Ensure that the server has valid keys so that logging in will work and not
// result in a server wipe, rendering many of these tests useless.
@@ -82,8 +82,8 @@ const PAYLOAD = 42;
function run_test() {
initTestLogging("Trace");
- Log4Moz.repository.getLogger("Sync.Service").level = Log4Moz.Level.Trace;
- Log4Moz.repository.getLogger("Sync.ErrorHandler").level = Log4Moz.Level.Trace;
+ Log.repository.getLogger("Sync.Service").level = Log.Level.Trace;
+ Log.repository.getLogger("Sync.ErrorHandler").level = Log.Level.Trace;
run_next_test();
}
@@ -95,7 +95,7 @@ add_test(function test_newAccount() {
"/1.1/johndoe/storage/meta/global": new ServerWBO("global", {}).handler(),
"/1.1/johndoe/storage/steam": new ServerWBO("steam", {}).handler()
});
- setUp();
+ setUp(server);
try {
_("Engine is enabled from the beginning.");
@@ -125,7 +125,7 @@ add_test(function test_enabledLocally() {
"/1.1/johndoe/storage/meta/global": metaWBO.handler(),
"/1.1/johndoe/storage/steam": new ServerWBO("steam", {}).handler()
});
- setUp();
+ setUp(server);
try {
_("Enable engine locally.");
@@ -161,7 +161,7 @@ add_test(function test_disabledLocally() {
"/1.1/johndoe/storage/meta/global": metaWBO.handler(),
"/1.1/johndoe/storage/steam": steamCollection.handler()
});
- setUp();
+ setUp(server);
try {
_("Disable engine locally.");
@@ -210,7 +210,7 @@ add_test(function test_disabledLocally_wipe503() {
"/1.1/johndoe/storage/meta/global": metaWBO.handler(),
"/1.1/johndoe/storage/steam": service_unavailable
});
- setUp();
+ setUp(server);
_("Disable engine locally.");
Service._ignorePrefObserver = true;
@@ -248,7 +248,7 @@ add_test(function test_enabledRemotely() {
"/1.1/johndoe/storage/steam":
upd("steam", new ServerWBO("steam", {}).handler())
});
- setUp();
+ setUp(server);
// We need to be very careful how we do this, so that we don't trigger a
// fresh start!
@@ -289,7 +289,7 @@ add_test(function test_disabledRemotelyTwoClients() {
"/1.1/johndoe/storage/steam":
upd("steam", new ServerWBO("steam", {}).handler())
});
- setUp();
+ setUp(server);
try {
_("Enable engine locally.");
@@ -330,7 +330,7 @@ add_test(function test_disabledRemotely() {
"/1.1/johndoe/storage/meta/global": metaWBO.handler(),
"/1.1/johndoe/storage/steam": new ServerWBO("steam", {}).handler()
});
- setUp();
+ setUp(server);
try {
_("Enable engine locally.");
@@ -363,7 +363,7 @@ add_test(function test_dependentEnginesEnabledLocally() {
"/1.1/johndoe/storage/steam": new ServerWBO("steam", {}).handler(),
"/1.1/johndoe/storage/stirling": new ServerWBO("stirling", {}).handler()
});
- setUp();
+ setUp(server);
try {
_("Enable engine locally. Doing it on one is enough.");
@@ -407,7 +407,7 @@ add_test(function test_dependentEnginesDisabledLocally() {
"/1.1/johndoe/storage/steam": steamCollection.handler(),
"/1.1/johndoe/storage/stirling": stirlingCollection.handler()
});
- setUp();
+ setUp(server);
try {
_("Disable engines locally. Doing it on one is enough.");
diff --git a/services/sync/tests/unit/test_service_verifyLogin.js b/services/sync/tests/unit/test_service_verifyLogin.js
index 0d33f50a5..2a27fd1b0 100644
--- a/services/sync/tests/unit/test_service_verifyLogin.js
+++ b/services/sync/tests/unit/test_service_verifyLogin.js
@@ -1,7 +1,7 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
-Cu.import("resource://services-common/log4moz.js");
+Cu.import("resource://gre/modules/Log.jsm");
Cu.import("resource://services-sync/constants.js");
Cu.import("resource://services-sync/service.js");
Cu.import("resource://services-sync/util.js");
@@ -27,9 +27,10 @@ function service_unavailable(request, response) {
}
function run_test() {
- let logger = Log4Moz.repository.rootLogger;
- Log4Moz.repository.rootLogger.addAppender(new Log4Moz.DumpAppender());
+ let logger = Log.repository.rootLogger;
+ Log.repository.rootLogger.addAppender(new Log.DumpAppender());
+ ensureLegacyIdentityManager();
// This test expects a clean slate -- no saved passphrase.
Services.logins.removeAllLogins();
let johnHelper = track_collections_helper();
@@ -37,17 +38,25 @@ function run_test() {
let johnColls = johnHelper.collections;
do_test_pending();
- let server = httpd_setup({
+
+ let server;
+ function weaveHandler (request, response) {
+ response.setStatusLine(request.httpVersion, 200, "OK");
+ let body = server.baseURI + "/api/";
+ response.bodyOutputStream.write(body, body.length);
+ }
+
+ server = httpd_setup({
"/api/1.1/johndoe/info/collections": login_handling(johnHelper.handler),
"/api/1.1/janedoe/info/collections": service_unavailable,
"/api/1.1/johndoe/storage/crypto/keys": johnU("crypto", new ServerWBO("keys").handler()),
"/api/1.1/johndoe/storage/meta/global": johnU("meta", new ServerWBO("global").handler()),
- "/user/1.0/johndoe/node/weave": httpd_handler(200, "OK", "http://localhost:8080/api/")
+ "/user/1.0/johndoe/node/weave": weaveHandler,
});
try {
- Service.serverURL = TEST_SERVER_URL;
+ Service.serverURL = server.baseURI;
_("Force the initial state.");
Service.status.service = STATUS_OK;
@@ -67,7 +76,7 @@ function run_test() {
do_check_eq(Service.status.login, LOGIN_FAILED_NO_PASSPHRASE);
_("verifyLogin() has found out the user's cluster URL, though.");
- do_check_eq(Service.clusterURL, "http://localhost:8080/api/");
+ do_check_eq(Service.clusterURL, server.baseURI + "/api/");
_("Success if passphrase is set.");
Service.status.resetSync();
diff --git a/services/sync/tests/unit/test_service_wipeClient.js b/services/sync/tests/unit/test_service_wipeClient.js
index 752611dbf..aab769229 100644
--- a/services/sync/tests/unit/test_service_wipeClient.js
+++ b/services/sync/tests/unit/test_service_wipeClient.js
@@ -82,9 +82,10 @@ add_test(function test_credentials_preserved() {
_("Ensure that credentials are preserved if client is wiped.");
// Required for wipeClient().
- Service.clusterURL = TEST_CLUSTER_URL;
+ ensureLegacyIdentityManager();
Service.identity.account = "testaccount";
Service.identity.basicPassword = "testpassword";
+ Service.clusterURL = "http://dummy:9000/";
let key = Utils.generatePassphrase();
Service.identity.syncKey = key;
Service.identity.persistCredentials();
diff --git a/services/sync/tests/unit/test_service_wipeServer.js b/services/sync/tests/unit/test_service_wipeServer.js
index e03f6caf0..3fc45cf86 100644
--- a/services/sync/tests/unit/test_service_wipeServer.js
+++ b/services/sync/tests/unit/test_service_wipeServer.js
@@ -7,6 +7,9 @@ Cu.import("resource://testing-common/services/sync/utils.js");
Svc.DefaultPrefs.set("registerEngines", "");
Cu.import("resource://services-sync/service.js");
+// configure the identity we use for this test.
+identityConfig = makeIdentityConfig({username: "johndoe"});
+
function FakeCollection() {
this.deleted = false;
}
@@ -28,13 +31,13 @@ FakeCollection.prototype = {
}
};
-function setUpTestFixtures() {
+function setUpTestFixtures(server) {
let cryptoService = new FakeCryptoService();
- Service.serverURL = TEST_SERVER_URL;
- Service.clusterURL = TEST_CLUSTER_URL;
+ Service.serverURL = server.baseURI + "/";
+ Service.clusterURL = server.baseURI + "/";
- setBasicCredentials("johndoe", null, "aabcdeabcdeabcdeabcdeabcde");
+ yield configureIdentity(identityConfig);
}
@@ -43,7 +46,13 @@ function run_test() {
run_next_test();
}
-add_test(function test_wipeServer_list_success() {
+function promiseStopServer(server) {
+ let deferred = Promise.defer();
+ server.stop(deferred.resolve);
+ return deferred.promise;
+}
+
+add_identity_test(this, function test_wipeServer_list_success() {
_("Service.wipeServer() deletes collections given as argument.");
let steam_coll = new FakeCollection();
@@ -56,8 +65,8 @@ add_test(function test_wipeServer_list_success() {
});
try {
- setUpTestFixtures();
- new SyncTestingInfrastructure("johndoe", "irrelevant", "irrelevant");
+ yield setUpTestFixtures(server);
+ new SyncTestingInfrastructure(server, "johndoe", "irrelevant", "irrelevant");
_("Confirm initial environment.");
do_check_false(steam_coll.deleted);
@@ -72,12 +81,12 @@ add_test(function test_wipeServer_list_success() {
do_check_true(diesel_coll.deleted);
} finally {
- server.stop(run_next_test);
+ yield promiseStopServer(server);
Svc.Prefs.resetBranch("");
}
});
-add_test(function test_wipeServer_list_503() {
+add_identity_test(this, function test_wipeServer_list_503() {
_("Service.wipeServer() deletes collections given as argument.");
let steam_coll = new FakeCollection();
@@ -90,8 +99,8 @@ add_test(function test_wipeServer_list_503() {
});
try {
- setUpTestFixtures();
- new SyncTestingInfrastructure("johndoe", "irrelevant", "irrelevant");
+ yield setUpTestFixtures(server);
+ new SyncTestingInfrastructure(server, "johndoe", "irrelevant", "irrelevant");
_("Confirm initial environment.");
do_check_false(steam_coll.deleted);
@@ -113,12 +122,12 @@ add_test(function test_wipeServer_list_503() {
do_check_false(diesel_coll.deleted);
} finally {
- server.stop(run_next_test);
+ yield promiseStopServer(server);
Svc.Prefs.resetBranch("");
}
});
-add_test(function test_wipeServer_all_success() {
+add_identity_test(this, function test_wipeServer_all_success() {
_("Service.wipeServer() deletes all the things.");
/**
@@ -136,19 +145,19 @@ add_test(function test_wipeServer_all_success() {
let server = httpd_setup({
"/1.1/johndoe/storage": storageHandler
});
- setUpTestFixtures();
+ yield setUpTestFixtures(server);
_("Try deletion.");
- new SyncTestingInfrastructure("johndoe", "irrelevant", "irrelevant");
+ new SyncTestingInfrastructure(server, "johndoe", "irrelevant", "irrelevant");
let returnedTimestamp = Service.wipeServer();
do_check_true(deleted);
do_check_eq(returnedTimestamp, serverTimestamp);
- server.stop(run_next_test);
+ yield promiseStopServer(server);
Svc.Prefs.resetBranch("");
});
-add_test(function test_wipeServer_all_404() {
+add_identity_test(this, function test_wipeServer_all_404() {
_("Service.wipeServer() accepts a 404.");
/**
@@ -168,19 +177,19 @@ add_test(function test_wipeServer_all_404() {
let server = httpd_setup({
"/1.1/johndoe/storage": storageHandler
});
- setUpTestFixtures();
+ yield setUpTestFixtures(server);
_("Try deletion.");
- new SyncTestingInfrastructure("johndoe", "irrelevant", "irrelevant");
+ new SyncTestingInfrastructure(server, "johndoe", "irrelevant", "irrelevant");
let returnedTimestamp = Service.wipeServer();
do_check_true(deleted);
do_check_eq(returnedTimestamp, serverTimestamp);
- server.stop(run_next_test);
+ yield promiseStopServer(server);
Svc.Prefs.resetBranch("");
});
-add_test(function test_wipeServer_all_503() {
+add_identity_test(this, function test_wipeServer_all_503() {
_("Service.wipeServer() throws if it encounters a non-200/404 response.");
/**
@@ -195,12 +204,12 @@ add_test(function test_wipeServer_all_503() {
let server = httpd_setup({
"/1.1/johndoe/storage": storageHandler
});
- setUpTestFixtures();
+ yield setUpTestFixtures(server);
_("Try deletion.");
let error;
try {
- new SyncTestingInfrastructure("johndoe", "irrelevant", "irrelevant");
+ new SyncTestingInfrastructure(server, "johndoe", "irrelevant", "irrelevant");
Service.wipeServer();
do_throw("Should have thrown!");
} catch (ex) {
@@ -208,13 +217,17 @@ add_test(function test_wipeServer_all_503() {
}
do_check_eq(error.status, 503);
- server.stop(run_next_test);
+ yield promiseStopServer(server);
Svc.Prefs.resetBranch("");
});
-add_test(function test_wipeServer_all_connectionRefused() {
+add_identity_test(this, function test_wipeServer_all_connectionRefused() {
_("Service.wipeServer() throws if it encounters a network problem.");
- setUpTestFixtures();
+ let server = httpd_setup({});
+ yield setUpTestFixtures(server);
+
+ Service.serverURL = "http://localhost:4352/";
+ Service.clusterURL = "http://localhost:4352/";
_("Try deletion.");
try {
@@ -224,6 +237,6 @@ add_test(function test_wipeServer_all_connectionRefused() {
do_check_eq(ex.result, Cr.NS_ERROR_CONNECTION_REFUSED);
}
- run_next_test();
Svc.Prefs.resetBranch("");
+ yield promiseStopServer(server);
});
diff --git a/services/sync/tests/unit/test_status_checkSetup.js b/services/sync/tests/unit/test_status_checkSetup.js
index f68b1b693..64a6aac93 100644
--- a/services/sync/tests/unit/test_status_checkSetup.js
+++ b/services/sync/tests/unit/test_status_checkSetup.js
@@ -4,9 +4,11 @@
Cu.import("resource://services-sync/constants.js");
Cu.import("resource://services-sync/status.js");
Cu.import("resource://services-sync/util.js");
+Cu.import("resource://testing-common/services/sync/utils.js");
function run_test() {
initTestLogging("Trace");
+ ensureLegacyIdentityManager();
try {
_("Ensure fresh config.");
diff --git a/services/sync/tests/unit/test_syncengine.js b/services/sync/tests/unit/test_syncengine.js
index 21a76b2d2..393e49607 100644
--- a/services/sync/tests/unit/test_syncengine.js
+++ b/services/sync/tests/unit/test_syncengine.js
@@ -10,9 +10,11 @@ function makeSteamEngine() {
return new SyncEngine('Steam', Service);
}
+let server;
+
function test_url_attributes() {
_("SyncEngine url attributes");
- let syncTesting = new SyncTestingInfrastructure();
+ let syncTesting = new SyncTestingInfrastructure(server);
Service.clusterURL = "https://cluster/";
let engine = makeSteamEngine();
try {
@@ -26,7 +28,7 @@ function test_url_attributes() {
function test_syncID() {
_("SyncEngine.syncID corresponds to preference");
- let syncTesting = new SyncTestingInfrastructure();
+ let syncTesting = new SyncTestingInfrastructure(server);
let engine = makeSteamEngine();
try {
// Ensure pristine environment
@@ -46,7 +48,7 @@ function test_syncID() {
function test_lastSync() {
_("SyncEngine.lastSync and SyncEngine.lastSyncLocal correspond to preferences");
- let syncTesting = new SyncTestingInfrastructure();
+ let syncTesting = new SyncTestingInfrastructure(server);
let engine = makeSteamEngine();
try {
// Ensure pristine environment
@@ -76,7 +78,7 @@ function test_lastSync() {
function test_toFetch() {
_("SyncEngine.toFetch corresponds to file on disk");
- let syncTesting = new SyncTestingInfrastructure();
+ let syncTesting = new SyncTestingInfrastructure(server);
const filename = "weave/toFetch/steam.json";
let engine = makeSteamEngine();
try {
@@ -106,7 +108,7 @@ function test_toFetch() {
function test_previousFailed() {
_("SyncEngine.previousFailed corresponds to file on disk");
- let syncTesting = new SyncTestingInfrastructure();
+ let syncTesting = new SyncTestingInfrastructure(server);
const filename = "weave/failed/steam.json";
let engine = makeSteamEngine();
try {
@@ -136,7 +138,7 @@ function test_previousFailed() {
function test_resetClient() {
_("SyncEngine.resetClient resets lastSync and toFetch");
- let syncTesting = new SyncTestingInfrastructure();
+ let syncTesting = new SyncTestingInfrastructure(server);
let engine = makeSteamEngine();
try {
// Ensure pristine environment
@@ -161,9 +163,6 @@ function test_resetClient() {
function test_wipeServer() {
_("SyncEngine.wipeServer deletes server data and resets the client.");
- let syncTesting = new SyncTestingInfrastructure();
- Service.serverURL = TEST_SERVER_URL;
- Service.clusterURL = TEST_CLUSTER_URL;
let engine = makeSteamEngine();
const PAYLOAD = 42;
@@ -171,6 +170,7 @@ function test_wipeServer() {
let server = httpd_setup({
"/1.1/foo/storage/steam": steamCollection.handler()
});
+ let syncTesting = new SyncTestingInfrastructure(server);
do_test_pending();
try {
@@ -191,6 +191,7 @@ function test_wipeServer() {
}
function run_test() {
+ server = httpd_setup({});
test_url_attributes();
test_syncID();
test_lastSync();
@@ -198,4 +199,6 @@ function run_test() {
test_previousFailed();
test_resetClient();
test_wipeServer();
+
+ server.stop(run_next_test);
}
diff --git a/services/sync/tests/unit/test_syncengine_sync.js b/services/sync/tests/unit/test_syncengine_sync.js
index c660f6ba0..6a6d047bf 100644
--- a/services/sync/tests/unit/test_syncengine_sync.js
+++ b/services/sync/tests/unit/test_syncengine_sync.js
@@ -22,8 +22,8 @@ function cleanAndGo(server) {
server.stop(run_next_test);
}
-function configureService(username, password) {
- Service.clusterURL = TEST_CLUSTER_URL;
+function configureService(server, username, password) {
+ Service.clusterURL = server.baseURI;
Service.identity.account = username || "foo";
Service.identity.basicPassword = password || "password";
@@ -40,15 +40,16 @@ function createServerAndConfigureClient() {
};
const USER = "foo";
- Service.serverURL = TEST_SERVER_URL;
- Service.clusterURL = TEST_CLUSTER_URL;
- Service.identity.username = USER;
-
let server = new SyncServer();
server.registerUser(USER, "password");
server.createContents(USER, contents);
server.start();
+ Service.serverURL = server.baseURI;
+ Service.clusterURL = server.baseURI;
+ Service.identity.username = USER;
+ Service._updateCachedURLs();
+
return [engine, server, USER];
}
@@ -75,11 +76,6 @@ function run_test() {
add_test(function test_syncStartup_emptyOrOutdatedGlobalsResetsSync() {
_("SyncEngine._syncStartup resets sync and wipes server data if there's no or an outdated global record");
- let syncTesting = new SyncTestingInfrastructure();
- Service.serverURL = TEST_SERVER_URL;
- Service.clusterURL = TEST_CLUSTER_URL;
- Service.identity.username = "foo";
-
// Some server side data that's going to be wiped
let collection = new ServerCollection();
collection.insert('flying',
@@ -93,6 +89,9 @@ add_test(function test_syncStartup_emptyOrOutdatedGlobalsResetsSync() {
"/1.1/foo/storage/rotary": collection.handler()
});
+ let syncTesting = new SyncTestingInfrastructure(server);
+ Service.identity.username = "foo";
+
let engine = makeRotaryEngine();
engine._store.items = {rekolok: "Rekonstruktionslokomotive"};
try {
@@ -129,15 +128,14 @@ add_test(function test_syncStartup_emptyOrOutdatedGlobalsResetsSync() {
add_test(function test_syncStartup_serverHasNewerVersion() {
_("SyncEngine._syncStartup ");
- let syncTesting = new SyncTestingInfrastructure();
- Service.serverURL = TEST_SERVER_URL;
- Service.clusterURL = TEST_CLUSTER_URL;
- Service.identity.username = "foo";
let global = new ServerWBO('global', {engines: {rotary: {version: 23456}}});
let server = httpd_setup({
"/1.1/foo/storage/meta/global": global.handler()
});
+ let syncTesting = new SyncTestingInfrastructure(server);
+ Service.identity.username = "foo";
+
let engine = makeRotaryEngine();
try {
@@ -160,11 +158,9 @@ add_test(function test_syncStartup_serverHasNewerVersion() {
add_test(function test_syncStartup_syncIDMismatchResetsClient() {
_("SyncEngine._syncStartup resets sync if syncIDs don't match");
- let syncTesting = new SyncTestingInfrastructure();
- Service.serverURL = TEST_SERVER_URL;
- Service.clusterURL = TEST_CLUSTER_URL;
- Service.identity.username = "foo";
let server = sync_httpd_setup({});
+ let syncTesting = new SyncTestingInfrastructure(server);
+ Service.identity.username = "foo";
// global record with a different syncID than our engine has
let engine = makeRotaryEngine();
@@ -198,16 +194,14 @@ add_test(function test_syncStartup_syncIDMismatchResetsClient() {
add_test(function test_processIncoming_emptyServer() {
_("SyncEngine._processIncoming working with an empty server backend");
- let syncTesting = new SyncTestingInfrastructure();
- Service.serverURL = TEST_SERVER_URL;
- Service.clusterURL = TEST_CLUSTER_URL;
- Service.identity.username = "foo";
let collection = new ServerCollection();
-
let server = sync_httpd_setup({
"/1.1/foo/storage/rotary": collection.handler()
});
+ let syncTesting = new SyncTestingInfrastructure(server);
+ Service.identity.username = "foo";
+
let engine = makeRotaryEngine();
try {
@@ -224,13 +218,6 @@ add_test(function test_processIncoming_emptyServer() {
add_test(function test_processIncoming_createFromServer() {
_("SyncEngine._processIncoming creates new records from server data");
- let syncTesting = new SyncTestingInfrastructure();
- Service.serverURL = TEST_SERVER_URL;
- Service.clusterURL = TEST_CLUSTER_URL;
- Service.identity.username = "foo";
-
- generateNewKeys(Service.collectionKeys);
-
// Some server records that will be downloaded
let collection = new ServerCollection();
collection.insert('flying',
@@ -251,6 +238,11 @@ add_test(function test_processIncoming_createFromServer() {
"/1.1/foo/storage/rotary/scotsman": collection.wbo("scotsman").handler()
});
+ let syncTesting = new SyncTestingInfrastructure(server);
+ Service.identity.username = "foo";
+
+ generateNewKeys(Service.collectionKeys);
+
let engine = makeRotaryEngine();
let meta_global = Service.recordManager.set(engine.metaURL,
new WBORecord(engine.metaURL));
@@ -287,10 +279,6 @@ add_test(function test_processIncoming_createFromServer() {
add_test(function test_processIncoming_reconcile() {
_("SyncEngine._processIncoming updates local records");
- let syncTesting = new SyncTestingInfrastructure();
- Service.serverURL = TEST_SERVER_URL;
- Service.clusterURL = TEST_CLUSTER_URL;
- Service.identity.username = "foo";
let collection = new ServerCollection();
// This server record is newer than the corresponding client one,
@@ -335,6 +323,9 @@ add_test(function test_processIncoming_reconcile() {
"/1.1/foo/storage/rotary": collection.handler()
});
+ let syncTesting = new SyncTestingInfrastructure(server);
+ Service.identity.username = "foo";
+
let engine = makeRotaryEngine();
engine._store.items = {newerserver: "New data, but not as new as server!",
olderidentical: "Older but identical",
@@ -410,8 +401,8 @@ add_test(function test_processIncoming_reconcile_local_deleted() {
let wbo = new ServerWBO("DUPE_INCOMING", record, now + 2);
server.insertWBO(user, "rotary", wbo);
- let record = encryptPayload({id: "DUPE_LOCAL", denomination: "local"});
- let wbo = new ServerWBO("DUPE_LOCAL", record, now - 1);
+ record = encryptPayload({id: "DUPE_LOCAL", denomination: "local"});
+ wbo = new ServerWBO("DUPE_LOCAL", record, now - 1);
server.insertWBO(user, "rotary", wbo);
engine._store.create({id: "DUPE_LOCAL", denomination: "local"});
@@ -484,7 +475,7 @@ add_test(function test_processIncoming_reconcile_locally_deleted_dupe_new() {
do_check_empty(engine._store.items);
let collection = server.getCollection(user, "rotary");
do_check_eq(1, collection.count());
- let wbo = collection.wbo("DUPE_INCOMING");
+ wbo = collection.wbo("DUPE_INCOMING");
do_check_neq(null, wbo);
let payload = JSON.parse(JSON.parse(wbo.payload).ciphertext);
do_check_true(payload.deleted);
@@ -524,7 +515,7 @@ add_test(function test_processIncoming_reconcile_locally_deleted_dupe_old() {
let collection = server.getCollection(user, "rotary");
do_check_eq(1, collection.count());
- let wbo = collection.wbo("DUPE_INCOMING");
+ wbo = collection.wbo("DUPE_INCOMING");
let payload = JSON.parse(JSON.parse(wbo.payload).ciphertext);
do_check_eq("incoming", payload.denomination);
@@ -560,7 +551,7 @@ add_test(function test_processIncoming_reconcile_changed_dupe() {
// have its payload set to what was in the local record.
let collection = server.getCollection(user, "rotary");
do_check_eq(1, collection.count());
- let wbo = collection.wbo("DUPE_INCOMING");
+ wbo = collection.wbo("DUPE_INCOMING");
do_check_neq(undefined, wbo);
let payload = JSON.parse(JSON.parse(wbo.payload).ciphertext);
do_check_eq("local", payload.denomination);
@@ -598,7 +589,7 @@ add_test(function test_processIncoming_reconcile_changed_dupe_new() {
// have its payload retained.
let collection = server.getCollection(user, "rotary");
do_check_eq(1, collection.count());
- let wbo = collection.wbo("DUPE_INCOMING");
+ wbo = collection.wbo("DUPE_INCOMING");
do_check_neq(undefined, wbo);
let payload = JSON.parse(JSON.parse(wbo.payload).ciphertext);
do_check_eq("incoming", payload.denomination);
@@ -608,11 +599,8 @@ add_test(function test_processIncoming_reconcile_changed_dupe_new() {
add_test(function test_processIncoming_mobile_batchSize() {
_("SyncEngine._processIncoming doesn't fetch everything at once on mobile clients");
- let syncTesting = new SyncTestingInfrastructure();
- Service.serverURL = TEST_SERVER_URL;
- Service.clusterURL = TEST_CLUSTER_URL;
- Service.identity.username = "foo";
Svc.Prefs.set("client.type", "mobile");
+ Service.identity.username = "foo";
// A collection that logs each GET
let collection = new ServerCollection();
@@ -637,6 +625,8 @@ add_test(function test_processIncoming_mobile_batchSize() {
"/1.1/foo/storage/rotary": collection.handler()
});
+ let syncTesting = new SyncTestingInfrastructure(server);
+
let engine = makeRotaryEngine();
let meta_global = Service.recordManager.set(engine.metaURL,
new WBORecord(engine.metaURL));
@@ -679,9 +669,6 @@ add_test(function test_processIncoming_mobile_batchSize() {
add_test(function test_processIncoming_store_toFetch() {
_("If processIncoming fails in the middle of a batch on mobile, state is saved in toFetch and lastSync.");
- let syncTesting = new SyncTestingInfrastructure();
- Service.serverURL = TEST_SERVER_URL;
- Service.clusterURL = TEST_CLUSTER_URL;
Service.identity.username = "foo";
Svc.Prefs.set("client.type", "mobile");
@@ -709,14 +696,16 @@ add_test(function test_processIncoming_store_toFetch() {
let engine = makeRotaryEngine();
engine.enabled = true;
- let meta_global = Service.recordManager.set(engine.metaURL,
- new WBORecord(engine.metaURL));
- meta_global.payload.engines = {rotary: {version: engine.version,
- syncID: engine.syncID}};
let server = sync_httpd_setup({
"/1.1/foo/storage/rotary": collection.handler()
});
+ let syncTesting = new SyncTestingInfrastructure(server);
+
+ let meta_global = Service.recordManager.set(engine.metaURL,
+ new WBORecord(engine.metaURL));
+ meta_global.payload.engines = {rotary: {version: engine.version,
+ syncID: engine.syncID}};
try {
// Confirm initial environment
@@ -748,9 +737,6 @@ add_test(function test_processIncoming_store_toFetch() {
add_test(function test_processIncoming_resume_toFetch() {
_("toFetch and previousFailed items left over from previous syncs are fetched on the next sync, along with new items.");
- let syncTesting = new SyncTestingInfrastructure();
- Service.serverURL = TEST_SERVER_URL;
- Service.clusterURL = TEST_CLUSTER_URL;
Service.identity.username = "foo";
const LASTSYNC = Date.now() / 1000;
@@ -784,14 +770,16 @@ add_test(function test_processIncoming_resume_toFetch() {
engine.toFetch = ["flying", "scotsman"];
engine.previousFailed = ["failed0", "failed1", "failed2"];
- let meta_global = Service.recordManager.set(engine.metaURL,
- new WBORecord(engine.metaURL));
- meta_global.payload.engines = {rotary: {version: engine.version,
- syncID: engine.syncID}};
let server = sync_httpd_setup({
"/1.1/foo/storage/rotary": collection.handler()
});
+ let syncTesting = new SyncTestingInfrastructure(server);
+
+ let meta_global = Service.recordManager.set(engine.metaURL,
+ new WBORecord(engine.metaURL));
+ meta_global.payload.engines = {rotary: {version: engine.version,
+ syncID: engine.syncID}};
try {
// Confirm initial environment
@@ -818,9 +806,6 @@ add_test(function test_processIncoming_resume_toFetch() {
add_test(function test_processIncoming_applyIncomingBatchSize_smaller() {
_("Ensure that a number of incoming items less than applyIncomingBatchSize is still applied.");
- let syncTesting = new SyncTestingInfrastructure();
- Service.serverURL = TEST_SERVER_URL;
- Service.clusterURL = TEST_CLUSTER_URL;
Service.identity.username = "foo";
// Engine that doesn't like the first and last record it's given.
@@ -843,14 +828,16 @@ add_test(function test_processIncoming_applyIncomingBatchSize_smaller() {
collection.insert(id, payload);
}
- let meta_global = Service.recordManager.set(engine.metaURL,
- new WBORecord(engine.metaURL));
- meta_global.payload.engines = {rotary: {version: engine.version,
- syncID: engine.syncID}};
let server = sync_httpd_setup({
"/1.1/foo/storage/rotary": collection.handler()
});
+ let syncTesting = new SyncTestingInfrastructure(server);
+
+ let meta_global = Service.recordManager.set(engine.metaURL,
+ new WBORecord(engine.metaURL));
+ meta_global.payload.engines = {rotary: {version: engine.version,
+ syncID: engine.syncID}};
try {
// Confirm initial environment
@@ -874,8 +861,6 @@ add_test(function test_processIncoming_applyIncomingBatchSize_smaller() {
add_test(function test_processIncoming_applyIncomingBatchSize_multiple() {
_("Ensure that incoming items are applied according to applyIncomingBatchSize.");
- let syncTesting = new SyncTestingInfrastructure();
- Service.clusterURL = TEST_CLUSTER_URL;
Service.identity.username = "foo";
const APPLY_BATCH_SIZE = 10;
@@ -899,14 +884,16 @@ add_test(function test_processIncoming_applyIncomingBatchSize_multiple() {
collection.insert(id, payload);
}
- let meta_global = Service.recordManager.set(engine.metaURL,
- new WBORecord(engine.metaURL));
- meta_global.payload.engines = {rotary: {version: engine.version,
- syncID: engine.syncID}};
let server = sync_httpd_setup({
"/1.1/foo/storage/rotary": collection.handler()
});
+ let syncTesting = new SyncTestingInfrastructure(server);
+
+ let meta_global = Service.recordManager.set(engine.metaURL,
+ new WBORecord(engine.metaURL));
+ meta_global.payload.engines = {rotary: {version: engine.version,
+ syncID: engine.syncID}};
try {
// Confirm initial environment
@@ -927,9 +914,6 @@ add_test(function test_processIncoming_applyIncomingBatchSize_multiple() {
add_test(function test_processIncoming_notify_count() {
_("Ensure that failed records are reported only once.");
- let syncTesting = new SyncTestingInfrastructure();
- Service.serverURL = TEST_SERVER_URL;
- Service.clusterURL = TEST_CLUSTER_URL;
Service.identity.username = "foo";
const APPLY_BATCH_SIZE = 5;
@@ -952,14 +936,16 @@ add_test(function test_processIncoming_notify_count() {
collection.insert(id, payload);
}
- let meta_global = Service.recordManager.set(engine.metaURL,
- new WBORecord(engine.metaURL));
- meta_global.payload.engines = {rotary: {version: engine.version,
- syncID: engine.syncID}};
let server = sync_httpd_setup({
"/1.1/foo/storage/rotary": collection.handler()
});
+ let syncTesting = new SyncTestingInfrastructure(server);
+
+ let meta_global = Service.recordManager.set(engine.metaURL,
+ new WBORecord(engine.metaURL));
+ meta_global.payload.engines = {rotary: {version: engine.version,
+ syncID: engine.syncID}};
try {
// Confirm initial environment.
do_check_eq(engine.lastSync, 0);
@@ -1017,9 +1003,6 @@ add_test(function test_processIncoming_notify_count() {
add_test(function test_processIncoming_previousFailed() {
_("Ensure that failed records are retried.");
- let syncTesting = new SyncTestingInfrastructure();
- Service.serverURL = TEST_SERVER_URL;
- Service.clusterURL = TEST_CLUSTER_URL;
Service.identity.username = "foo";
Svc.Prefs.set("client.type", "mobile");
@@ -1043,14 +1026,16 @@ add_test(function test_processIncoming_previousFailed() {
collection.insert(id, payload);
}
- let meta_global = Service.recordManager.set(engine.metaURL,
- new WBORecord(engine.metaURL));
- meta_global.payload.engines = {rotary: {version: engine.version,
- syncID: engine.syncID}};
let server = sync_httpd_setup({
"/1.1/foo/storage/rotary": collection.handler()
});
+ let syncTesting = new SyncTestingInfrastructure(server);
+
+ let meta_global = Service.recordManager.set(engine.metaURL,
+ new WBORecord(engine.metaURL));
+ meta_global.payload.engines = {rotary: {version: engine.version,
+ syncID: engine.syncID}};
try {
// Confirm initial environment.
do_check_eq(engine.lastSync, 0);
@@ -1104,9 +1089,6 @@ add_test(function test_processIncoming_previousFailed() {
add_test(function test_processIncoming_failed_records() {
_("Ensure that failed records from _reconcile and applyIncomingBatch are refetched.");
- let syncTesting = new SyncTestingInfrastructure();
- Service.serverURL = TEST_SERVER_URL;
- Service.clusterURL = TEST_CLUSTER_URL;
Service.identity.username = "foo";
// Let's create three and a bit batches worth of server side records.
@@ -1149,11 +1131,6 @@ add_test(function test_processIncoming_failed_records() {
return this._applyIncoming.apply(this, arguments);
};
- let meta_global = Service.recordManager.set(engine.metaURL,
- new WBORecord(engine.metaURL));
- meta_global.payload.engines = {rotary: {version: engine.version,
- syncID: engine.syncID}};
-
// Keep track of requests made of a collection.
let count = 0;
let uris = [];
@@ -1169,6 +1146,13 @@ add_test(function test_processIncoming_failed_records() {
"/1.1/foo/storage/rotary": recording_handler(collection)
});
+ let syncTesting = new SyncTestingInfrastructure(server);
+
+ let meta_global = Service.recordManager.set(engine.metaURL,
+ new WBORecord(engine.metaURL));
+ meta_global.payload.engines = {rotary: {version: engine.version,
+ syncID: engine.syncID}};
+
try {
// Confirm initial environment
@@ -1240,9 +1224,6 @@ add_test(function test_processIncoming_failed_records() {
add_test(function test_processIncoming_decrypt_failed() {
_("Ensure that records failing to decrypt are either replaced or refetched.");
- let syncTesting = new SyncTestingInfrastructure();
- Service.serverURL = TEST_SERVER_URL;
- Service.clusterURL = TEST_CLUSTER_URL;
Service.identity.username = "foo";
// Some good and some bogus records. One doesn't contain valid JSON,
@@ -1274,14 +1255,16 @@ add_test(function test_processIncoming_decrypt_failed() {
engine._store.items = {nojson: "Valid JSON",
nodecrypt: "Valid ciphertext"};
- let meta_global = Service.recordManager.set(engine.metaURL,
- new WBORecord(engine.metaURL));
- meta_global.payload.engines = {rotary: {version: engine.version,
- syncID: engine.syncID}};
let server = sync_httpd_setup({
"/1.1/foo/storage/rotary": collection.handler()
});
+ let syncTesting = new SyncTestingInfrastructure(server);
+
+ let meta_global = Service.recordManager.set(engine.metaURL,
+ new WBORecord(engine.metaURL));
+ meta_global.payload.engines = {rotary: {version: engine.version,
+ syncID: engine.syncID}};
try {
// Confirm initial state
@@ -1319,9 +1302,6 @@ add_test(function test_processIncoming_decrypt_failed() {
add_test(function test_uploadOutgoing_toEmptyServer() {
_("SyncEngine._uploadOutgoing uploads new records to server");
- let syncTesting = new SyncTestingInfrastructure();
- Service.serverURL = TEST_SERVER_URL;
- Service.clusterURL = TEST_CLUSTER_URL;
Service.identity.username = "foo";
let collection = new ServerCollection();
collection._wbos.flying = new ServerWBO('flying');
@@ -1332,6 +1312,8 @@ add_test(function test_uploadOutgoing_toEmptyServer() {
"/1.1/foo/storage/rotary/flying": collection.wbo("flying").handler(),
"/1.1/foo/storage/rotary/scotsman": collection.wbo("scotsman").handler()
});
+
+ let syncTesting = new SyncTestingInfrastructure(server);
generateNewKeys(Service.collectionKeys);
let engine = makeRotaryEngine();
@@ -1379,9 +1361,6 @@ add_test(function test_uploadOutgoing_toEmptyServer() {
add_test(function test_uploadOutgoing_failed() {
_("SyncEngine._uploadOutgoing doesn't clear the tracker of objects that failed to upload.");
- let syncTesting = new SyncTestingInfrastructure();
- Service.serverURL = TEST_SERVER_URL;
- Service.clusterURL = TEST_CLUSTER_URL;
Service.identity.username = "foo";
let collection = new ServerCollection();
// We only define the "flying" WBO on the server, not the "scotsman"
@@ -1392,6 +1371,8 @@ add_test(function test_uploadOutgoing_failed() {
"/1.1/foo/storage/rotary": collection.handler()
});
+ let syncTesting = new SyncTestingInfrastructure(server);
+
let engine = makeRotaryEngine();
engine.lastSync = 123; // needs to be non-zero so that tracker is queried
engine._store.items = {flying: "LNER Class A3 4472",
@@ -1443,9 +1424,6 @@ add_test(function test_uploadOutgoing_failed() {
add_test(function test_uploadOutgoing_MAX_UPLOAD_RECORDS() {
_("SyncEngine._uploadOutgoing uploads in batches of MAX_UPLOAD_RECORDS");
- let syncTesting = new SyncTestingInfrastructure();
- Service.serverURL = TEST_SERVER_URL;
- Service.clusterURL = TEST_CLUSTER_URL;
Service.identity.username = "foo";
let collection = new ServerCollection();
@@ -1476,6 +1454,8 @@ add_test(function test_uploadOutgoing_MAX_UPLOAD_RECORDS() {
"/1.1/foo/storage/rotary": collection.handler()
});
+ let syncTesting = new SyncTestingInfrastructure(server);
+
try {
// Confirm initial environment.
@@ -1501,7 +1481,9 @@ add_test(function test_uploadOutgoing_MAX_UPLOAD_RECORDS() {
add_test(function test_syncFinish_noDelete() {
_("SyncEngine._syncFinish resets tracker's score");
- let syncTesting = new SyncTestingInfrastructure();
+ let server = httpd_setup({});
+
+ let syncTesting = new SyncTestingInfrastructure(server);
let engine = makeRotaryEngine();
engine._delete = {}; // Nothing to delete
engine._tracker.score = 100;
@@ -1509,16 +1491,13 @@ add_test(function test_syncFinish_noDelete() {
// _syncFinish() will reset the engine's score.
engine._syncFinish();
do_check_eq(engine.score, 0);
- run_next_test();
+ server.stop(run_next_test);
});
add_test(function test_syncFinish_deleteByIds() {
_("SyncEngine._syncFinish deletes server records slated for deletion (list of record IDs).");
- let syncTesting = new SyncTestingInfrastructure();
- Service.serverURL = TEST_SERVER_URL;
- Service.clusterURL = TEST_CLUSTER_URL;
Service.identity.username = "foo";
let collection = new ServerCollection();
collection._wbos.flying = new ServerWBO(
@@ -1534,6 +1513,7 @@ add_test(function test_syncFinish_deleteByIds() {
let server = httpd_setup({
"/1.1/foo/storage/rotary": collection.handler()
});
+ let syncTesting = new SyncTestingInfrastructure(server);
let engine = makeRotaryEngine();
try {
@@ -1558,9 +1538,6 @@ add_test(function test_syncFinish_deleteByIds() {
add_test(function test_syncFinish_deleteLotsInBatches() {
_("SyncEngine._syncFinish deletes server records in batches of 100 (list of record IDs).");
- let syncTesting = new SyncTestingInfrastructure();
- Service.serverURL = TEST_SERVER_URL;
- Service.clusterURL = TEST_CLUSTER_URL;
Service.identity.username = "foo";
let collection = new ServerCollection();
@@ -1587,6 +1564,8 @@ add_test(function test_syncFinish_deleteLotsInBatches() {
"/1.1/foo/storage/rotary": collection.handler()
});
+ let syncTesting = new SyncTestingInfrastructure(server);
+
let engine = makeRotaryEngine();
try {
@@ -1630,15 +1609,13 @@ add_test(function test_syncFinish_deleteLotsInBatches() {
add_test(function test_sync_partialUpload() {
_("SyncEngine.sync() keeps changedIDs that couldn't be uploaded.");
- let syncTesting = new SyncTestingInfrastructure();
- Service.serverURL = TEST_SERVER_URL;
- Service.clusterURL = TEST_CLUSTER_URL;
Service.identity.username = "foo";
let collection = new ServerCollection();
let server = sync_httpd_setup({
"/1.1/foo/storage/rotary": collection.handler()
});
+ let syncTesting = new SyncTestingInfrastructure(server);
generateNewKeys(Service.collectionKeys);
let engine = makeRotaryEngine();
@@ -1705,9 +1682,6 @@ add_test(function test_sync_partialUpload() {
add_test(function test_canDecrypt_noCryptoKeys() {
_("SyncEngine.canDecrypt returns false if the engine fails to decrypt items on the server, e.g. due to a missing crypto key collection.");
- let syncTesting = new SyncTestingInfrastructure();
- Service.serverURL = TEST_SERVER_URL;
- Service.clusterURL = TEST_CLUSTER_URL;
Service.identity.username = "foo";
// Wipe collection keys so we can test the desired scenario.
@@ -1722,6 +1696,7 @@ add_test(function test_canDecrypt_noCryptoKeys() {
"/1.1/foo/storage/rotary": collection.handler()
});
+ let syncTesting = new SyncTestingInfrastructure(server);
let engine = makeRotaryEngine();
try {
@@ -1734,9 +1709,6 @@ add_test(function test_canDecrypt_noCryptoKeys() {
add_test(function test_canDecrypt_true() {
_("SyncEngine.canDecrypt returns true if the engine can decrypt the items on the server.");
- let syncTesting = new SyncTestingInfrastructure();
- Service.serverURL = TEST_SERVER_URL;
- Service.clusterURL = TEST_CLUSTER_URL;
Service.identity.username = "foo";
generateNewKeys(Service.collectionKeys);
@@ -1750,6 +1722,7 @@ add_test(function test_canDecrypt_true() {
"/1.1/foo/storage/rotary": collection.handler()
});
+ let syncTesting = new SyncTestingInfrastructure(server);
let engine = makeRotaryEngine();
try {
@@ -1762,9 +1735,6 @@ add_test(function test_canDecrypt_true() {
});
add_test(function test_syncapplied_observer() {
- let syncTesting = new SyncTestingInfrastructure();
- Service.serverURL = TEST_SERVER_URL;
- Service.clusterURL = TEST_CLUSTER_URL;
Service.identity.username = "foo";
const NUMBER_OF_RECORDS = 10;
@@ -1779,13 +1749,16 @@ add_test(function test_syncapplied_observer() {
collection.insert(id, payload);
}
+ let server = httpd_setup({
+ "/1.1/foo/storage/rotary": collection.handler()
+ });
+
+ let syncTesting = new SyncTestingInfrastructure(server);
+
let meta_global = Service.recordManager.set(engine.metaURL,
new WBORecord(engine.metaURL));
meta_global.payload.engines = {rotary: {version: engine.version,
syncID: engine.syncID}};
- let server = httpd_setup({
- "/1.1/foo/storage/rotary": collection.handler()
- });
let numApplyCalls = 0;
let engine_name;
diff --git a/services/sync/tests/unit/test_syncscheduler.js b/services/sync/tests/unit/test_syncscheduler.js
index 90d1b82d1..8136aadbe 100644
--- a/services/sync/tests/unit/test_syncscheduler.js
+++ b/services/sync/tests/unit/test_syncscheduler.js
@@ -52,32 +52,45 @@ function sync_httpd_setup() {
});
}
-function setUp() {
- setBasicCredentials("johndoe", "ilovejane", "abcdeabcdeabcdeabcdeabcdea");
- Service.clusterURL = TEST_CLUSTER_URL;
-
- generateNewKeys(Service.collectionKeys);
- let serverKeys = Service.collectionKeys.asWBO("crypto", "keys");
- serverKeys.encrypt(Service.identity.syncKeyBundle);
- return serverKeys.upload(Service.resource(Service.cryptoKeysURL)).success;
+function setUp(server) {
+ let deferred = Promise.defer();
+ configureIdentity({username: "johndoe"}).then(() => {
+ Service.clusterURL = server.baseURI + "/";
+
+ generateNewKeys(Service.collectionKeys);
+ let serverKeys = Service.collectionKeys.asWBO("crypto", "keys");
+ serverKeys.encrypt(Service.identity.syncKeyBundle);
+ let result = serverKeys.upload(Service.resource(Service.cryptoKeysURL)).success;
+ deferred.resolve(result);
+ });
+ return deferred.promise;
}
function cleanUpAndGo(server) {
+ let deferred = Promise.defer();
Utils.nextTick(function () {
Service.startOver();
if (server) {
- server.stop(run_next_test);
+ server.stop(deferred.resolve);
} else {
- run_next_test();
+ deferred.resolve();
}
});
+ return deferred.promise;
}
function run_test() {
initTestLogging("Trace");
- Log4Moz.repository.getLogger("Sync.Service").level = Log4Moz.Level.Trace;
- Log4Moz.repository.getLogger("Sync.scheduler").level = Log4Moz.Level.Trace;
+ Log.repository.getLogger("Sync.Service").level = Log.Level.Trace;
+ Log.repository.getLogger("Sync.scheduler").level = Log.Level.Trace;
+
+ // The scheduler checks Weave.fxaEnabled to determine whether to use
+ // FxA defaults or legacy defaults. As .fxaEnabled checks the username, we
+ // set a username here then reset the default to ensure they are used.
+ ensureLegacyIdentityManager();
+ setBasicCredentials("johndoe");
+ scheduler.setDefaults();
run_next_test();
}
@@ -119,7 +132,7 @@ add_test(function test_prefAttributes() {
_("Intervals correspond to default preferences.");
do_check_eq(scheduler.singleDeviceInterval,
- Svc.Prefs.get("scheduler.singleDeviceInterval") * 1000);
+ Svc.Prefs.get("scheduler.sync11.singleDeviceInterval") * 1000);
do_check_eq(scheduler.idleInterval,
Svc.Prefs.get("scheduler.idleInterval") * 1000);
do_check_eq(scheduler.activeInterval,
@@ -128,7 +141,7 @@ add_test(function test_prefAttributes() {
Svc.Prefs.get("scheduler.immediateInterval") * 1000);
_("Custom values for prefs will take effect after a restart.");
- Svc.Prefs.set("scheduler.singleDeviceInterval", 42);
+ Svc.Prefs.set("scheduler.sync11.singleDeviceInterval", 42);
Svc.Prefs.set("scheduler.idleInterval", 23);
Svc.Prefs.set("scheduler.activeInterval", 18);
Svc.Prefs.set("scheduler.immediateInterval", 31415);
@@ -143,7 +156,7 @@ add_test(function test_prefAttributes() {
run_next_test();
});
-add_test(function test_updateClientMode() {
+add_identity_test(this, function test_updateClientMode() {
_("Test updateClientMode adjusts scheduling attributes based on # of clients appropriately");
do_check_eq(scheduler.syncThreshold, SINGLE_USER_THRESHOLD);
do_check_eq(scheduler.syncInterval, scheduler.singleDeviceInterval);
@@ -170,10 +183,10 @@ add_test(function test_updateClientMode() {
do_check_false(scheduler.numClients > 1);
do_check_false(scheduler.idle);
- cleanUpAndGo();
+ yield cleanUpAndGo();
});
-add_test(function test_masterpassword_locked_retry_interval() {
+add_identity_test(this, function test_masterpassword_locked_retry_interval() {
_("Test Status.login = MASTER_PASSWORD_LOCKED results in reschedule at MASTER_PASSWORD interval");
let loginFailed = false;
Svc.Obs.add("weave:service:login:error", function onLoginError() {
@@ -196,7 +209,7 @@ add_test(function test_masterpassword_locked_retry_interval() {
};
let server = sync_httpd_setup();
- setUp();
+ yield setUp(server);
Service.sync();
@@ -207,10 +220,10 @@ add_test(function test_masterpassword_locked_retry_interval() {
Service.verifyLogin = oldVerifyLogin;
SyncScheduler.prototype.scheduleAtInterval = oldScheduleAtInterval;
- cleanUpAndGo(server);
+ yield cleanUpAndGo(server);
});
-add_test(function test_calculateBackoff() {
+add_identity_test(this, function test_calculateBackoff() {
do_check_eq(Status.backoffInterval, 0);
// Test no interval larger than the maximum backoff is used if
@@ -229,23 +242,25 @@ add_test(function test_calculateBackoff() {
do_check_eq(backoffInterval, MAXIMUM_BACKOFF_INTERVAL + 10);
- cleanUpAndGo();
+ yield cleanUpAndGo();
});
-add_test(function test_scheduleNextSync_nowOrPast() {
+add_identity_test(this, function test_scheduleNextSync_nowOrPast() {
+ let deferred = Promise.defer();
Svc.Obs.add("weave:service:sync:finish", function onSyncFinish() {
Svc.Obs.remove("weave:service:sync:finish", onSyncFinish);
- cleanUpAndGo(server);
+ cleanUpAndGo(server).then(deferred.resolve);
});
let server = sync_httpd_setup();
- setUp();
+ yield setUp(server);
// We're late for a sync...
scheduler.scheduleNextSync(-1);
+ yield deferred.promise;
});
-add_test(function test_scheduleNextSync_future_noBackoff() {
+add_identity_test(this, function test_scheduleNextSync_future_noBackoff() {
_("scheduleNextSync() uses the current syncInterval if no interval is provided.");
// Test backoffInterval is 0 as expected.
do_check_eq(Status.backoffInterval, 0);
@@ -291,10 +306,10 @@ add_test(function test_scheduleNextSync_future_noBackoff() {
do_check_true(scheduler.nextSync <= Date.now() + 1);
do_check_eq(scheduler.syncTimer.delay, 1);
- cleanUpAndGo();
+ yield cleanUpAndGo();
});
-add_test(function test_scheduleNextSync_future_backoff() {
+add_identity_test(this, function test_scheduleNextSync_future_backoff() {
_("scheduleNextSync() will honour backoff in all scheduling requests.");
// Let's take a backoff interval that's bigger than the default sync interval.
const BACKOFF = 7337;
@@ -341,12 +356,12 @@ add_test(function test_scheduleNextSync_future_backoff() {
do_check_true(scheduler.nextSync <= Date.now() + Status.backoffInterval);
do_check_eq(scheduler.syncTimer.delay, Status.backoffInterval);
- cleanUpAndGo();
+ yield cleanUpAndGo();
});
-add_test(function test_handleSyncError() {
+add_identity_test(this, function test_handleSyncError() {
let server = sync_httpd_setup();
- setUp();
+ yield setUp(server);
// Force sync to fail.
Svc.Prefs.set("firstSync", "notReady");
@@ -399,12 +414,20 @@ add_test(function test_handleSyncError() {
do_check_true(Status.enforceBackoff);
scheduler.syncTimer.clear();
- cleanUpAndGo(server);
+ _("Arrange for a successful sync to reset the scheduler error count");
+ let deferred = Promise.defer();
+ Svc.Obs.add("weave:service:sync:finish", function onSyncFinish() {
+ Svc.Obs.remove("weave:service:sync:finish", onSyncFinish);
+ cleanUpAndGo(server).then(deferred.resolve);
+ });
+ Svc.Prefs.set("firstSync", "wipeRemote");
+ scheduler.scheduleNextSync(-1);
+ yield deferred.promise;
});
-add_test(function test_client_sync_finish_updateClientMode() {
+add_identity_test(this, function test_client_sync_finish_updateClientMode() {
let server = sync_httpd_setup();
- setUp();
+ yield setUp(server);
// Confirm defaults.
do_check_eq(scheduler.syncThreshold, SINGLE_USER_THRESHOLD);
@@ -433,24 +456,27 @@ add_test(function test_client_sync_finish_updateClientMode() {
do_check_false(scheduler.numClients > 1);
do_check_false(scheduler.idle);
- cleanUpAndGo(server);
+ yield cleanUpAndGo(server);
});
-add_test(function test_autoconnect_nextSync_past() {
+add_identity_test(this, function test_autoconnect_nextSync_past() {
+ let deferred = Promise.defer();
// nextSync will be 0 by default, so it's way in the past.
Svc.Obs.add("weave:service:sync:finish", function onSyncFinish() {
Svc.Obs.remove("weave:service:sync:finish", onSyncFinish);
- cleanUpAndGo(server);
+ cleanUpAndGo(server).then(deferred.resolve);
});
let server = sync_httpd_setup();
- setUp();
+ yield setUp(server);
scheduler.delayedAutoConnect(0);
+ yield deferred.promise;
});
-add_test(function test_autoconnect_nextSync_future() {
+add_identity_test(this, function test_autoconnect_nextSync_future() {
+ let deferred = Promise.defer();
let previousSync = Date.now() + scheduler.syncInterval / 2;
scheduler.nextSync = previousSync;
// nextSync rounds to the nearest second.
@@ -468,20 +494,23 @@ add_test(function test_autoconnect_nextSync_future() {
do_check_true(scheduler.syncTimer.delay >= expectedInterval);
Svc.Obs.remove("weave:service:login:start", onLoginStart);
- cleanUpAndGo();
+ cleanUpAndGo().then(deferred.resolve);
});
- setBasicCredentials("johndoe", "ilovejane", "abcdeabcdeabcdeabcdeabcdea");
+ yield configureIdentity({username: "johndoe"});
scheduler.delayedAutoConnect(0);
+ yield deferred.promise;
});
-add_test(function test_autoconnect_mp_locked() {
+// XXX - this test can't be run with the browserid identity as it relies
+// on the syncKey getter behaving in a certain way...
+add_task(function test_autoconnect_mp_locked() {
let server = sync_httpd_setup();
- setUp();
+ yield setUp(server);
// Pretend user did not unlock master password.
let origLocked = Utils.mpLocked;
- Utils.mpLocked = function() true;
+ Utils.mpLocked = () => true;
let origGetter = Service.identity.__lookupGetter__("syncKey");
let origSetter = Service.identity.__lookupSetter__("syncKey");
@@ -491,6 +520,7 @@ add_test(function test_autoconnect_mp_locked() {
throw "User canceled Master Password entry";
});
+ let deferred = Promise.defer();
// A locked master password will still trigger a sync, but then we'll hit
// MASTER_PASSWORD_LOCKED and hence MASTER_PASSWORD_LOCKED_RETRY_INTERVAL.
Svc.Obs.add("weave:service:login:error", function onLoginError() {
@@ -503,16 +533,17 @@ add_test(function test_autoconnect_mp_locked() {
Service.identity.__defineGetter__("syncKey", origGetter);
Service.identity.__defineSetter__("syncKey", origSetter);
- cleanUpAndGo(server);
+ cleanUpAndGo(server).then(deferred.resolve);
});
});
scheduler.delayedAutoConnect(0);
+ yield deferred.promise;
});
-add_test(function test_no_autoconnect_during_wizard() {
+add_identity_test(this, function test_no_autoconnect_during_wizard() {
let server = sync_httpd_setup();
- setUp();
+ yield setUp(server);
// Simulate the Sync setup wizard.
Svc.Prefs.set("firstSync", "notReady");
@@ -523,15 +554,17 @@ add_test(function test_no_autoconnect_during_wizard() {
}
Svc.Obs.add("weave:service:login:start", onLoginStart);
+ let deferred = Promise.defer();
waitForZeroTimer(function () {
Svc.Obs.remove("weave:service:login:start", onLoginStart);
- cleanUpAndGo(server);
+ cleanUpAndGo(server).then(deferred.resolve);
});
scheduler.delayedAutoConnect(0);
+ yield deferred.promise;
});
-add_test(function test_no_autoconnect_status_not_ok() {
+add_identity_test(this, function test_no_autoconnect_status_not_ok() {
let server = sync_httpd_setup();
// Ensure we don't actually try to sync (or log in for that matter).
@@ -540,37 +573,41 @@ add_test(function test_no_autoconnect_status_not_ok() {
}
Svc.Obs.add("weave:service:login:start", onLoginStart);
+ let deferred = Promise.defer();
waitForZeroTimer(function () {
Svc.Obs.remove("weave:service:login:start", onLoginStart);
do_check_eq(Status.service, CLIENT_NOT_CONFIGURED);
do_check_eq(Status.login, LOGIN_FAILED_NO_USERNAME);
- cleanUpAndGo(server);
+ cleanUpAndGo(server).then(deferred.resolve);
});
scheduler.delayedAutoConnect(0);
+ yield deferred.promise;
});
-add_test(function test_autoconnectDelay_pref() {
+add_identity_test(this, function test_autoconnectDelay_pref() {
+ let deferred = Promise.defer();
Svc.Obs.add("weave:service:sync:finish", function onSyncFinish() {
Svc.Obs.remove("weave:service:sync:finish", onSyncFinish);
- cleanUpAndGo(server);
+ cleanUpAndGo(server).then(deferred.resolve);
});
Svc.Prefs.set("autoconnectDelay", 1);
let server = sync_httpd_setup();
- setUp();
+ yield setUp(server);
Svc.Obs.notify("weave:service:ready");
// autoconnectDelay pref is multiplied by 1000.
do_check_eq(scheduler._autoTimer.delay, 1000);
do_check_eq(Status.service, STATUS_OK);
+ yield deferred.promise;
});
-add_test(function test_idle_adjustSyncInterval() {
+add_identity_test(this, function test_idle_adjustSyncInterval() {
// Confirm defaults.
do_check_eq(scheduler.idle, false);
@@ -587,10 +624,10 @@ add_test(function test_idle_adjustSyncInterval() {
do_check_eq(scheduler.idle, true);
do_check_eq(scheduler.syncInterval, scheduler.idleInterval);
- cleanUpAndGo();
+ yield cleanUpAndGo();
});
-add_test(function test_back_triggersSync() {
+add_identity_test(this, function test_back_triggersSync() {
// Confirm defaults.
do_check_false(scheduler.idle);
do_check_eq(Status.backoffInterval, 0);
@@ -600,18 +637,20 @@ add_test(function test_back_triggersSync() {
scheduler.observe(null, "idle", Svc.Prefs.get("scheduler.idleTime"));
do_check_true(scheduler.idle);
+ let deferred = Promise.defer();
// We don't actually expect the sync (or the login, for that matter) to
// succeed. We just want to ensure that it was attempted.
Svc.Obs.add("weave:service:login:error", function onLoginError() {
Svc.Obs.remove("weave:service:login:error", onLoginError);
- cleanUpAndGo();
+ cleanUpAndGo().then(deferred.resolve);
});
- // Send a 'back' event to trigger sync soonish.
- scheduler.observe(null, "back", Svc.Prefs.get("scheduler.idleTime"));
+ // Send an 'active' event to trigger sync soonish.
+ scheduler.observe(null, "active", Svc.Prefs.get("scheduler.idleTime"));
+ yield deferred.promise;
});
-add_test(function test_back_triggersSync_observesBackoff() {
+add_identity_test(this, function test_active_triggersSync_observesBackoff() {
// Confirm defaults.
do_check_false(scheduler.idle);
@@ -627,20 +666,22 @@ add_test(function test_back_triggersSync_observesBackoff() {
}
Svc.Obs.add("weave:service:login:start", onLoginStart);
+ let deferred = Promise.defer();
timer = Utils.namedTimer(function () {
Svc.Obs.remove("weave:service:login:start", onLoginStart);
do_check_true(scheduler.nextSync <= Date.now() + Status.backoffInterval);
do_check_eq(scheduler.syncTimer.delay, Status.backoffInterval);
- cleanUpAndGo();
+ cleanUpAndGo().then(deferred.resolve);
}, IDLE_OBSERVER_BACK_DELAY * 1.5, {}, "timer");
- // Send a 'back' event to try to trigger sync soonish.
- scheduler.observe(null, "back", Svc.Prefs.get("scheduler.idleTime"));
+ // Send an 'active' event to try to trigger sync soonish.
+ scheduler.observe(null, "active", Svc.Prefs.get("scheduler.idleTime"));
+ yield deferred.promise;
});
-add_test(function test_back_debouncing() {
+add_identity_test(this, function test_back_debouncing() {
_("Ensure spurious back-then-idle events, as observed on OS X, don't trigger a sync.");
// Confirm defaults.
@@ -657,31 +698,33 @@ add_test(function test_back_debouncing() {
Svc.Obs.add("weave:service:login:start", onLoginStart);
// Create spurious back-then-idle events as observed on OS X:
- scheduler.observe(null, "back", Svc.Prefs.get("scheduler.idleTime"));
+ scheduler.observe(null, "active", Svc.Prefs.get("scheduler.idleTime"));
scheduler.observe(null, "idle", Svc.Prefs.get("scheduler.idleTime"));
+ let deferred = Promise.defer();
timer = Utils.namedTimer(function () {
Svc.Obs.remove("weave:service:login:start", onLoginStart);
- cleanUpAndGo();
+ cleanUpAndGo().then(deferred.resolve);
}, IDLE_OBSERVER_BACK_DELAY * 1.5, {}, "timer");
+ yield deferred.promise;
});
-add_test(function test_no_sync_node() {
+add_identity_test(this, function test_no_sync_node() {
// Test when Status.sync == NO_SYNC_NODE_FOUND
// it is not overwritten on sync:finish
let server = sync_httpd_setup();
- setUp();
+ yield setUp(server);
- Service.serverURL = TEST_SERVER_URL;
+ Service.serverURL = server.baseURI + "/";
Service.sync();
do_check_eq(Status.sync, NO_SYNC_NODE_FOUND);
do_check_eq(scheduler.syncTimer.delay, NO_SYNC_NODE_INTERVAL);
- cleanUpAndGo(server);
+ yield cleanUpAndGo(server);
});
-add_test(function test_sync_failed_partial_500s() {
+add_identity_test(this, function test_sync_failed_partial_500s() {
_("Test a 5xx status calls handleSyncError.");
scheduler._syncErrors = MAX_ERROR_COUNT_BEFORE_BACKOFF;
let server = sync_httpd_setup();
@@ -692,7 +735,7 @@ add_test(function test_sync_failed_partial_500s() {
do_check_eq(Status.sync, SYNC_SUCCEEDED);
- do_check_true(setUp());
+ do_check_true(yield setUp(server));
Service.sync();
@@ -705,10 +748,10 @@ add_test(function test_sync_failed_partial_500s() {
do_check_true(scheduler.nextSync <= (Date.now() + maxInterval));
do_check_true(scheduler.syncTimer.delay <= maxInterval);
- cleanUpAndGo(server);
+ yield cleanUpAndGo(server);
});
-add_test(function test_sync_failed_partial_400s() {
+add_identity_test(this, function test_sync_failed_partial_400s() {
_("Test a non-5xx status doesn't call handleSyncError.");
scheduler._syncErrors = MAX_ERROR_COUNT_BEFORE_BACKOFF;
let server = sync_httpd_setup();
@@ -722,7 +765,7 @@ add_test(function test_sync_failed_partial_400s() {
do_check_eq(Status.sync, SYNC_SUCCEEDED);
- do_check_true(setUp());
+ do_check_true(yield setUp(server));
Service.sync();
@@ -735,12 +778,12 @@ add_test(function test_sync_failed_partial_400s() {
do_check_true(scheduler.nextSync <= (Date.now() + scheduler.activeInterval));
do_check_true(scheduler.syncTimer.delay <= scheduler.activeInterval);
- cleanUpAndGo(server);
+ yield cleanUpAndGo(server);
});
-add_test(function test_sync_X_Weave_Backoff() {
+add_identity_test(this, function test_sync_X_Weave_Backoff() {
let server = sync_httpd_setup();
- setUp();
+ yield setUp(server);
// Use an odd value on purpose so that it doesn't happen to coincide with one
// of the sync intervals.
@@ -790,12 +833,12 @@ add_test(function test_sync_X_Weave_Backoff() {
do_check_true(scheduler.nextSync >= Date.now() + minimumExpectedDelay);
do_check_true(scheduler.syncTimer.delay >= minimumExpectedDelay);
- cleanUpAndGo(server);
+ yield cleanUpAndGo(server);
});
-add_test(function test_sync_503_Retry_After() {
+add_identity_test(this, function test_sync_503_Retry_After() {
let server = sync_httpd_setup();
- setUp();
+ yield setUp(server);
// Use an odd value on purpose so that it doesn't happen to coincide with one
// of the sync intervals.
@@ -849,17 +892,18 @@ add_test(function test_sync_503_Retry_After() {
do_check_true(scheduler.nextSync >= Date.now() + minimumExpectedDelay);
do_check_true(scheduler.syncTimer.delay >= minimumExpectedDelay);
- cleanUpAndGo(server);
+ yield cleanUpAndGo(server);
});
-add_test(function test_loginError_recoverable_reschedules() {
+add_identity_test(this, function test_loginError_recoverable_reschedules() {
_("Verify that a recoverable login error schedules a new sync.");
- setBasicCredentials("johndoe", "ilovejane", "abcdeabcdeabcdeabcdeabcdea");
- Service.serverURL = TEST_SERVER_URL;
- Service.clusterURL = TEST_CLUSTER_URL;
+ yield configureIdentity({username: "johndoe"});
+ Service.serverURL = "http://localhost:1234/";
+ Service.clusterURL = Service.serverURL;
Service.persistLogin();
Status.resetSync(); // reset Status.login
+ let deferred = Promise.defer();
Svc.Obs.add("weave:service:login:error", function onLoginError() {
Svc.Obs.remove("weave:service:login:error", onLoginError);
Utils.nextTick(function aLittleBitAfterLoginError() {
@@ -872,7 +916,7 @@ add_test(function test_loginError_recoverable_reschedules() {
do_check_true(scheduler.syncTimer.delay <= scheduler.syncInterval);
Svc.Obs.remove("weave:service:sync:start", onSyncStart);
- cleanUpAndGo();
+ cleanUpAndGo().then(deferred.resolve);
});
});
@@ -892,20 +936,23 @@ add_test(function test_loginError_recoverable_reschedules() {
do_check_eq(Status.login, LOGIN_SUCCEEDED);
scheduler.scheduleNextSync(0);
+ yield deferred.promise;
});
-add_test(function test_loginError_fatal_clearsTriggers() {
+add_identity_test(this, function test_loginError_fatal_clearsTriggers() {
_("Verify that a fatal login error clears sync triggers.");
- setBasicCredentials("johndoe", "ilovejane", "abcdeabcdeabcdeabcdeabcdea");
- Service.serverURL = TEST_SERVER_URL;
- Service.clusterURL = TEST_CLUSTER_URL;
- Service.persistLogin();
- Status.resetSync(); // reset Status.login
+ yield configureIdentity({username: "johndoe"});
let server = httpd_setup({
"/1.1/johndoe/info/collections": httpd_handler(401, "Unauthorized")
});
+ Service.serverURL = server.baseURI + "/";
+ Service.clusterURL = Service.serverURL;
+ Service.persistLogin();
+ Status.resetSync(); // reset Status.login
+
+ let deferred = Promise.defer();
Svc.Obs.add("weave:service:login:error", function onLoginError() {
Svc.Obs.remove("weave:service:login:error", onLoginError);
Utils.nextTick(function aLittleBitAfterLoginError() {
@@ -914,7 +961,7 @@ add_test(function test_loginError_fatal_clearsTriggers() {
do_check_eq(scheduler.nextSync, 0);
do_check_eq(scheduler.syncTimer, null);
- cleanUpAndGo(server);
+ cleanUpAndGo(server).then(deferred.resolve);
});
});
@@ -925,9 +972,10 @@ add_test(function test_loginError_fatal_clearsTriggers() {
do_check_eq(Status.login, LOGIN_SUCCEEDED);
scheduler.scheduleNextSync(0);
+ yield deferred.promise;
});
-add_test(function test_proper_interval_on_only_failing() {
+add_identity_test(this, function test_proper_interval_on_only_failing() {
_("Ensure proper behavior when only failed records are applied.");
// If an engine reports that no records succeeded, we shouldn't decrease the
@@ -944,11 +992,13 @@ add_test(function test_proper_interval_on_only_failing() {
reconciled: 0
});
+ let deferred = Promise.defer();
Utils.nextTick(function() {
scheduler.adjustSyncInterval();
do_check_false(scheduler.hasIncomingItems);
do_check_eq(scheduler.syncInterval, scheduler.singleDeviceInterval);
- run_next_test();
+ deferred.resolve();
});
+ yield deferred.promise;
});
diff --git a/services/sync/tests/unit/test_syncstoragerequest.js b/services/sync/tests/unit/test_syncstoragerequest.js
index 544893268..7c5246bab 100644
--- a/services/sync/tests/unit/test_syncstoragerequest.js
+++ b/services/sync/tests/unit/test_syncstoragerequest.js
@@ -1,19 +1,19 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
-Cu.import("resource://services-common/log4moz.js");
+Cu.import("resource://gre/modules/Log.jsm");
Cu.import("resource://services-sync/constants.js");
Cu.import("resource://services-sync/rest.js");
Cu.import("resource://services-sync/service.js");
Cu.import("resource://services-sync/util.js");
Cu.import("resource://testing-common/services/sync/utils.js");
-const STORAGE_REQUEST_RESOURCE_URL = TEST_SERVER_URL + "resource";
-
function run_test() {
- Log4Moz.repository.getLogger("Sync.RESTRequest").level = Log4Moz.Level.Trace;
+ Log.repository.getLogger("Sync.RESTRequest").level = Log.Level.Trace;
initTestLogging();
+ ensureLegacyIdentityManager();
+
run_next_test();
}
@@ -25,7 +25,7 @@ add_test(function test_user_agent_desktop() {
" FxSync/" + WEAVE_VERSION + "." +
Services.appinfo.appBuildID + ".desktop";
- let request = new SyncStorageRequest(STORAGE_REQUEST_RESOURCE_URL);
+ let request = new SyncStorageRequest(server.baseURI + "/resource");
request.onComplete = function onComplete(error) {
do_check_eq(error, null);
do_check_eq(this.response.status, 200);
@@ -44,7 +44,7 @@ add_test(function test_user_agent_mobile() {
" FxSync/" + WEAVE_VERSION + "." +
Services.appinfo.appBuildID + ".mobile";
- let request = new SyncStorageRequest(STORAGE_REQUEST_RESOURCE_URL);
+ let request = new SyncStorageRequest(server.baseURI + "/resource");
request.get(function (error) {
do_check_eq(error, null);
do_check_eq(this.response.status, 200);
@@ -60,7 +60,7 @@ add_test(function test_auth() {
setBasicCredentials("johndoe", "ilovejane", "XXXXXXXXX");
- let request = Service.getStorageRequest(STORAGE_REQUEST_RESOURCE_URL);
+ let request = Service.getStorageRequest(server.baseURI + "/resource");
request.get(function (error) {
do_check_eq(error, null);
do_check_eq(this.response.status, 200);
@@ -84,7 +84,7 @@ add_test(function test_weave_timestamp() {
let server = httpd_setup({"/resource": handler});
do_check_eq(SyncStorageRequest.serverTime, undefined);
- let request = new SyncStorageRequest(STORAGE_REQUEST_RESOURCE_URL);
+ let request = new SyncStorageRequest(server.baseURI + "/resource");
request.get(function (error) {
do_check_eq(error, null);
do_check_eq(this.response.status, 200);
@@ -110,7 +110,7 @@ add_test(function test_weave_backoff() {
backoffInterval = subject;
});
- let request = new SyncStorageRequest(STORAGE_REQUEST_RESOURCE_URL);
+ let request = new SyncStorageRequest(server.baseURI + "/resource");
request.get(function (error) {
do_check_eq(error, null);
do_check_eq(this.response.status, 200);
@@ -135,7 +135,7 @@ add_test(function test_weave_quota_notice() {
quotaValue = subject;
});
- let request = new SyncStorageRequest(STORAGE_REQUEST_RESOURCE_URL);
+ let request = new SyncStorageRequest(server.baseURI + "/resource");
request.get(function (error) {
do_check_eq(error, null);
do_check_eq(this.response.status, 200);
@@ -160,7 +160,7 @@ add_test(function test_weave_quota_error() {
}
Svc.Obs.add("weave:service:quota:remaining", onQuota);
- let request = new SyncStorageRequest(STORAGE_REQUEST_RESOURCE_URL);
+ let request = new SyncStorageRequest(server.baseURI + "/resource");
request.get(function (error) {
do_check_eq(error, null);
do_check_eq(this.response.status, 400);
@@ -179,7 +179,7 @@ add_test(function test_abort() {
}
let server = httpd_setup({"/resource": handler});
- let request = new SyncStorageRequest(STORAGE_REQUEST_RESOURCE_URL);
+ let request = new SyncStorageRequest(server.baseURI + "/resource");
// Aborting a request that hasn't been sent yet is pointless and will throw.
do_check_throws(function () {
diff --git a/services/sync/tests/unit/test_tab_engine.js b/services/sync/tests/unit/test_tab_engine.js
index c05b8ed54..db4b20a70 100644
--- a/services/sync/tests/unit/test_tab_engine.js
+++ b/services/sync/tests/unit/test_tab_engine.js
@@ -2,50 +2,134 @@
http://creativecommons.org/publicdomain/zero/1.0/ */
Cu.import("resource://services-sync/engines/tabs.js");
+Cu.import("resource://services-sync/record.js");
Cu.import("resource://services-sync/service.js");
Cu.import("resource://services-sync/util.js");
+Cu.import("resource://testing-common/services/sync/utils.js");
-function fakeSessionSvc() {
- let tabs = [];
- for(let i = 0; i < arguments.length; i++) {
- tabs.push({
- index: 1,
- entries: [{
- url: arguments[i],
- title: "title"
- }],
- attributes: {
- image: "image"
- },
- extData: {
- weaveLastUsed: 1
- }
- });
- }
- let obj = {windows: [{tabs: tabs}]};
-
- // delete the getter, or the previously created fake Session
- delete Svc.Session;
- Svc.Session = {
- getBrowserState: function() JSON.stringify(obj)
- };
+function getMocks() {
+ let engine = new TabEngine(Service);
+ let store = engine._store;
+ store.getTabState = mockGetTabState;
+ store.shouldSkipWindow = mockShouldSkipWindow;
+ return [engine, store];
}
function run_test() {
+ run_next_test();
+}
- _("test locallyOpenTabMatchesURL");
- let engine = new TabEngine(Service);
+add_test(function test_getOpenURLs() {
+ _("Test getOpenURLs.");
+ let [engine, store] = getMocks();
- // 3 tabs
- fakeSessionSvc("http://bar.com", "http://foo.com", "http://foobar.com");
+ let urls = ["http://bar.com", "http://foo.com", "http://foobar.com"];
+ function threeURLs() {
+ return urls.pop();
+ }
+ store.getWindowEnumerator = mockGetWindowEnumerator.bind(this, threeURLs, 1, 3);
let matches;
_(" test matching works (true)");
- matches = engine.locallyOpenTabMatchesURL("http://foo.com");
- do_check_true(matches);
+ let openurlsset = engine.getOpenURLs();
+ matches = openurlsset.has("http://foo.com");
+ ok(matches);
_(" test matching works (false)");
- matches = engine.locallyOpenTabMatchesURL("http://barfoo.com");
- do_check_false(matches);
-}
+ matches = openurlsset.has("http://barfoo.com");
+ ok(!matches);
+
+ run_next_test();
+});
+
+add_test(function test_tab_engine_skips_incoming_local_record() {
+ _("Ensure incoming records that match local client ID are never applied.");
+ let [engine, store] = getMocks();
+ let localID = engine.service.clientsEngine.localID;
+ let apply = store.applyIncoming;
+ let applied = [];
+
+ store.applyIncoming = function (record) {
+ notEqual(record.id, localID, "Only apply tab records from remote clients");
+ applied.push(record);
+ apply.call(store, record);
+ }
+
+ let collection = new ServerCollection();
+
+ _("Creating remote tab record with local client ID");
+ let localRecord = encryptPayload({id: localID, clientName: "local"});
+ collection.insert(localID, localRecord);
+
+ _("Creating remote tab record with a different client ID");
+ let remoteID = "different";
+ let remoteRecord = encryptPayload({id: remoteID, clientName: "not local"});
+ collection.insert(remoteID, remoteRecord);
+
+ _("Setting up Sync server");
+ let server = sync_httpd_setup({
+ "/1.1/foo/storage/tabs": collection.handler()
+ });
+
+ let syncTesting = new SyncTestingInfrastructure(server);
+ Service.identity.username = "foo";
+
+ let meta_global = Service.recordManager.set(engine.metaURL,
+ new WBORecord(engine.metaURL));
+ meta_global.payload.engines = {tabs: {version: engine.version,
+ syncID: engine.syncID}};
+
+ generateNewKeys(Service.collectionKeys);
+
+ let syncFinish = engine._syncFinish;
+ engine._syncFinish = function () {
+ equal(applied.length, 1, "Remote client record was applied");
+ equal(applied[0].id, remoteID, "Remote client ID matches");
+
+ syncFinish.call(engine);
+ run_next_test();
+ }
+
+ _("Start sync");
+ engine._sync();
+});
+
+add_test(function test_reconcile() {
+ let [engine, store] = getMocks();
+
+ _("Setup engine for reconciling");
+ engine._syncStartup();
+
+ _("Create an incoming remote record");
+ let remoteRecord = {id: "remote id",
+ cleartext: "stuff and things!",
+ modified: 1000};
+
+ ok(engine._reconcile(remoteRecord), "Apply a recently modified remote record");
+
+ remoteRecord.modified = 0;
+ ok(engine._reconcile(remoteRecord), "Apply a remote record modified long ago");
+
+ // Remote tab records are never tracked locally, so the only
+ // time they're skipped is when they're marked as deleted.
+ remoteRecord.deleted = true;
+ ok(!engine._reconcile(remoteRecord), "Skip a deleted remote record");
+
+ _("Create an incoming local record");
+ // The locally tracked tab record always takes precedence over its
+ // remote counterparts.
+ let localRecord = {id: engine.service.clientsEngine.localID,
+ cleartext: "this should always be skipped",
+ modified: 2000};
+
+ ok(!engine._reconcile(localRecord), "Skip incoming local if recently modified");
+
+ localRecord.modified = 0;
+ ok(!engine._reconcile(localRecord), "Skip incoming local if modified long ago");
+
+ localRecord.deleted = true;
+ ok(!engine._reconcile(localRecord), "Skip incoming local if deleted");
+
+ run_next_test();
+});
diff --git a/services/sync/tests/unit/test_tab_store.js b/services/sync/tests/unit/test_tab_store.js
index 1dd8e06d1..f8265492f 100644
--- a/services/sync/tests/unit/test_tab_store.js
+++ b/services/sync/tests/unit/test_tab_store.js
@@ -4,25 +4,14 @@
Cu.import("resource://services-sync/engines/tabs.js");
Cu.import("resource://services-sync/service.js");
Cu.import("resource://services-sync/util.js");
-Cu.import("resource://testing-common/services-common/utils.js");
-
-function test_lastUsed() {
- let store = new TabEngine(Service)._store;
-
- _("Check extraction of last used times from tab objects.");
- let expected = [
- [0, {}],
- [0, {extData: null}],
- [0, {extData: {}}],
- [0, {extData: {weaveLastUsed: null}}],
- [123456789, {extData: {weaveLastUsed: "123456789"}}],
- [123456789, {extData: {weaveLastUsed: 123456789}}],
- [123456789, {extData: {weaveLastUsed: 123456789.12}}]
- ];
-
- for each (let [ex, input] in expected) {
- do_check_eq(ex, store.tabLastUsed(input));
- }
+Cu.import("resource://testing-common/services/common/utils.js");
+
+function getMockStore() {
+ let engine = new TabEngine(Service);
+ let store = engine._store;
+ store.getTabState = mockGetTabState;
+ store.shouldSkipWindow = mockShouldSkipWindow;
+ return store;
}
function test_create() {
@@ -38,19 +27,19 @@ function test_create() {
do_check_eq(Svc.Prefs.get("notifyTabState"), 1);
_("Create a second record");
- let rec = {id: "id2",
- clientName: "clientName2",
- cleartext: "cleartext2",
- modified: 2000};
+ rec = {id: "id2",
+ clientName: "clientName2",
+ cleartext: "cleartext2",
+ modified: 2000};
store.applyIncoming(rec);
do_check_eq(store._remoteClients["id2"], "cleartext2");
do_check_eq(Svc.Prefs.get("notifyTabState"), 0);
_("Create a third record");
- let rec = {id: "id3",
- clientName: "clientName3",
- cleartext: "cleartext3",
- modified: 3000};
+ rec = {id: "id3",
+ clientName: "clientName3",
+ cleartext: "cleartext3",
+ modified: 3000};
store.applyIncoming(rec);
do_check_eq(store._remoteClients["id3"], "cleartext3");
do_check_eq(Svc.Prefs.get("notifyTabState"), 0);
@@ -59,84 +48,74 @@ function test_create() {
Svc.Prefs.reset("notifyTabState");
}
-function fakeSessionSvc(url, numtabs) {
- // first delete the getter, or the previously
- // created fake Session
- delete Svc.Session;
- Svc.Session = {
- getBrowserState: function() {
- let obj = {
- windows: [{
- tabs: [{
- index: 1,
- entries: [{
- url: url,
- title: "title"
- }],
- attributes: {
- image: "image"
- },
- extData: {
- weaveLastUsed: 1
- }
- }]
- }]
- };
- if (numtabs) {
- let tabs = obj.windows[0].tabs;
- for (let i = 0; i < numtabs-1; i++)
- tabs.push(TestingUtils.deepCopy(tabs[0]));
- }
- return JSON.stringify(obj);
- }
- };
-};
-
function test_getAllTabs() {
- let store = new TabEngine(Service)._store, tabs;
+ let store = getMockStore();
+ let tabs;
+
+ let threeUrls = ["http://foo.com", "http://fuubar.com", "http://barbar.com"];
+
+ store.getWindowEnumerator = mockGetWindowEnumerator.bind(this, "http://bar.com", 1, 1, () => 2, () => threeUrls);
- _("get all tabs");
- fakeSessionSvc("http://foo.com");
+ _("Get all tabs.");
tabs = store.getAllTabs();
+ _("Tabs: " + JSON.stringify(tabs));
do_check_eq(tabs.length, 1);
do_check_eq(tabs[0].title, "title");
- do_check_eq(tabs[0].urlHistory.length, 1);
- do_check_eq(tabs[0].urlHistory[0], ["http://foo.com"]);
+ do_check_eq(tabs[0].urlHistory.length, 2);
+ do_check_eq(tabs[0].urlHistory[0], "http://foo.com");
+ do_check_eq(tabs[0].urlHistory[1], "http://bar.com");
do_check_eq(tabs[0].icon, "image");
do_check_eq(tabs[0].lastUsed, 1);
- _("get all tabs, and check that filtering works");
- // we don't bother testing every URL type here, the
- // filteredUrls regex really should have it own tests
- fakeSessionSvc("about:foo");
+ _("Get all tabs, and check that filtering works.");
+ let twoUrls = ["about:foo", "http://fuubar.com"];
+ store.getWindowEnumerator = mockGetWindowEnumerator.bind(this, "http://foo.com", 1, 1, () => 2, () => twoUrls);
tabs = store.getAllTabs(true);
+ _("Filtered: " + JSON.stringify(tabs));
do_check_eq(tabs.length, 0);
+
+ _("Get all tabs, and check that the entries safety limit works.");
+ let allURLs = [];
+ for (let i = 0; i < 50; i++) {
+ allURLs.push("http://foo" + i + ".bar");
+ }
+ allURLs.splice(35, 0, "about:foo", "about:bar", "about:foobar");
+
+ store.getWindowEnumerator = mockGetWindowEnumerator.bind(this, "http://bar.com", 1, 1, () => 45, () => allURLs);
+ tabs = store.getAllTabs((url) => url.startsWith("about"));
+
+ _("Sliced: " + JSON.stringify(tabs));
+ do_check_eq(tabs.length, 1);
+ do_check_eq(tabs[0].urlHistory.length, 25);
+ do_check_eq(tabs[0].urlHistory[0], "http://foo40.bar");
+ do_check_eq(tabs[0].urlHistory[24], "http://foo16.bar");
}
function test_createRecord() {
- let store = new TabEngine(Service)._store, record;
+ let store = getMockStore();
+ let record;
+
+ store.getTabState = mockGetTabState;
+ store.shouldSkipWindow = mockShouldSkipWindow;
+ store.getWindowEnumerator = mockGetWindowEnumerator.bind(this, "http://foo.com", 1, 1);
- // get some values before testing
- fakeSessionSvc("http://foo.com");
let tabs = store.getAllTabs();
let tabsize = JSON.stringify(tabs[0]).length;
let numtabs = Math.ceil(20000./77.);
- _("create a record");
- fakeSessionSvc("http://foo.com");
+ store.getWindowEnumerator = mockGetWindowEnumerator.bind(this, "http://foo.com", 1, 1);
record = store.createRecord("fake-guid");
do_check_true(record instanceof TabSetRecord);
do_check_eq(record.tabs.length, 1);
_("create a big record");
- fakeSessionSvc("http://foo.com", numtabs);
+ store.getWindowEnumerator = mockGetWindowEnumerator.bind(this, "http://foo.com", 1, numtabs);
record = store.createRecord("fake-guid");
do_check_true(record instanceof TabSetRecord);
do_check_eq(record.tabs.length, 256);
}
function run_test() {
- test_lastUsed();
test_create();
test_getAllTabs();
test_createRecord();
diff --git a/services/sync/tests/unit/test_tab_tracker.js b/services/sync/tests/unit/test_tab_tracker.js
index 230dc68c1..e7dd48829 100644
--- a/services/sync/tests/unit/test_tab_tracker.js
+++ b/services/sync/tests/unit/test_tab_tracker.js
@@ -34,18 +34,6 @@ function fakeSvcWinMediator() {
return logs;
}
-function fakeSvcSession() {
- // actions on Session are captured in logs
- let logs = [];
- delete Svc.Session;
- Svc.Session = {
- setTabValue: function(target, prop, value) {
- logs.push({target: target, prop: prop, value: value});
- }
- };
- return logs;
-}
-
function run_test() {
let engine = Service.engineManager.get("tabs");
@@ -88,8 +76,6 @@ function run_test() {
}
_("Test tab listener");
- logs = fakeSvcSession();
- let idx = 0;
for each (let evttype in ["TabOpen", "TabClose", "TabSelect"]) {
// Pretend we just synced.
tracker.clearChangedIDs();
@@ -100,11 +86,6 @@ function run_test() {
do_check_true(tracker.modified);
do_check_true(Utils.deepEquals(Object.keys(engine.getChangedIDs()),
[clientsEngine.localID]));
- do_check_eq(logs.length, idx+1);
- do_check_eq(logs[idx].target, evttype);
- do_check_eq(logs[idx].prop, "weaveLastUsed");
- do_check_true(typeof logs[idx].value == "number");
- idx++;
}
// Pretend we just synced.
@@ -114,5 +95,4 @@ function run_test() {
tracker.onTab({type: "pageshow", originalTarget: "pageshow"});
do_check_true(Utils.deepEquals(Object.keys(engine.getChangedIDs()),
[clientsEngine.localID]));
- do_check_eq(logs.length, idx); // test that setTabValue isn't called
}
diff --git a/services/sync/tests/unit/test_upgrade_old_sync_key.js b/services/sync/tests/unit/test_upgrade_old_sync_key.js
index f9f5210d6..ff75a435a 100644
--- a/services/sync/tests/unit/test_upgrade_old_sync_key.js
+++ b/services/sync/tests/unit/test_upgrade_old_sync_key.js
@@ -4,11 +4,13 @@
Cu.import("resource://services-sync/constants.js");
Cu.import("resource://services-sync/service.js");
Cu.import("resource://services-sync/util.js");
+Cu.import("resource://testing-common/services/sync/utils.js");
// Test upgrade of a dashed old-style sync key.
function run_test() {
const PBKDF2_KEY_BYTES = 16;
initTestLogging("Trace");
+ ensureLegacyIdentityManager();
let passphrase = "abcde-abcde-abcde-abcde";
do_check_false(Utils.isPassphrase(passphrase));
diff --git a/services/sync/tests/unit/test_utils_getIcon.js b/services/sync/tests/unit/test_utils_getIcon.js
deleted file mode 100644
index 8435ee180..000000000
--- a/services/sync/tests/unit/test_utils_getIcon.js
+++ /dev/null
@@ -1,18 +0,0 @@
-Cu.import("resource://services-sync/util.js");
-
-function run_test() {
- _("Test with a valid icon URI");
- let iconUri = "http://foo.bar/favicon.png";
- let icon1 = Utils.getIcon(iconUri);
- do_check_true(icon1.indexOf(iconUri) > 0);
-
- _("Test with an invalid icon URI and default icon");
- let icon2 = Utils.getIcon("foo", "bar");
- do_check_eq(icon2, "bar");
-
- _("Test with an invalid icon URI and no default icon");
- let icon3 = Utils.getIcon("foo");
- var defaultFavicon = Cc["@mozilla.org/browser/favicon-service;1"]
- .getService(Ci.nsIFaviconService).defaultFavicon.spec;
- do_check_eq(icon3, defaultFavicon);
-}
diff --git a/services/sync/tests/unit/test_utils_json.js b/services/sync/tests/unit/test_utils_json.js
index 33afb6207..efa7d9b4d 100644
--- a/services/sync/tests/unit/test_utils_json.js
+++ b/services/sync/tests/unit/test_utils_json.js
@@ -109,3 +109,6 @@ add_test(function test_load_logging() {
}));
});
+add_task(function* test_undefined_callback() {
+ yield Utils.jsonSave("foo", {}, ["v1", "v2"]);
+});
diff --git a/services/sync/tests/unit/test_utils_passphrase.js b/services/sync/tests/unit/test_utils_passphrase.js
index 38c7f9c11..6d34697be 100644
--- a/services/sync/tests/unit/test_utils_passphrase.js
+++ b/services/sync/tests/unit/test_utils_passphrase.js
@@ -7,7 +7,7 @@ function run_test() {
const key = "abcdefghijkmnpqrstuvwxyz23456789";
_("Passphrase only contains [" + key + "].");
- do_check_true(pp.split('').every(function(chr) key.indexOf(chr) != -1));
+ do_check_true(pp.split('').every(chr => key.indexOf(chr) != -1));
_("Hyphenated passphrase has 5 hyphens.");
let hyphenated = Utils.hyphenatePassphrase(pp);
diff --git a/services/sync/tests/unit/test_warn_on_truncated_response.js b/services/sync/tests/unit/test_warn_on_truncated_response.js
new file mode 100644
index 000000000..a9f070ee4
--- /dev/null
+++ b/services/sync/tests/unit/test_warn_on_truncated_response.js
@@ -0,0 +1,95 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+Cu.import("resource://testing-common/httpd.js");
+Cu.import("resource://services-sync/resource.js");
+Cu.import("resource://services-sync/rest.js");
+
+function run_test() {
+ initTestLogging("Trace");
+ run_next_test();
+}
+
+let BODY = "response body";
+// contentLength needs to be longer than the response body
+// length in order to get a mismatch between what is sent in
+// the response and the content-length header value.
+let contentLength = BODY.length + 1;
+
+function contentHandler(request, response) {
+ _("Handling request.");
+ response.setHeader("Content-Type", "text/plain");
+ response.setStatusLine(request.httpVersion, 200, "OK");
+ response.bodyOutputStream.write(BODY, contentLength);
+}
+
+function getWarningMessages(log) {
+ let warnMessages = [];
+ let warn = log.warn;
+ log.warn = function (message) {
+ let regEx = /The response body\'s length of: \d+ doesn\'t match the header\'s content-length of: \d+/i
+ if (message.match(regEx)) {
+ warnMessages.push(message);
+ }
+ warn.call(log, message);
+ }
+ return warnMessages;
+}
+
+add_test(function test_resource_logs_content_length_mismatch() {
+ _("Issuing request.");
+ let httpServer = httpd_setup({"/content": contentHandler});
+ let resource = new Resource(httpServer.baseURI + "/content");
+
+ let warnMessages = getWarningMessages(resource._log);
+ let result = resource.get();
+
+ notEqual(warnMessages.length, 0, "test that a warning was logged");
+ notEqual(result.length, contentLength);
+ equal(result, BODY);
+
+ httpServer.stop(run_next_test);
+});
+
+add_test(function test_async_resource_logs_content_length_mismatch() {
+ _("Issuing request.");
+ let httpServer = httpd_setup({"/content": contentHandler});
+ let asyncResource = new AsyncResource(httpServer.baseURI + "/content");
+
+ let warnMessages = getWarningMessages(asyncResource._log);
+
+ asyncResource.get(function (error, content) {
+ equal(error, null);
+ equal(content, BODY);
+ notEqual(warnMessages.length, 0, "test that warning was logged");
+ notEqual(content.length, contentLength);
+ httpServer.stop(run_next_test);
+ });
+});
+
+add_test(function test_sync_storage_request_logs_content_length_mismatch() {
+ _("Issuing request.");
+ let httpServer = httpd_setup({"/content": contentHandler});
+ let request = new SyncStorageRequest(httpServer.baseURI + "/content");
+ let warnMessages = getWarningMessages(request._log);
+
+ // Setting this affects how received data is read from the underlying
+ // nsIHttpChannel in rest.js. If it's left as UTF-8 (the default) an
+ // nsIConverterInputStream is used and the data read from channel's stream
+ // isn't truncated at the null byte mark (\u0000). Therefore the
+ // content-length mismatch being tested for doesn't occur. Setting it to
+ // a falsy value results in an nsIScriptableInputStream being used to read
+ // the stream, which stops reading at the null byte mark resulting in a
+ // content-length mismatch.
+ request.charset = "";
+
+ request.get(function (error) {
+ equal(error, null);
+ equal(this.response.body, BODY);
+ notEqual(warnMessages.length, 0, "test that a warning was logged");
+ notEqual(BODY.length, contentLength);
+ httpServer.stop(run_next_test);
+ });
+});
diff --git a/services/sync/tests/unit/xpcshell.ini b/services/sync/tests/unit/xpcshell.ini
index 107a573aa..dc33c0eb2 100644
--- a/services/sync/tests/unit/xpcshell.ini
+++ b/services/sync/tests/unit/xpcshell.ini
@@ -2,6 +2,15 @@
head = head_appinfo.js ../../../common/tests/unit/head_helpers.js head_helpers.js head_http_server.js
tail =
firefox-appdir = browser
+skip-if = toolkit == 'gonk'
+support-files =
+ addon1-search.xml
+ bootstrap1-search.xml
+ fake_login_manager.js
+ missing-sourceuri.xml
+ missing-xpi-search.xml
+ places_v10_from_v11.sqlite
+ rewrite-search.xml
# The manifest is roughly ordered from low-level to high-level. When making
# systemic sweeping changes, this makes it easier to identify errors closer to
@@ -17,7 +26,6 @@ firefox-appdir = browser
[test_utils_deriveKey.js]
[test_utils_keyEncoding.js]
[test_utils_getErrorString.js]
-[test_utils_getIcon.js]
[test_utils_json.js]
[test_utils_lazyStrings.js]
[test_utils_lock.js]
@@ -27,6 +35,7 @@ firefox-appdir = browser
# We have a number of other libraries that are pretty much standalone.
[test_addon_utils.js]
+run-sequentially = Restarts server, can't change pref.
[test_httpd_sync_server.js]
[test_jpakeclient.js]
# Bug 618233: this test produces random failures on Windows 7.
@@ -41,6 +50,7 @@ skip-if = os == "win" || os == "android"
[test_syncstoragerequest.js]
# Generic Sync types.
+[test_browserid_identity.js]
[test_collection_inc_get.js]
[test_collections_recovery.js]
[test_identity_manager.js]
@@ -92,6 +102,7 @@ skip-if = os == "android"
skip-if = os == "mac" || os == "linux"
[test_corrupt_keys.js]
+[test_declined.js]
[test_errorhandler.js]
[test_errorhandler_filelog.js]
# Bug 676978: test hangs on Android (see also testing/xpcshell/xpcshell.ini)
@@ -99,6 +110,7 @@ skip-if = os == "android"
[test_errorhandler_sync_checkServerError.js]
# Bug 676978: test hangs on Android (see also testing/xpcshell/xpcshell.ini)
skip-if = os == "android"
+[test_errorhandler_eol.js]
[test_hmac_error.js]
[test_interval_triggers.js]
[test_node_reassignment.js]
@@ -110,10 +122,17 @@ skip-if = os == "android"
[test_syncscheduler.js]
[test_upgrade_old_sync_key.js]
+# Firefox Accounts specific tests
+[test_fxa_startOver.js]
+[test_fxa_service_cluster.js]
+[test_fxa_node_reassignment.js]
+
# Finally, we test each engine.
[test_addons_engine.js]
+run-sequentially = Hardcoded port in static files.
[test_addons_reconciler.js]
[test_addons_store.js]
+run-sequentially = Hardcoded port in static files.
[test_addons_tracker.js]
[test_bookmark_batch_fail.js]
[test_bookmark_engine.js]
@@ -148,3 +167,16 @@ skip-if = debug
[test_tab_engine.js]
[test_tab_store.js]
[test_tab_tracker.js]
+
+[test_healthreport.js]
+skip-if = ! healthreport
+
+[test_healthreport_migration.js]
+skip-if = ! healthreport
+
+[test_warn_on_truncated_response.js]
+
+# FxA migration
+[test_block_sync.js]
+[test_fxa_migration.js]
+[test_fxa_migration_sentinel.js]
diff --git a/services/sync/tps/extensions/mozmill/chrome.manifest b/services/sync/tps/extensions/mozmill/chrome.manifest
index 8e1dcf37c..dfb370321 100644
--- a/services/sync/tps/extensions/mozmill/chrome.manifest
+++ b/services/sync/tps/extensions/mozmill/chrome.manifest
@@ -1,2 +1,2 @@
-resource mozmill resource/
+resource mozmill resource/
diff --git a/services/sync/tps/extensions/mozmill/defaults/preferences/debug.js b/services/sync/tps/extensions/mozmill/defaults/preferences/debug.js
deleted file mode 100644
index 03b780e8d..000000000
--- a/services/sync/tps/extensions/mozmill/defaults/preferences/debug.js
+++ /dev/null
@@ -1,7 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-/* debugging prefs */
-pref("browser.dom.window.dump.enabled", true);
-pref("javascript.options.showInConsole", true);
diff --git a/services/sync/tps/extensions/mozmill/install.rdf b/services/sync/tps/extensions/mozmill/install.rdf
index 165498161..bbc759cf1 100644
--- a/services/sync/tps/extensions/mozmill/install.rdf
+++ b/services/sync/tps/extensions/mozmill/install.rdf
@@ -1,71 +1,24 @@
<?xml version="1.0"?>
-<!-- This Source Code Form is subject to the terms of the Mozilla Public
- - License, v. 2.0. If a copy of the MPL was not distributed with this
- - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
-
<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:em="http://www.mozilla.org/2004/em-rdf#">
+
<Description about="urn:mozilla:install-manifest">
<em:id>mozmill@mozilla.com</em:id>
- <em:name>MozMill</em:name>
- <em:version>2.0b1</em:version>
- <em:creator>Adam Christian</em:creator>
- <em:description>A testing extension based on the Windmill Testing Framework client source</em:description>
+ <em:name>Mozmill</em:name>
+ <em:version>2.0.8</em:version>
+ <em:description>UI Automation tool for Mozilla applications</em:description>
<em:unpack>true</em:unpack>
+
+ <em:creator>Mozilla Automation and Testing Team</em:creator>
+ <em:contributor>Adam Christian</em:contributor>
+ <em:contributor>Mikeal Rogers</em:contributor>
+
<em:targetApplication>
- <!-- Pale Moon -->
- <Description>
- <em:id>{8de7fcbb-c55c-4fbe-bfc5-fc555c87dbc4}</em:id>
- <em:minVersion>3.5</em:minVersion>
- <em:maxVersion>24.*</em:maxVersion>
- </Description>
- </em:targetApplication>
- <em:targetApplication>
- <!-- Firefox -->
- <Description>
- <em:id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</em:id>
- <em:minVersion>3.5</em:minVersion>
- <em:maxVersion>12.*</em:maxVersion>
- </Description>
- </em:targetApplication>
- <em:targetApplication>
- <!-- Thunderbird -->
- <Description>
- <em:id>{3550f703-e582-4d05-9a08-453d09bdfdc6}</em:id>
- <em:minVersion>3.0a1pre</em:minVersion>
- <em:maxVersion>9.*</em:maxVersion>
- </Description>
- </em:targetApplication>
- <em:targetApplication>
- <!-- Sunbird -->
- <Description>
- <em:id>{718e30fb-e89b-41dd-9da7-e25a45638b28}</em:id>
- <em:minVersion>0.6a1</em:minVersion>
- <em:maxVersion>1.0pre</em:maxVersion>
- </Description>
- </em:targetApplication>
- <em:targetApplication>
- <!-- SeaMonkey -->
<Description>
- <em:id>{92650c4d-4b8e-4d2a-b7eb-24ecf4f6b63a}</em:id>
- <em:minVersion>2.0a1</em:minVersion>
- <em:maxVersion>9.*</em:maxVersion>
+ <em:id>toolkit@mozilla.org</em:id>
+ <em:minVersion>10.0</em:minVersion>
+ <em:maxVersion>38.*</em:maxVersion>
</Description>
</em:targetApplication>
- <em:targetApplication>
- <!-- Songbird -->
- <Description>
- <em:id>songbird@songbirdnest.com</em:id>
- <em:minVersion>0.3pre</em:minVersion>
- <em:maxVersion>1.3.0a</em:maxVersion>
- </Description>
- </em:targetApplication>
- <em:targetApplication>
- <Description>
- <em:id>toolkit@mozilla.org</em:id>
- <em:minVersion>1.9.1</em:minVersion>
- <em:maxVersion>9.*</em:maxVersion>
- </Description>
- </em:targetApplication>
</Description>
</RDF>
diff --git a/services/sync/tps/extensions/mozmill/resource/driver/controller.js b/services/sync/tps/extensions/mozmill/resource/driver/controller.js
new file mode 100644
index 000000000..c3539bcb3
--- /dev/null
+++ b/services/sync/tps/extensions/mozmill/resource/driver/controller.js
@@ -0,0 +1,1150 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, you can obtain one at http://mozilla.org/MPL/2.0/. */
+
+var EXPORTED_SYMBOLS = ["MozMillController", "globalEventRegistry",
+ "sleep", "windowMap"];
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cu = Components.utils;
+
+var EventUtils = {}; Cu.import('resource://mozmill/stdlib/EventUtils.js', EventUtils);
+
+var assertions = {}; Cu.import('resource://mozmill/modules/assertions.js', assertions);
+var broker = {}; Cu.import('resource://mozmill/driver/msgbroker.js', broker);
+var elementslib = {}; Cu.import('resource://mozmill/driver/elementslib.js', elementslib);
+var errors = {}; Cu.import('resource://mozmill/modules/errors.js', errors);
+var mozelement = {}; Cu.import('resource://mozmill/driver/mozelement.js', mozelement);
+var utils = {}; Cu.import('resource://mozmill/stdlib/utils.js', utils);
+var windows = {}; Cu.import('resource://mozmill/modules/windows.js', windows);
+
+// Declare most used utils functions in the controller namespace
+var assert = new assertions.Assert();
+var waitFor = assert.waitFor;
+
+var sleep = utils.sleep;
+
+// For Mozmill 1.5 backward compatibility
+var windowMap = windows.map;
+
+waitForEvents = function () {
+}
+
+waitForEvents.prototype = {
+ /**
+ * Initialize list of events for given node
+ */
+ init: function waitForEvents_init(node, events) {
+ if (node.getNode != undefined)
+ node = node.getNode();
+
+ this.events = events;
+ this.node = node;
+ node.firedEvents = {};
+ this.registry = {};
+
+ for each (var e in events) {
+ var listener = function (event) {
+ this.firedEvents[event.type] = true;
+ }
+
+ this.registry[e] = listener;
+ this.registry[e].result = false;
+ this.node.addEventListener(e, this.registry[e], true);
+ }
+ },
+
+ /**
+ * Wait until all assigned events have been fired
+ */
+ wait: function waitForEvents_wait(timeout, interval) {
+ for (var e in this.registry) {
+ assert.waitFor(function () {
+ return this.node.firedEvents[e] == true;
+ }, "waitForEvents.wait(): Event '" + ex + "' has been fired.", timeout, interval);
+
+ this.node.removeEventListener(e, this.registry[e], true);
+ }
+ }
+}
+
+/**
+ * Class to handle menus and context menus
+ *
+ * @constructor
+ * @param {MozMillController} controller
+ * Mozmill controller of the window under test
+ * @param {string} menuSelector
+ * jQuery like selector string of the element
+ * @param {object} document
+ * Document to use for finding the menu
+ * [optional - default: aController.window.document]
+ */
+var Menu = function (controller, menuSelector, document) {
+ this._controller = controller;
+ this._menu = null;
+
+ document = document || controller.window.document;
+ var node = document.querySelector(menuSelector);
+ if (node) {
+ // We don't unwrap nodes automatically yet (Bug 573185)
+ node = node.wrappedJSObject || node;
+ this._menu = new mozelement.Elem(node);
+ } else {
+ throw new Error("Menu element '" + menuSelector + "' not found.");
+ }
+}
+
+Menu.prototype = {
+
+ /**
+ * Open and populate the menu
+ *
+ * @param {ElemBase} contextElement
+ * Element whose context menu has to be opened
+ * @returns {Menu} The Menu instance
+ */
+ open: function Menu_open(contextElement) {
+ // We have to open the context menu
+ var menu = this._menu.getNode();
+ if ((menu.localName == "popup" || menu.localName == "menupopup") &&
+ contextElement && contextElement.exists()) {
+ this._controller.rightClick(contextElement);
+ assert.waitFor(function () {
+ return menu.state == "open";
+ }, "Context menu has been opened.");
+ }
+
+ // Run through the entire menu and populate with dynamic entries
+ this._buildMenu(menu);
+
+ return this;
+ },
+
+ /**
+ * Close the menu
+ *
+ * @returns {Menu} The Menu instance
+ */
+ close: function Menu_close() {
+ var menu = this._menu.getNode();
+
+ this._controller.keypress(this._menu, "VK_ESCAPE", {});
+ assert.waitFor(function () {
+ return menu.state == "closed";
+ }, "Context menu has been closed.");
+
+ return this;
+ },
+
+ /**
+ * Retrieve the specified menu entry
+ *
+ * @param {string} itemSelector
+ * jQuery like selector string of the menu item
+ * @returns {ElemBase} Menu element
+ * @throws Error If menu element has not been found
+ */
+ getItem: function Menu_getItem(itemSelector) {
+ // Run through the entire menu and populate with dynamic entries
+ this._buildMenu(this._menu.getNode());
+
+ var node = this._menu.getNode().querySelector(itemSelector);
+
+ if (!node) {
+ throw new Error("Menu entry '" + itemSelector + "' not found.");
+ }
+
+ return new mozelement.Elem(node);
+ },
+
+ /**
+ * Click the specified menu entry
+ *
+ * @param {string} itemSelector
+ * jQuery like selector string of the menu item
+ *
+ * @returns {Menu} The Menu instance
+ */
+ click: function Menu_click(itemSelector) {
+ this._controller.click(this.getItem(itemSelector));
+
+ return this;
+ },
+
+ /**
+ * Synthesize a keypress against the menu
+ *
+ * @param {string} key
+ * Key to press
+ * @param {object} modifier
+ * Key modifiers
+ * @see MozMillController#keypress
+ *
+ * @returns {Menu} The Menu instance
+ */
+ keypress: function Menu_keypress(key, modifier) {
+ this._controller.keypress(this._menu, key, modifier);
+
+ return this;
+ },
+
+ /**
+ * Opens the context menu, click the specified entry and
+ * make sure that the menu has been closed.
+ *
+ * @param {string} itemSelector
+ * jQuery like selector string of the element
+ * @param {ElemBase} contextElement
+ * Element whose context menu has to be opened
+ *
+ * @returns {Menu} The Menu instance
+ */
+ select: function Menu_select(itemSelector, contextElement) {
+ this.open(contextElement);
+ this.click(itemSelector);
+ this.close();
+ },
+
+ /**
+ * Recursive function which iterates through all menu elements and
+ * populates the menus with dynamic menu entries.
+ *
+ * @param {node} menu
+ * Top menu node whose elements have to be populated
+ */
+ _buildMenu: function Menu__buildMenu(menu) {
+ var items = menu ? menu.childNodes : null;
+
+ Array.forEach(items, function (item) {
+ // When we have a menu node, fake a click onto it to populate
+ // the sub menu with dynamic entries
+ if (item.tagName == "menu") {
+ var popup = item.querySelector("menupopup");
+
+ if (popup) {
+ var popupEvent = this._controller.window.document.createEvent("MouseEvent");
+ popupEvent.initMouseEvent("popupshowing", true, true,
+ this._controller.window, 0, 0, 0, 0, 0,
+ false, false, false, false, 0, null);
+ popup.dispatchEvent(popupEvent);
+
+ this._buildMenu(popup);
+ }
+ }
+ }, this);
+ }
+};
+
+var MozMillController = function (window) {
+ this.window = window;
+
+ this.mozmillModule = {};
+ Cu.import('resource://mozmill/driver/mozmill.js', this.mozmillModule);
+
+ var self = this;
+ assert.waitFor(function () {
+ return window != null && self.isLoaded();
+ }, "controller(): Window has been initialized.");
+
+ // Ensure to focus the window which will move it virtually into the foreground
+ // when focusmanager.testmode is set enabled.
+ this.window.focus();
+
+ var windowType = window.document.documentElement.getAttribute('windowtype');
+ if (controllerAdditions[windowType] != undefined ) {
+ this.prototype = new utils.Copy(this.prototype);
+ controllerAdditions[windowType](this);
+ this.windowtype = windowType;
+ }
+}
+
+/**
+ * Returns the global browser object of the window
+ *
+ * @returns {Object} The browser object
+ */
+MozMillController.prototype.__defineGetter__("browserObject", function () {
+ return utils.getBrowserObject(this.window);
+});
+
+// constructs a MozMillElement from the controller's window
+MozMillController.prototype.__defineGetter__("rootElement", function () {
+ if (this._rootElement == undefined) {
+ let docElement = this.window.document.documentElement;
+ this._rootElement = new mozelement.MozMillElement("Elem", docElement);
+ }
+
+ return this._rootElement;
+});
+
+MozMillController.prototype.sleep = utils.sleep;
+MozMillController.prototype.waitFor = assert.waitFor;
+
+// Open the specified url in the current tab
+MozMillController.prototype.open = function (url) {
+ switch (this.mozmillModule.Application) {
+ case "Firefox":
+ case "MetroFirefox":
+ // Stop a running page load to not overlap requests
+ if (this.browserObject.selectedBrowser) {
+ this.browserObject.selectedBrowser.stop();
+ }
+
+ this.browserObject.loadURI(url);
+ break;
+
+ default:
+ throw new Error("MozMillController.open not supported.");
+ }
+
+ broker.pass({'function':'Controller.open()'});
+}
+
+/**
+ * Take a screenshot of specified node
+ *
+ * @param {Element} node
+ * The window or DOM element to capture
+ * @param {String} name
+ * The name of the screenshot used in reporting and as filename
+ * @param {Boolean} save
+ * If true saves the screenshot as 'name.jpg' in tempdir,
+ * otherwise returns a dataURL
+ * @param {Element[]} highlights
+ * A list of DOM elements to highlight by drawing a red rectangle around them
+ *
+ * @returns {Object} Object which contains properties like filename, dataURL,
+ * name and timestamp of the screenshot
+ */
+MozMillController.prototype.screenshot = function (node, name, save, highlights) {
+ if (!node) {
+ throw new Error("node is undefined");
+ }
+
+ // Unwrap the node and highlights
+ if ("getNode" in node) {
+ node = node.getNode();
+ }
+
+ if (highlights) {
+ for (var i = 0; i < highlights.length; ++i) {
+ if ("getNode" in highlights[i]) {
+ highlights[i] = highlights[i].getNode();
+ }
+ }
+ }
+
+ // If save is false, a dataURL is used
+ // Include both in the report anyway to avoid confusion and make the report easier to parse
+ var screenshot = {"filename": undefined,
+ "dataURL": utils.takeScreenshot(node, highlights),
+ "name": name,
+ "timestamp": new Date().toLocaleString()};
+
+ if (!save) {
+ return screenshot;
+ }
+
+ // Save the screenshot to disk
+
+ let {filename, failure} = utils.saveDataURL(screenshot.dataURL, name);
+ screenshot.filename = filename;
+ screenshot.failure = failure;
+
+ if (failure) {
+ broker.log({'function': 'controller.screenshot()',
+ 'message': 'Error writing to file: ' + screenshot.filename});
+ } else {
+ // Send the screenshot object to python over jsbridge
+ broker.sendMessage("screenshot", screenshot);
+ broker.pass({'function': 'controller.screenshot()'});
+ }
+
+ return screenshot;
+}
+
+/**
+ * Checks if the specified window has been loaded
+ *
+ * @param {DOMWindow} [aWindow=this.window] Window object to check for loaded state
+ */
+MozMillController.prototype.isLoaded = function (aWindow) {
+ var win = aWindow || this.window;
+
+ return windows.map.getValue(utils.getWindowId(win), "loaded") || false;
+};
+
+MozMillController.prototype.__defineGetter__("waitForEvents", function () {
+ if (this._waitForEvents == undefined) {
+ this._waitForEvents = new waitForEvents();
+ }
+
+ return this._waitForEvents;
+});
+
+/**
+ * Wrapper function to create a new instance of a menu
+ * @see Menu
+ */
+MozMillController.prototype.getMenu = function (menuSelector, document) {
+ return new Menu(this, menuSelector, document);
+};
+
+MozMillController.prototype.__defineGetter__("mainMenu", function () {
+ return this.getMenu("menubar");
+});
+
+MozMillController.prototype.__defineGetter__("menus", function () {
+ logDeprecated('controller.menus', 'Use controller.mainMenu instead');
+});
+
+MozMillController.prototype.waitForImage = function (aElement, timeout, interval) {
+ this.waitFor(function () {
+ return aElement.getNode().complete == true;
+ }, "timeout exceeded for waitForImage " + aElement.getInfo(), timeout, interval);
+
+ broker.pass({'function':'Controller.waitForImage()'});
+}
+
+MozMillController.prototype.startUserShutdown = function (timeout, restart, next, resetProfile) {
+ if (restart && resetProfile) {
+ throw new Error("You can't have a user-restart and reset the profile; there is a race condition");
+ }
+
+ let shutdownObj = {
+ 'user': true,
+ 'restart': Boolean(restart),
+ 'next': next,
+ 'resetProfile': Boolean(resetProfile),
+ 'timeout': timeout
+ };
+
+ broker.sendMessage('shutdown', shutdownObj);
+}
+
+/**
+ * Restart the application
+ *
+ * @param {string} aNext
+ * Name of the next test function to run after restart
+ * @param {boolean} [aFlags=undefined]
+ * Additional flags how to handle the shutdown or restart. The attributes
+ * eRestarti386 (0x20) and eRestartx86_64 (0x30) have not been documented yet.
+ * @see https://developer.mozilla.org/nsIAppStartup#Attributes
+ */
+MozMillController.prototype.restartApplication = function (aNext, aFlags) {
+ var flags = Ci.nsIAppStartup.eAttemptQuit | Ci.nsIAppStartup.eRestart;
+
+ if (aFlags) {
+ flags |= aFlags;
+ }
+
+ broker.sendMessage('shutdown', {'user': false,
+ 'restart': true,
+ 'flags': flags,
+ 'next': aNext,
+ 'timeout': 0 });
+
+ // We have to ensure to stop the test from continuing until the application is
+ // shutting down. The only way to do that is by throwing an exception.
+ throw new errors.ApplicationQuitError();
+}
+
+/**
+ * Stop the application
+ *
+ * @param {boolean} [aResetProfile=false]
+ * Whether to reset the profile during restart
+ * @param {boolean} [aFlags=undefined]
+ * Additional flags how to handle the shutdown or restart. The attributes
+ * eRestarti386 and eRestartx86_64 have not been documented yet.
+ * @see https://developer.mozilla.org/nsIAppStartup#Attributes
+ */
+MozMillController.prototype.stopApplication = function (aResetProfile, aFlags) {
+ var flags = Ci.nsIAppStartup.eAttemptQuit;
+
+ if (aFlags) {
+ flags |= aFlags;
+ }
+
+ broker.sendMessage('shutdown', {'user': false,
+ 'restart': false,
+ 'flags': flags,
+ 'resetProfile': aResetProfile,
+ 'timeout': 0 });
+
+ // We have to ensure to stop the test from continuing until the application is
+ // shutting down. The only way to do that is by throwing an exception.
+ throw new errors.ApplicationQuitError();
+}
+
+//Browser navigation functions
+MozMillController.prototype.goBack = function () {
+ this.window.content.history.back();
+ broker.pass({'function':'Controller.goBack()'});
+
+ return true;
+}
+
+MozMillController.prototype.goForward = function () {
+ this.window.content.history.forward();
+ broker.pass({'function':'Controller.goForward()'});
+
+ return true;
+}
+
+MozMillController.prototype.refresh = function () {
+ this.window.content.location.reload(true);
+ broker.pass({'function':'Controller.refresh()'});
+
+ return true;
+}
+
+function logDeprecated(funcName, message) {
+ broker.log({'function': funcName + '() - DEPRECATED',
+ 'message': funcName + '() is deprecated. ' + message});
+}
+
+function logDeprecatedAssert(funcName) {
+ logDeprecated('controller.' + funcName,
+ '. Use the generic `assertion` module instead.');
+}
+
+MozMillController.prototype.assertText = function (el, text) {
+ logDeprecatedAssert("assertText");
+
+ var n = el.getNode();
+
+ if (n && n.innerHTML == text) {
+ broker.pass({'function': 'Controller.assertText()'});
+ } else {
+ throw new Error("could not validate element " + el.getInfo() +
+ " with text "+ text);
+ }
+
+ return true;
+};
+
+/**
+ * Assert that a specified node exists
+ */
+MozMillController.prototype.assertNode = function (el) {
+ logDeprecatedAssert("assertNode");
+
+ //this.window.focus();
+ var element = el.getNode();
+ if (!element) {
+ throw new Error("could not find element " + el.getInfo());
+ }
+
+ broker.pass({'function': 'Controller.assertNode()'});
+ return true;
+};
+
+/**
+ * Assert that a specified node doesn't exist
+ */
+MozMillController.prototype.assertNodeNotExist = function (el) {
+ logDeprecatedAssert("assertNodeNotExist");
+
+ try {
+ var element = el.getNode();
+ } catch (e) {
+ broker.pass({'function': 'Controller.assertNodeNotExist()'});
+ }
+
+ if (element) {
+ throw new Error("Unexpectedly found element " + el.getInfo());
+ } else {
+ broker.pass({'function':'Controller.assertNodeNotExist()'});
+ }
+
+ return true;
+};
+
+/**
+ * Assert that a form element contains the expected value
+ */
+MozMillController.prototype.assertValue = function (el, value) {
+ logDeprecatedAssert("assertValue");
+
+ var n = el.getNode();
+
+ if (n && n.value == value) {
+ broker.pass({'function': 'Controller.assertValue()'});
+ } else {
+ throw new Error("could not validate element " + el.getInfo() +
+ " with value " + value);
+ }
+
+ return false;
+};
+
+/**
+ * Check if the callback function evaluates to true
+ */
+MozMillController.prototype.assert = function (callback, message, thisObject) {
+ logDeprecatedAssert("assert");
+
+ utils.assert(callback, message, thisObject);
+ broker.pass({'function': ": controller.assert('" + callback + "')"});
+
+ return true;
+}
+
+/**
+ * Assert that a provided value is selected in a select element
+ */
+MozMillController.prototype.assertSelected = function (el, value) {
+ logDeprecatedAssert("assertSelected");
+
+ var n = el.getNode();
+ var validator = value;
+
+ if (n && n.options[n.selectedIndex].value == validator) {
+ broker.pass({'function':'Controller.assertSelected()'});
+ } else {
+ throw new Error("could not assert value for element " + el.getInfo() +
+ " with value " + value);
+ }
+
+ return true;
+};
+
+/**
+ * Assert that a provided checkbox is checked
+ */
+MozMillController.prototype.assertChecked = function (el) {
+ logDeprecatedAssert("assertChecked");
+
+ var element = el.getNode();
+
+ if (element && element.checked == true) {
+ broker.pass({'function':'Controller.assertChecked()'});
+ } else {
+ throw new Error("assert failed for checked element " + el.getInfo());
+ }
+
+ return true;
+};
+
+/**
+ * Assert that a provided checkbox is not checked
+ */
+MozMillController.prototype.assertNotChecked = function (el) {
+ logDeprecatedAssert("assertNotChecked");
+
+ var element = el.getNode();
+
+ if (!element) {
+ throw new Error("Could not find element" + el.getInfo());
+ }
+
+ if (!element.hasAttribute("checked") || element.checked != true) {
+ broker.pass({'function': 'Controller.assertNotChecked()'});
+ } else {
+ throw new Error("assert failed for not checked element " + el.getInfo());
+ }
+
+ return true;
+};
+
+/**
+ * Assert that an element's javascript property exists or has a particular value
+ *
+ * if val is undefined, will return true if the property exists.
+ * if val is specified, will return true if the property exists and has the correct value
+ */
+MozMillController.prototype.assertJSProperty = function (el, attrib, val) {
+ logDeprecatedAssert("assertJSProperty");
+
+ var element = el.getNode();
+
+ if (!element){
+ throw new Error("could not find element " + el.getInfo());
+ }
+
+ var value = element[attrib];
+ var res = (value !== undefined && (val === undefined ? true :
+ String(value) == String(val)));
+ if (res) {
+ broker.pass({'function':'Controller.assertJSProperty("' + el.getInfo() + '") : ' + val});
+ } else {
+ throw new Error("Controller.assertJSProperty(" + el.getInfo() + ") : " +
+ (val === undefined ? "property '" + attrib +
+ "' doesn't exist" : val + " == " + value));
+ }
+
+ return true;
+};
+
+/**
+ * Assert that an element's javascript property doesn't exist or doesn't have a particular value
+ *
+ * if val is undefined, will return true if the property doesn't exist.
+ * if val is specified, will return true if the property doesn't exist or doesn't have the specified value
+ */
+MozMillController.prototype.assertNotJSProperty = function (el, attrib, val) {
+ logDeprecatedAssert("assertNotJSProperty");
+
+ var element = el.getNode();
+
+ if (!element){
+ throw new Error("could not find element " + el.getInfo());
+ }
+
+ var value = element[attrib];
+ var res = (val === undefined ? value === undefined : String(value) != String(val));
+ if (res) {
+ broker.pass({'function':'Controller.assertNotProperty("' + el.getInfo() + '") : ' + val});
+ } else {
+ throw new Error("Controller.assertNotJSProperty(" + el.getInfo() + ") : " +
+ (val === undefined ? "property '" + attrib +
+ "' exists" : val + " != " + value));
+ }
+
+ return true;
+};
+
+/**
+ * Assert that an element's dom property exists or has a particular value
+ *
+ * if val is undefined, will return true if the property exists.
+ * if val is specified, will return true if the property exists and has the correct value
+ */
+MozMillController.prototype.assertDOMProperty = function (el, attrib, val) {
+ logDeprecatedAssert("assertDOMProperty");
+
+ var element = el.getNode();
+
+ if (!element){
+ throw new Error("could not find element " + el.getInfo());
+ }
+
+ var value, res = element.hasAttribute(attrib);
+ if (res && val !== undefined) {
+ value = element.getAttribute(attrib);
+ res = (String(value) == String(val));
+ }
+
+ if (res) {
+ broker.pass({'function':'Controller.assertDOMProperty("' + el.getInfo() + '") : ' + val});
+ } else {
+ throw new Error("Controller.assertDOMProperty(" + el.getInfo() + ") : " +
+ (val === undefined ? "property '" + attrib +
+ "' doesn't exist" : val + " == " + value));
+ }
+
+ return true;
+};
+
+/**
+ * Assert that an element's dom property doesn't exist or doesn't have a particular value
+ *
+ * if val is undefined, will return true if the property doesn't exist.
+ * if val is specified, will return true if the property doesn't exist or doesn't have the specified value
+ */
+MozMillController.prototype.assertNotDOMProperty = function (el, attrib, val) {
+ logDeprecatedAssert("assertNotDOMProperty");
+
+ var element = el.getNode();
+
+ if (!element) {
+ throw new Error("could not find element " + el.getInfo());
+ }
+
+ var value, res = element.hasAttribute(attrib);
+ if (res && val !== undefined) {
+ value = element.getAttribute(attrib);
+ res = (String(value) == String(val));
+ }
+
+ if (!res) {
+ broker.pass({'function':'Controller.assertNotDOMProperty("' + el.getInfo() + '") : ' + val});
+ } else {
+ throw new Error("Controller.assertNotDOMProperty(" + el.getInfo() + ") : " +
+ (val == undefined ? "property '" + attrib +
+ "' exists" : val + " == " + value));
+ }
+
+ return true;
+};
+
+/**
+ * Assert that a specified image has actually loaded. The Safari workaround results
+ * in additional requests for broken images (in Safari only) but works reliably
+ */
+MozMillController.prototype.assertImageLoaded = function (el) {
+ logDeprecatedAssert("assertImageLoaded");
+
+ var img = el.getNode();
+
+ if (!img || img.tagName != 'IMG') {
+ throw new Error('Controller.assertImageLoaded() failed.')
+ return false;
+ }
+
+ var comp = img.complete;
+ var ret = null; // Return value
+
+ // Workaround for Safari -- it only supports the
+ // complete attrib on script-created images
+ if (typeof comp == 'undefined') {
+ test = new Image();
+ // If the original image was successfully loaded,
+ // src for new one should be pulled from cache
+ test.src = img.src;
+ comp = test.complete;
+ }
+
+ // Check the complete attrib. Note the strict
+ // equality check -- we don't want undefined, null, etc.
+ // --------------------------
+ if (comp === false) {
+ // False -- Img failed to load in IE/Safari, or is
+ // still trying to load in FF
+ ret = false;
+ } else if (comp === true && img.naturalWidth == 0) {
+ // True, but image has no size -- image failed to
+ // load in FF
+ ret = false;
+ } else {
+ // Otherwise all we can do is assume everything's
+ // hunky-dory
+ ret = true;
+ }
+
+ if (ret) {
+ broker.pass({'function':'Controller.assertImageLoaded'});
+ } else {
+ throw new Error('Controller.assertImageLoaded() failed.')
+ }
+
+ return true;
+};
+
+/**
+ * Drag one element to the top x,y coords of another specified element
+ */
+MozMillController.prototype.mouseMove = function (doc, start, dest) {
+ // if one of these elements couldn't be looked up
+ if (typeof start != 'object'){
+ throw new Error("received bad coordinates");
+ }
+
+ if (typeof dest != 'object'){
+ throw new Error("received bad coordinates");
+ }
+
+ var triggerMouseEvent = function (element, clientX, clientY) {
+ clientX = clientX ? clientX: 0;
+ clientY = clientY ? clientY: 0;
+
+ // make the mouse understand where it is on the screen
+ var screenX = element.boxObject.screenX ? element.boxObject.screenX : 0;
+ var screenY = element.boxObject.screenY ? element.boxObject.screenY : 0;
+
+ var evt = element.ownerDocument.createEvent('MouseEvents');
+ if (evt.initMouseEvent) {
+ evt.initMouseEvent('mousemove', true, true, element.ownerDocument.defaultView,
+ 1, screenX, screenY, clientX, clientY);
+ } else {
+ evt.initEvent('mousemove', true, true);
+ }
+
+ element.dispatchEvent(evt);
+ };
+
+ // Do the initial move to the drag element position
+ triggerMouseEvent(doc.body, start[0], start[1]);
+ triggerMouseEvent(doc.body, dest[0], dest[1]);
+
+ broker.pass({'function':'Controller.mouseMove()'});
+ return true;
+}
+
+/**
+ * Drag an element to the specified offset on another element, firing mouse and
+ * drag events. Adapted from ChromeUtils.js synthesizeDrop()
+ *
+ * @deprecated Use the MozMillElement object
+ *
+ * @param {MozElement} aSrc
+ * Source element to be dragged
+ * @param {MozElement} aDest
+ * Destination element over which the drop occurs
+ * @param {Number} [aOffsetX=element.width/2]
+ * Relative x offset for dropping on the aDest element
+ * @param {Number} [aOffsetY=element.height/2]
+ * Relative y offset for dropping on the aDest element
+ * @param {DOMWindow} [aSourceWindow=this.element.ownerDocument.defaultView]
+ * Custom source Window to be used.
+ * @param {String} [aDropEffect="move"]
+ * Effect used for the drop event
+ * @param {Object[]} [aDragData]
+ * An array holding custom drag data to be used during the drag event
+ * Format: [{ type: "text/plain", "Text to drag"}, ...]
+ *
+ * @returns {String} the captured dropEffect
+ */
+MozMillController.prototype.dragToElement = function (aSrc, aDest, aOffsetX,
+ aOffsetY, aSourceWindow,
+ aDropEffect, aDragData) {
+ logDeprecated("controller.dragToElement", "Use the MozMillElement object.");
+ return aSrc.dragToElement(aDest, aOffsetX, aOffsetY, aSourceWindow, null,
+ aDropEffect, aDragData);
+};
+
+function Tabs(controller) {
+ this.controller = controller;
+}
+
+Tabs.prototype.getTab = function (index) {
+ return this.controller.browserObject.browsers[index].contentDocument;
+}
+
+Tabs.prototype.__defineGetter__("activeTab", function () {
+ return this.controller.browserObject.selectedBrowser.contentDocument;
+});
+
+Tabs.prototype.selectTab = function (index) {
+ // GO in to tab manager and grab the tab by index and call focus.
+}
+
+Tabs.prototype.findWindow = function (doc) {
+ for (var i = 0; i <= (this.controller.window.frames.length - 1); i++) {
+ if (this.controller.window.frames[i].document == doc) {
+ return this.controller.window.frames[i];
+ }
+ }
+
+ throw new Error("Cannot find window for document. Doc title == " + doc.title);
+}
+
+Tabs.prototype.getTabWindow = function (index) {
+ return this.findWindow(this.getTab(index));
+}
+
+Tabs.prototype.__defineGetter__("activeTabWindow", function () {
+ return this.findWindow(this.activeTab);
+});
+
+Tabs.prototype.__defineGetter__("length", function () {
+ return this.controller.browserObject.browsers.length;
+});
+
+Tabs.prototype.__defineGetter__("activeTabIndex", function () {
+ var browser = this.controller.browserObject;
+
+ switch(this.controller.mozmillModule.Application) {
+ case "MetroFirefox":
+ return browser.tabs.indexOf(browser.selectedTab);
+ case "Firefox":
+ default:
+ return browser.tabContainer.selectedIndex;
+ }
+});
+
+Tabs.prototype.selectTabIndex = function (aIndex) {
+ var browser = this.controller.browserObject;
+
+ switch(this.controller.mozmillModule.Application) {
+ case "MetroFirefox":
+ browser.selectedTab = browser.tabs[aIndex];
+ break;
+ case "Firefox":
+ default:
+ browser.selectTabAtIndex(aIndex);
+ }
+}
+
+function browserAdditions (controller) {
+ controller.tabs = new Tabs(controller);
+
+ controller.waitForPageLoad = function (aDocument, aTimeout, aInterval) {
+ var timeout = aTimeout || 30000;
+ var win = null;
+ var timed_out = false;
+
+ // If a user tries to do waitForPageLoad(2000), this will assign the
+ // interval the first arg which is most likely what they were expecting
+ if (typeof(aDocument) == "number"){
+ timeout = aDocument;
+ }
+
+ // If we have a real document use its default view
+ if (aDocument && (typeof(aDocument) === "object") &&
+ "defaultView" in aDocument)
+ win = aDocument.defaultView;
+
+ // If no document has been specified, fallback to the default view of the
+ // currently selected tab browser
+ win = win || this.browserObject.selectedBrowser.contentWindow;
+
+ // Wait until the content in the tab has been loaded
+ try {
+ this.waitFor(function () {
+ return windows.map.hasPageLoaded(utils.getWindowId(win));
+ }, "Timeout", timeout, aInterval);
+ }
+ catch (ex if ex instanceof errors.TimeoutError) {
+ timed_out = true;
+ }
+ finally {
+ state = 'URI=' + win.document.location.href +
+ ', readyState=' + win.document.readyState;
+ message = "controller.waitForPageLoad(" + state + ")";
+
+ if (timed_out) {
+ throw new errors.AssertionError(message);
+ }
+
+ broker.pass({'function': message});
+ }
+ }
+}
+
+var controllerAdditions = {
+ 'navigator:browser' :browserAdditions
+};
+
+/**
+ * DEPRECATION WARNING
+ *
+ * The following methods have all been DEPRECATED as of Mozmill 2.0
+ */
+MozMillController.prototype.assertProperty = function (el, attrib, val) {
+ logDeprecatedAssert("assertProperty");
+
+ return this.assertJSProperty(el, attrib, val);
+};
+
+MozMillController.prototype.assertPropertyNotExist = function (el, attrib) {
+ logDeprecatedAssert("assertPropertyNotExist");
+ return this.assertNotJSProperty(el, attrib);
+};
+
+/**
+ * DEPRECATION WARNING
+ *
+ * The following methods have all been DEPRECATED as of Mozmill 2.0
+ * Use the MozMillElement object instead (https://developer.mozilla.org/en/Mozmill/Mozmill_Element_Object)
+ */
+MozMillController.prototype.select = function (aElement, index, option, value) {
+ logDeprecated("controller.select", "Use the MozMillElement object.");
+
+ return aElement.select(index, option, value);
+};
+
+MozMillController.prototype.keypress = function (aElement, aKey, aModifiers, aExpectedEvent) {
+ logDeprecated("controller.keypress", "Use the MozMillElement object.");
+
+ if (!aElement) {
+ aElement = new mozelement.MozMillElement("Elem", this.window);
+ }
+
+ return aElement.keypress(aKey, aModifiers, aExpectedEvent);
+}
+
+MozMillController.prototype.type = function (aElement, aText, aExpectedEvent) {
+ logDeprecated("controller.type", "Use the MozMillElement object.");
+
+ if (!aElement) {
+ aElement = new mozelement.MozMillElement("Elem", this.window);
+ }
+
+ var that = this;
+ var retval = true;
+ Array.forEach(aText, function (letter) {
+ if (!that.keypress(aElement, letter, {}, aExpectedEvent)) {
+ retval = false; }
+ });
+
+ return retval;
+}
+
+MozMillController.prototype.mouseEvent = function (aElement, aOffsetX, aOffsetY, aEvent, aExpectedEvent) {
+ logDeprecated("controller.mouseEvent", "Use the MozMillElement object.");
+
+ return aElement.mouseEvent(aOffsetX, aOffsetY, aEvent, aExpectedEvent);
+}
+
+MozMillController.prototype.click = function (aElement, left, top, expectedEvent) {
+ logDeprecated("controller.click", "Use the MozMillElement object.");
+
+ return aElement.click(left, top, expectedEvent);
+}
+
+MozMillController.prototype.doubleClick = function (aElement, left, top, expectedEvent) {
+ logDeprecated("controller.doubleClick", "Use the MozMillElement object.");
+
+ return aElement.doubleClick(left, top, expectedEvent);
+}
+
+MozMillController.prototype.mouseDown = function (aElement, button, left, top, expectedEvent) {
+ logDeprecated("controller.mouseDown", "Use the MozMillElement object.");
+
+ return aElement.mouseDown(button, left, top, expectedEvent);
+};
+
+MozMillController.prototype.mouseOut = function (aElement, button, left, top, expectedEvent) {
+ logDeprecated("controller.mouseOut", "Use the MozMillElement object.");
+
+ return aElement.mouseOut(button, left, top, expectedEvent);
+};
+
+MozMillController.prototype.mouseOver = function (aElement, button, left, top, expectedEvent) {
+ logDeprecated("controller.mouseOver", "Use the MozMillElement object.");
+
+ return aElement.mouseOver(button, left, top, expectedEvent);
+};
+
+MozMillController.prototype.mouseUp = function (aElement, button, left, top, expectedEvent) {
+ logDeprecated("controller.mouseUp", "Use the MozMillElement object.");
+
+ return aElement.mouseUp(button, left, top, expectedEvent);
+};
+
+MozMillController.prototype.middleClick = function (aElement, left, top, expectedEvent) {
+ logDeprecated("controller.middleClick", "Use the MozMillElement object.");
+
+ return aElement.middleClick(aElement, left, top, expectedEvent);
+}
+
+MozMillController.prototype.rightClick = function (aElement, left, top, expectedEvent) {
+ logDeprecated("controller.rightClick", "Use the MozMillElement object.");
+
+ return aElement.rightClick(left, top, expectedEvent);
+}
+
+MozMillController.prototype.check = function (aElement, state) {
+ logDeprecated("controller.check", "Use the MozMillElement object.");
+
+ return aElement.check(state);
+}
+
+MozMillController.prototype.radio = function (aElement) {
+ logDeprecated("controller.radio", "Use the MozMillElement object.");
+
+ return aElement.select();
+}
+
+MozMillController.prototype.waitThenClick = function (aElement, timeout, interval) {
+ logDeprecated("controller.waitThenClick", "Use the MozMillElement object.");
+
+ return aElement.waitThenClick(timeout, interval);
+}
+
+MozMillController.prototype.waitForElement = function (aElement, timeout, interval) {
+ logDeprecated("controller.waitForElement", "Use the MozMillElement object.");
+
+ return aElement.waitForElement(timeout, interval);
+}
+
+MozMillController.prototype.waitForElementNotPresent = function (aElement, timeout, interval) {
+ logDeprecated("controller.waitForElementNotPresent", "Use the MozMillElement object.");
+
+ return aElement.waitForElementNotPresent(timeout, interval);
+}
diff --git a/services/sync/tps/extensions/mozmill/resource/modules/elementslib.js b/services/sync/tps/extensions/mozmill/resource/driver/elementslib.js
index e59429f06..f08cf42f3 100644
--- a/services/sync/tps/extensions/mozmill/resource/modules/elementslib.js
+++ b/services/sync/tps/extensions/mozmill/resource/driver/elementslib.js
@@ -1,23 +1,30 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+ * file, you can obtain one at http://mozilla.org/MPL/2.0/. */
-var EXPORTED_SYMBOLS = ["Elem", "ID", "Link", "XPath", "Selector", "Name", "Anon", "AnonXPath",
+var EXPORTED_SYMBOLS = ["ID", "Link", "XPath", "Selector", "Name", "Anon", "AnonXPath",
"Lookup", "_byID", "_byName", "_byAttrib", "_byAnonAttrib",
];
-var utils = {}; Components.utils.import('resource://mozmill/modules/utils.js', utils);
-var strings = {}; Components.utils.import('resource://mozmill/stdlib/strings.js', strings);
-var arrays = {}; Components.utils.import('resource://mozmill/stdlib/arrays.js', arrays);
-var json2 = {}; Components.utils.import('resource://mozmill/stdlib/json2.js', json2);
-var withs = {}; Components.utils.import('resource://mozmill/stdlib/withs.js', withs);
-var dom = {}; Components.utils.import('resource://mozmill/stdlib/dom.js', dom);
-var objects = {}; Components.utils.import('resource://mozmill/stdlib/objects.js', objects);
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cu = Components.utils;
-var countQuotes = function(str){
+Cu.import("resource://gre/modules/Services.jsm");
+
+var utils = {}; Cu.import('resource://mozmill/stdlib/utils.js', utils);
+var strings = {}; Cu.import('resource://mozmill/stdlib/strings.js', strings);
+var arrays = {}; Cu.import('resource://mozmill/stdlib/arrays.js', arrays);
+var json2 = {}; Cu.import('resource://mozmill/stdlib/json2.js', json2);
+var withs = {}; Cu.import('resource://mozmill/stdlib/withs.js', withs);
+var dom = {}; Cu.import('resource://mozmill/stdlib/dom.js', dom);
+var objects = {}; Cu.import('resource://mozmill/stdlib/objects.js', objects);
+
+var countQuotes = function (str) {
var count = 0;
var i = 0;
- while(i < str.length) {
+
+ while (i < str.length) {
i = str.indexOf('"', i);
if (i != -1) {
count++;
@@ -26,6 +33,7 @@ var countQuotes = function(str){
break;
}
}
+
return count;
};
@@ -53,10 +61,12 @@ var smartSplit = function (str) {
var re = /\/([^\/"]*"[^"]*")*[^\/]*/g
var ret = []
var match = re.exec(str);
+
while (match != null) {
ret.push(match[0].replace(/^\//, ""));
match = re.exec(str);
}
+
return ret;
};
@@ -67,9 +77,12 @@ var smartSplit = function (str) {
* if no document is provided
*/
function defaultDocuments() {
- var windowManager = Components.classes['@mozilla.org/appshell/window-mediator;1'].getService(Components.interfaces.nsIWindowMediator);
- win = windowManager.getMostRecentWindow("navigator:browser");
- return [win.gBrowser.selectedBrowser.contentDocument, win.document];
+ var win = Services.wm.getMostRecentWindow("navigator:browser");
+
+ return [
+ win.document,
+ utils.getBrowserObject(win).selectedBrowser.contentWindow.document
+ ];
};
/**
@@ -84,30 +97,37 @@ function nodeSearch(doc, func, string) {
} else {
var documents = defaultDocuments();
}
+
var e = null;
var element = null;
+
//inline function to recursively find the element in the DOM, cross frame.
- var search = function(win, func, string) {
- if (win == null)
+ var search = function (win, func, string) {
+ if (win == null) {
return;
+ }
//do the lookup in the current window
element = func.call(win, string);
if (!element || (element.length == 0)) {
var frames = win.frames;
- for (var i=0; i < frames.length; i++) {
+ for (var i = 0; i < frames.length; i++) {
search(frames[i], func, string);
}
+ } else {
+ e = element;
}
- else { e = element; }
};
for (var i = 0; i < documents.length; ++i) {
var win = documents[i].defaultView;
search(win, func, string);
- if (e) break;
+ if (e) {
+ break;
+ }
}
+
return e;
};
@@ -120,11 +140,15 @@ function Selector(_document, selector, index) {
if (selector == undefined) {
throw new Error('Selector constructor did not recieve enough arguments.');
}
+
this.selector = selector;
+
this.getNodeForDocument = function (s) {
return this.document.querySelectorAll(s);
};
+
var nodes = nodeSearch(_document, this.getNodeForDocument, this.selector);
+
return nodes ? nodes[index || 0] : null;
};
@@ -137,9 +161,11 @@ function ID(_document, nodeID) {
if (nodeID == undefined) {
throw new Error('ID constructor did not recieve enough arguments.');
}
+
this.getNodeForDocument = function (nodeID) {
return this.document.getElementById(nodeID);
};
+
return nodeSearch(_document, this.getNodeForDocument, nodeID);
};
@@ -154,40 +180,48 @@ function Link(_document, linkName) {
}
this.getNodeForDocument = function (linkName) {
- var getText = function(el){
+ var getText = function (el) {
var text = "";
- if (el.nodeType == 3){ //textNode
- if (el.data != undefined){
+
+ if (el.nodeType == 3) { //textNode
+ if (el.data != undefined) {
text = el.data;
} else {
text = el.innerHTML;
}
- text = text.replace(/n|r|t/g, " ");
+
+ text = text.replace(/n|r|t/g, " ");
}
- if (el.nodeType == 1){ //elementNode
+ else if (el.nodeType == 1) { //elementNode
for (var i = 0; i < el.childNodes.length; i++) {
var child = el.childNodes.item(i);
text += getText(child);
}
- if (el.tagName == "P" || el.tagName == "BR" || el.tagName == "HR" || el.tagName == "DIV") {
- text += "n";
+
+ if (el.tagName == "P" || el.tagName == "BR" ||
+ el.tagName == "HR" || el.tagName == "DIV") {
+ text += "\n";
}
}
+
return text;
};
//sometimes the windows won't have this function
try {
- var links = this.document.getElementsByTagName('a'); }
- catch(err){ // ADD LOG LINE mresults.write('Error: '+ err, 'lightred');
+ var links = this.document.getElementsByTagName('a');
+ } catch (e) {
+ // ADD LOG LINE mresults.write('Error: '+ e, 'lightred');
}
+
for (var i = 0; i < links.length; i++) {
var el = links[i];
//if (getText(el).indexOf(this.linkName) != -1) {
- if (el.innerHTML.indexOf(linkName) != -1){
+ if (el.innerHTML.indexOf(linkName) != -1) {
return el;
}
}
+
return null;
};
@@ -214,14 +248,20 @@ function XPath(_document, expr) {
} else {
xpe = new this.document.defaultView.XPathEvaluator();
}
- var nsResolver = xpe.createNSResolver(aNode.ownerDocument == null ? aNode.documentElement : aNode.ownerDocument.documentElement);
+
+ var nsResolver = xpe.createNSResolver(aNode.ownerDocument == null ? aNode.documentElement
+ : aNode.ownerDocument.documentElement);
var result = xpe.evaluate(aExpr, aNode, nsResolver, 0, null);
var found = [];
var res;
- while (res = result.iterateNext())
+
+ while (res = result.iterateNext()) {
found.push(res);
+ }
+
return found[0];
};
+
return nodeSearch(_document, this.getNodeForDocument, expr);
};
@@ -234,14 +274,19 @@ function Name(_document, nName) {
if (nName == undefined) {
throw new Error('Name constructor did not recieve enough arguments.');
}
+
this.getNodeForDocument = function (s) {
try{
var els = this.document.getElementsByName(s);
- if (els.length > 0) { return els[0]; }
+ if (els.length > 0) {
+ return els[0];
+ }
+ } catch (e) {
}
- catch(err){};
+
return null;
};
+
return nodeSearch(_document, this.getNodeForDocument, nName);
};
@@ -249,110 +294,138 @@ function Name(_document, nName) {
var _returnResult = function (results) {
if (results.length == 0) {
return null
- } else if (results.length == 1) {
+ }
+ else if (results.length == 1) {
return results[0];
} else {
return results;
}
}
+
var _forChildren = function (element, name, value) {
var results = [];
var nodes = [e for each (e in element.childNodes) if (e)]
+
for (var i in nodes) {
var n = nodes[i];
if (n[name] == value) {
results.push(n);
}
}
+
return results;
}
+
var _forAnonChildren = function (_document, element, name, value) {
var results = [];
var nodes = [e for each (e in _document.getAnoymousNodes(element)) if (e)];
+
for (var i in nodes ) {
var n = nodes[i];
if (n[name] == value) {
results.push(n);
}
}
+
return results;
}
+
var _byID = function (_document, parent, value) {
return _returnResult(_forChildren(parent, 'id', value));
}
+
var _byName = function (_document, parent, value) {
return _returnResult(_forChildren(parent, 'tagName', value));
}
+
var _byAttrib = function (parent, attributes) {
var results = [];
-
var nodes = parent.childNodes;
+
for (var i in nodes) {
var n = nodes[i];
requirementPass = 0;
requirementLength = 0;
+
for (var a in attributes) {
requirementLength++;
try {
if (n.getAttribute(a) == attributes[a]) {
requirementPass++;
}
- } catch (err) {
+ } catch (e) {
// Workaround any bugs in custom attribute crap in XUL elements
}
}
+
if (requirementPass == requirementLength) {
results.push(n);
}
}
+
return _returnResult(results)
}
+
var _byAnonAttrib = function (_document, parent, attributes) {
var results = [];
if (objects.getLength(attributes) == 1) {
- for (var i in attributes) {var k = i; var v = attributes[i]; }
- var result = _document.getAnonymousElementByAttribute(parent, k, v)
+ for (var i in attributes) {
+ var k = i;
+ var v = attributes[i];
+ }
+
+ var result = _document.getAnonymousElementByAttribute(parent, k, v);
if (result) {
return result;
-
}
}
+
var nodes = [n for each (n in _document.getAnonymousNodes(parent)) if (n.getAttribute)];
+
function resultsForNodes (nodes) {
for (var i in nodes) {
var n = nodes[i];
requirementPass = 0;
requirementLength = 0;
+
for (var a in attributes) {
requirementLength++;
if (n.getAttribute(a) == attributes[a]) {
requirementPass++;
}
}
+
if (requirementPass == requirementLength) {
results.push(n);
}
}
}
- resultsForNodes(nodes)
+
+ resultsForNodes(nodes);
if (results.length == 0) {
resultsForNodes([n for each (n in parent.childNodes) if (n != undefined && n.getAttribute)])
}
+
return _returnResult(results)
}
+
var _byIndex = function (_document, parent, i) {
if (parent instanceof Array) {
return parent[i];
}
+
return parent.childNodes[i];
}
+
var _anonByName = function (_document, parent, value) {
return _returnResult(_forAnonChildren(_document, parent, 'tagName', value));
}
+
var _anonByAttrib = function (_document, parent, value) {
return _byAnonAttrib(_document, parent, value);
}
+
var _anonByIndex = function (_document, parent, i) {
return _document.getAnonymousNodes(parent)[i];
}
@@ -362,18 +435,32 @@ var _anonByIndex = function (_document, parent, i) {
*
* Finds an element by Lookup expression
*/
-function Lookup (_document, expression) {
+function Lookup(_document, expression) {
if (expression == undefined) {
throw new Error('Lookup constructor did not recieve enough arguments.');
}
var expSplit = [e for each (e in smartSplit(expression) ) if (e != '')];
- expSplit.unshift(_document)
+ expSplit.unshift(_document);
+
var nCases = {'id':_byID, 'name':_byName, 'attrib':_byAttrib, 'index':_byIndex};
var aCases = {'name':_anonByName, 'attrib':_anonByAttrib, 'index':_anonByIndex};
+ /**
+ * Reduces the lookup expression
+ * @param {Object} parentNode
+ * Parent node (previousValue of the formerly executed reduce callback)
+ * @param {String} exp
+ * Lookup expression for the parents child node
+ *
+ * @returns {Object} Node found by the given expression
+ */
+ var reduceLookup = function (parentNode, exp) {
+ // Abort in case the parent node was not found
+ if (!parentNode) {
+ return false;
+ }
- var reduceLookup = function (parent, exp) {
// Handle case where only index is provided
var cases = nCases;
@@ -381,21 +468,27 @@ function Lookup (_document, expression) {
if (withs.endsWith(exp, ']')) {
var expIndex = json2.JSON.parse(strings.vslice(exp, '[', ']'));
}
+
// Handle anon
if (withs.startsWith(exp, 'anon')) {
- var exp = strings.vslice(exp, '(', ')');
- var cases = aCases;
+ exp = strings.vslice(exp, '(', ')');
+ cases = aCases;
}
+
if (withs.startsWith(exp, '[')) {
try {
var obj = json2.JSON.parse(strings.vslice(exp, '[', ']'));
- } catch (err) {
- throw new Error(err+'. String to be parsed was || '+strings.vslice(exp, '[', ']')+' ||');
+ } catch (e) {
+ throw new SyntaxError(e + '. String to be parsed was || ' +
+ strings.vslice(exp, '[', ']') + ' ||');
}
- var r = cases['index'](_document, parent, obj);
+
+ var r = cases['index'](_document, parentNode, obj);
if (r == null) {
- throw new Error('Expression "'+exp+'" returned null. Anonymous == '+(cases == aCases));
+ throw new SyntaxError('Expression "' + exp +
+ '" returned null. Anonymous == ' + (cases == aCases));
}
+
return r;
}
@@ -403,30 +496,28 @@ function Lookup (_document, expression) {
if (withs.startsWith(exp, c)) {
try {
var obj = json2.JSON.parse(strings.vslice(exp, '(', ')'))
- } catch(err) {
- throw new Error(err+'. String to be parsed was || '+strings.vslice(exp, '(', ')')+' ||');
+ } catch (e) {
+ throw new SyntaxError(e + '. String to be parsed was || ' +
+ strings.vslice(exp, '(', ')') + ' ||');
}
- var result = cases[c](_document, parent, obj);
+ var result = cases[c](_document, parentNode, obj);
}
}
if (!result) {
- if ( withs.startsWith(exp, '{') ) {
+ if (withs.startsWith(exp, '{')) {
try {
- var obj = json2.JSON.parse(exp)
- } catch(err) {
- throw new Error(err+'. String to be parsed was || '+exp+' ||');
+ var obj = json2.JSON.parse(exp);
+ } catch (e) {
+ throw new SyntaxError(e + '. String to be parsed was || ' + exp + ' ||');
}
if (cases == aCases) {
- var result = _anonByAttrib(_document, parent, obj)
+ var result = _anonByAttrib(_document, parentNode, obj);
} else {
- var result = _byAttrib(parent, obj)
+ var result = _byAttrib(parentNode, obj);
}
}
- if (!result) {
- throw new Error('Expression "'+exp+'" returned null. Anonymous == '+(cases == aCases));
- }
}
// Final return
@@ -437,8 +528,10 @@ function Lookup (_document, expression) {
// TODO: Check length and raise error
return result;
}
+
// Maybe we should cause an exception here
return false;
};
+
return expSplit.reduce(reduceLookup);
};
diff --git a/services/sync/tps/extensions/mozmill/resource/driver/mozelement.js b/services/sync/tps/extensions/mozmill/resource/driver/mozelement.js
new file mode 100644
index 000000000..0af204794
--- /dev/null
+++ b/services/sync/tps/extensions/mozmill/resource/driver/mozelement.js
@@ -0,0 +1,1163 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, you can obtain one at http://mozilla.org/MPL/2.0/. */
+
+var EXPORTED_SYMBOLS = ["Elem", "Selector", "ID", "Link", "XPath", "Name", "Lookup",
+ "MozMillElement", "MozMillCheckBox", "MozMillRadio", "MozMillDropList",
+ "MozMillTextBox", "subclasses"
+ ];
+
+const NAMESPACE_XUL = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cu = Components.utils;
+
+var EventUtils = {}; Cu.import('resource://mozmill/stdlib/EventUtils.js', EventUtils);
+
+var assertions = {}; Cu.import('resource://mozmill/modules/assertions.js', assertions);
+var broker = {}; Cu.import('resource://mozmill/driver/msgbroker.js', broker);
+var elementslib = {}; Cu.import('resource://mozmill/driver/elementslib.js', elementslib);
+var utils = {}; Cu.import('resource://mozmill/stdlib/utils.js', utils);
+
+var assert = new assertions.Assert();
+
+// A list of all the subclasses available. Shared modules can push their own subclasses onto this list
+var subclasses = [MozMillCheckBox, MozMillRadio, MozMillDropList, MozMillTextBox];
+
+/**
+ * createInstance()
+ *
+ * Returns an new instance of a MozMillElement
+ * The type of the element is automatically determined
+ */
+function createInstance(locatorType, locator, elem, document) {
+ var args = { "document": document, "element": elem };
+
+ // If we already have an element lets determine the best MozMillElement type
+ if (elem) {
+ for (var i = 0; i < subclasses.length; ++i) {
+ if (subclasses[i].isType(elem)) {
+ return new subclasses[i](locatorType, locator, args);
+ }
+ }
+ }
+
+ // By default we create a base MozMillElement
+ if (MozMillElement.isType(elem)) {
+ return new MozMillElement(locatorType, locator, args);
+ }
+
+ throw new Error("Unsupported element type " + locatorType + ": " + locator);
+}
+
+var Elem = function (node) {
+ return createInstance("Elem", node, node);
+};
+
+var Selector = function (document, selector, index) {
+ return createInstance("Selector", selector, elementslib.Selector(document, selector, index), document);
+};
+
+var ID = function (document, nodeID) {
+ return createInstance("ID", nodeID, elementslib.ID(document, nodeID), document);
+};
+
+var Link = function (document, linkName) {
+ return createInstance("Link", linkName, elementslib.Link(document, linkName), document);
+};
+
+var XPath = function (document, expr) {
+ return createInstance("XPath", expr, elementslib.XPath(document, expr), document);
+};
+
+var Name = function (document, nName) {
+ return createInstance("Name", nName, elementslib.Name(document, nName), document);
+};
+
+var Lookup = function (document, expression) {
+ var elem = createInstance("Lookup", expression, elementslib.Lookup(document, expression), document);
+
+ // Bug 864268 - Expose the expression property to maintain backwards compatibility
+ elem.expression = elem._locator;
+
+ return elem;
+};
+
+/**
+ * MozMillElement
+ * The base class for all mozmill elements
+ */
+function MozMillElement(locatorType, locator, args) {
+ args = args || {};
+ this._locatorType = locatorType;
+ this._locator = locator;
+ this._element = args["element"];
+ this._owner = args["owner"];
+
+ this._document = this._element ? this._element.ownerDocument : args["document"];
+ this._defaultView = this._document ? this._document.defaultView : null;
+
+ // Used to maintain backwards compatibility with controller.js
+ this.isElement = true;
+}
+
+// Static method that returns true if node is of this element type
+MozMillElement.isType = function (node) {
+ return true;
+};
+
+// This getter is the magic behind lazy loading (note distinction between _element and element)
+MozMillElement.prototype.__defineGetter__("element", function () {
+ // If the document is invalid (e.g. reload of the page), invalidate the cached
+ // element and update the document cache
+ if (this._defaultView && this._defaultView.document !== this._document) {
+ this._document = this._defaultView.document;
+ this._element = undefined;
+ }
+
+ if (this._element == undefined) {
+ if (elementslib[this._locatorType]) {
+ this._element = elementslib[this._locatorType](this._document, this._locator);
+ } else if (this._locatorType == "Elem") {
+ this._element = this._locator;
+ } else {
+ throw new Error("Unknown locator type: " + this._locatorType);
+ }
+ }
+
+ return this._element;
+});
+
+/**
+ * Drag an element to the specified offset on another element, firing mouse and
+ * drag events. Adapted from ChromeUtils.js synthesizeDrop()
+ *
+ * By default it will drag the source element over the destination's element
+ * center with a "move" dropEffect.
+ *
+ * @param {MozElement} aElement
+ * Destination element over which the drop occurs
+ * @param {Number} [aOffsetX=aElement.width/2]
+ * Relative x offset for dropping on aElement
+ * @param {Number} [aOffsetY=aElement.height/2]
+ * Relative y offset for dropping on aElement
+ * @param {DOMWindow} [aSourceWindow=this.element.ownerDocument.defaultView]
+ * Custom source Window to be used.
+ * @param {DOMWindow} [aDestWindow=aElement.getNode().ownerDocument.defaultView]
+ * Custom destination Window to be used.
+ * @param {String} [aDropEffect="move"]
+ * Possible values: copy, move, link, none
+ * @param {Object[]} [aDragData]
+ * An array holding custom drag data to be used during the drag event
+ * Format: [{ type: "text/plain", "Text to drag"}, ...]
+ *
+ * @returns {String} the captured dropEffect
+ */
+MozMillElement.prototype.dragToElement = function(aElement, aOffsetX, aOffsetY,
+ aSourceWindow, aDestWindow,
+ aDropEffect, aDragData) {
+ if (!this.element) {
+ throw new Error("Could not find element " + this.getInfo());
+ }
+ if (!aElement) {
+ throw new Error("Missing destination element");
+ }
+
+ var srcNode = this.element;
+ var destNode = aElement.getNode();
+ var srcWindow = aSourceWindow ||
+ (srcNode.ownerDocument ? srcNode.ownerDocument.defaultView
+ : srcNode);
+ var destWindow = aDestWindow ||
+ (destNode.ownerDocument ? destNode.ownerDocument.defaultView
+ : destNode);
+
+ var srcRect = srcNode.getBoundingClientRect();
+ var srcCoords = {
+ x: srcRect.width / 2,
+ y: srcRect.height / 2
+ };
+ var destRect = destNode.getBoundingClientRect();
+ var destCoords = {
+ x: (!aOffsetX || isNaN(aOffsetX)) ? (destRect.width / 2) : aOffsetX,
+ y: (!aOffsetY || isNaN(aOffsetY)) ? (destRect.height / 2) : aOffsetY
+ };
+
+ var windowUtils = destWindow.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindowUtils);
+ var ds = Cc["@mozilla.org/widget/dragservice;1"].getService(Ci.nsIDragService);
+
+ var dataTransfer;
+ var trapDrag = function (event) {
+ srcWindow.removeEventListener("dragstart", trapDrag, true);
+ dataTransfer = event.dataTransfer;
+
+ if (!aDragData) {
+ return;
+ }
+
+ for (var i = 0; i < aDragData.length; i++) {
+ var item = aDragData[i];
+ for (var j = 0; j < item.length; j++) {
+ dataTransfer.mozSetDataAt(item[j].type, item[j].data, i);
+ }
+ }
+
+ dataTransfer.dropEffect = aDropEffect || "move";
+ event.preventDefault();
+ event.stopPropagation();
+ }
+
+ ds.startDragSession();
+
+ try {
+ srcWindow.addEventListener("dragstart", trapDrag, true);
+ EventUtils.synthesizeMouse(srcNode, srcCoords.x, srcCoords.y,
+ { type: "mousedown" }, srcWindow);
+ EventUtils.synthesizeMouse(destNode, destCoords.x, destCoords.y,
+ { type: "mousemove" }, destWindow);
+
+ var event = destWindow.document.createEvent("DragEvents");
+ event.initDragEvent("dragenter", true, true, destWindow, 0, 0, 0, 0, 0,
+ false, false, false, false, 0, null, dataTransfer);
+ event.initDragEvent("dragover", true, true, destWindow, 0, 0, 0, 0, 0,
+ false, false, false, false, 0, null, dataTransfer);
+ event.initDragEvent("drop", true, true, destWindow, 0, 0, 0, 0, 0,
+ false, false, false, false, 0, null, dataTransfer);
+ windowUtils.dispatchDOMEventViaPresShell(destNode, event, true);
+
+ EventUtils.synthesizeMouse(destNode, destCoords.x, destCoords.y,
+ { type: "mouseup" }, destWindow);
+
+ return dataTransfer.dropEffect;
+ } finally {
+ ds.endDragSession(true);
+ }
+
+};
+
+// Returns the actual wrapped DOM node
+MozMillElement.prototype.getNode = function () {
+ return this.element;
+};
+
+MozMillElement.prototype.getInfo = function () {
+ return this._locatorType + ": " + this._locator;
+};
+
+/**
+ * Sometimes an element which once existed will no longer exist in the DOM
+ * This function re-searches for the element
+ */
+MozMillElement.prototype.exists = function () {
+ this._element = undefined;
+ if (this.element) {
+ return true;
+ }
+
+ return false;
+};
+
+/**
+ * Synthesize a keypress event on the given element
+ *
+ * @param {string} aKey
+ * Key to use for synthesizing the keypress event. It can be a simple
+ * character like "k" or a string like "VK_ESCAPE" for command keys
+ * @param {object} aModifiers
+ * Information about the modifier keys to send
+ * Elements: accelKey - Hold down the accelerator key (ctrl/meta)
+ * [optional - default: false]
+ * altKey - Hold down the alt key
+ * [optional - default: false]
+ * ctrlKey - Hold down the ctrl key
+ * [optional - default: false]
+ * metaKey - Hold down the meta key (command key on Mac)
+ * [optional - default: false]
+ * shiftKey - Hold down the shift key
+ * [optional - default: false]
+ * @param {object} aExpectedEvent
+ * Information about the expected event to occur
+ * Elements: target - Element which should receive the event
+ * [optional - default: current element]
+ * type - Type of the expected key event
+ */
+MozMillElement.prototype.keypress = function (aKey, aModifiers, aExpectedEvent) {
+ if (!this.element) {
+ throw new Error("Could not find element " + this.getInfo());
+ }
+
+ var win = this.element.ownerDocument ? this.element.ownerDocument.defaultView
+ : this.element;
+ this.element.focus();
+
+ if (aExpectedEvent) {
+ if (!aExpectedEvent.type) {
+ throw new Error(arguments.callee.name + ": Expected event type not specified");
+ }
+
+ var target = aExpectedEvent.target ? aExpectedEvent.target.getNode()
+ : this.element;
+ EventUtils.synthesizeKeyExpectEvent(aKey, aModifiers || {}, target, aExpectedEvent.type,
+ "MozMillElement.keypress()", win);
+ } else {
+ EventUtils.synthesizeKey(aKey, aModifiers || {}, win);
+ }
+
+ broker.pass({'function':'MozMillElement.keypress()'});
+
+ return true;
+};
+
+
+/**
+ * Synthesize a general mouse event on the given element
+ *
+ * @param {number} aOffsetX
+ * Relative x offset in the elements bounds to click on
+ * @param {number} aOffsetY
+ * Relative y offset in the elements bounds to click on
+ * @param {object} aEvent
+ * Information about the event to send
+ * Elements: accelKey - Hold down the accelerator key (ctrl/meta)
+ * [optional - default: false]
+ * altKey - Hold down the alt key
+ * [optional - default: false]
+ * button - Mouse button to use
+ * [optional - default: 0]
+ * clickCount - Number of counts to click
+ * [optional - default: 1]
+ * ctrlKey - Hold down the ctrl key
+ * [optional - default: false]
+ * metaKey - Hold down the meta key (command key on Mac)
+ * [optional - default: false]
+ * shiftKey - Hold down the shift key
+ * [optional - default: false]
+ * type - Type of the mouse event ('click', 'mousedown',
+ * 'mouseup', 'mouseover', 'mouseout')
+ * [optional - default: 'mousedown' + 'mouseup']
+ * @param {object} aExpectedEvent
+ * Information about the expected event to occur
+ * Elements: target - Element which should receive the event
+ * [optional - default: current element]
+ * type - Type of the expected mouse event
+ */
+MozMillElement.prototype.mouseEvent = function (aOffsetX, aOffsetY, aEvent, aExpectedEvent) {
+ if (!this.element) {
+ throw new Error(arguments.callee.name + ": could not find element " + this.getInfo());
+ }
+
+ if ("document" in this.element) {
+ throw new Error("A window cannot be a target for mouse events.");
+ }
+
+ var rect = this.element.getBoundingClientRect();
+
+ if (!aOffsetX || isNaN(aOffsetX)) {
+ aOffsetX = rect.width / 2;
+ }
+
+ if (!aOffsetY || isNaN(aOffsetY)) {
+ aOffsetY = rect.height / 2;
+ }
+
+ // Scroll element into view otherwise the click will fail
+ if ("scrollIntoView" in this.element)
+ this.element.scrollIntoView();
+
+ if (aExpectedEvent) {
+ // The expected event type has to be set
+ if (!aExpectedEvent.type) {
+ throw new Error(arguments.callee.name + ": Expected event type not specified");
+ }
+
+ // If no target has been specified use the specified element
+ var target = aExpectedEvent.target ? aExpectedEvent.target.getNode()
+ : this.element;
+ if (!target) {
+ throw new Error(arguments.callee.name + ": could not find element " +
+ aExpectedEvent.target.getInfo());
+ }
+
+ EventUtils.synthesizeMouseExpectEvent(this.element, aOffsetX, aOffsetY, aEvent,
+ target, aExpectedEvent.type,
+ "MozMillElement.mouseEvent()",
+ this.element.ownerDocument.defaultView);
+ } else {
+ EventUtils.synthesizeMouse(this.element, aOffsetX, aOffsetY, aEvent,
+ this.element.ownerDocument.defaultView);
+ }
+
+ // Bug 555347
+ // We don't know why this sleep is necessary but more investigation is needed
+ // before it can be removed
+ utils.sleep(0);
+
+ return true;
+};
+
+/**
+ * Synthesize a mouse click event on the given element
+ */
+MozMillElement.prototype.click = function (aOffsetX, aOffsetY, aExpectedEvent) {
+ // Handle menu items differently
+ if (this.element && this.element.tagName == "menuitem") {
+ this.element.click();
+ } else {
+ this.mouseEvent(aOffsetX, aOffsetY, {}, aExpectedEvent);
+ }
+
+ broker.pass({'function':'MozMillElement.click()'});
+
+ return true;
+};
+
+/**
+ * Synthesize a double click on the given element
+ */
+MozMillElement.prototype.doubleClick = function (aOffsetX, aOffsetY, aExpectedEvent) {
+ this.mouseEvent(aOffsetX, aOffsetY, {clickCount: 2}, aExpectedEvent);
+
+ broker.pass({'function':'MozMillElement.doubleClick()'});
+
+ return true;
+};
+
+/**
+ * Synthesize a mouse down event on the given element
+ */
+MozMillElement.prototype.mouseDown = function (aButton, aOffsetX, aOffsetY, aExpectedEvent) {
+ this.mouseEvent(aOffsetX, aOffsetY, {button: aButton, type: "mousedown"}, aExpectedEvent);
+
+ broker.pass({'function':'MozMillElement.mouseDown()'});
+
+ return true;
+};
+
+/**
+ * Synthesize a mouse out event on the given element
+ */
+MozMillElement.prototype.mouseOut = function (aButton, aOffsetX, aOffsetY, aExpectedEvent) {
+ this.mouseEvent(aOffsetX, aOffsetY, {button: aButton, type: "mouseout"}, aExpectedEvent);
+
+ broker.pass({'function':'MozMillElement.mouseOut()'});
+
+ return true;
+};
+
+/**
+ * Synthesize a mouse over event on the given element
+ */
+MozMillElement.prototype.mouseOver = function (aButton, aOffsetX, aOffsetY, aExpectedEvent) {
+ this.mouseEvent(aOffsetX, aOffsetY, {button: aButton, type: "mouseover"}, aExpectedEvent);
+
+ broker.pass({'function':'MozMillElement.mouseOver()'});
+
+ return true;
+};
+
+/**
+ * Synthesize a mouse up event on the given element
+ */
+MozMillElement.prototype.mouseUp = function (aButton, aOffsetX, aOffsetY, aExpectedEvent) {
+ this.mouseEvent(aOffsetX, aOffsetY, {button: aButton, type: "mouseup"}, aExpectedEvent);
+
+ broker.pass({'function':'MozMillElement.mouseUp()'});
+
+ return true;
+};
+
+/**
+ * Synthesize a mouse middle click event on the given element
+ */
+MozMillElement.prototype.middleClick = function (aOffsetX, aOffsetY, aExpectedEvent) {
+ this.mouseEvent(aOffsetX, aOffsetY, {button: 1}, aExpectedEvent);
+
+ broker.pass({'function':'MozMillElement.middleClick()'});
+
+ return true;
+};
+
+/**
+ * Synthesize a mouse right click event on the given element
+ */
+MozMillElement.prototype.rightClick = function (aOffsetX, aOffsetY, aExpectedEvent) {
+ this.mouseEvent(aOffsetX, aOffsetY, {type : "contextmenu", button: 2 }, aExpectedEvent);
+
+ broker.pass({'function':'MozMillElement.rightClick()'});
+
+ return true;
+};
+
+/**
+ * Synthesize a general touch event on the given element
+ *
+ * @param {Number} [aOffsetX=aElement.width / 2]
+ * Relative x offset in the elements bounds to click on
+ * @param {Number} [aOffsetY=aElement.height / 2]
+ * Relative y offset in the elements bounds to click on
+ * @param {Object} [aEvent]
+ * Information about the event to send
+ * @param {Boolean} [aEvent.altKey=false]
+ * A Boolean value indicating whether or not the alt key was down when
+ * the touch event was fired
+ * @param {Number} [aEvent.angle=0]
+ * The angle (in degrees) that the ellipse described by rx and
+ * ry must be rotated, clockwise, to most accurately cover the area
+ * of contact between the user and the surface.
+ * @param {Touch[]} [aEvent.changedTouches]
+ * A TouchList of all the Touch objects representing individual points of
+ * contact whose states changed between the previous touch event and
+ * this one
+ * @param {Boolean} [aEvent.ctrlKey]
+ * A Boolean value indicating whether or not the control key was down
+ * when the touch event was fired
+ * @param {Number} [aEvent.force=1]
+ * The amount of pressure being applied to the surface by the user, as a
+ * float between 0.0 (no pressure) and 1.0 (maximum pressure)
+ * @param {Number} [aEvent.id=0]
+ * A unique identifier for this Touch object. A given touch (say, by a
+ * finger) will have the same identifier for the duration of its movement
+ * around the surface. This lets you ensure that you're tracking the same
+ * touch all the time
+ * @param {Boolean} [aEvent.metaKey]
+ * A Boolean value indicating whether or not the meta key was down when
+ * the touch event was fired.
+ * @param {Number} [aEvent.rx=1]
+ * The X radius of the ellipse that most closely circumscribes the area
+ * of contact with the screen.
+ * @param {Number} [aEvent.ry=1]
+ * The Y radius of the ellipse that most closely circumscribes the area
+ * of contact with the screen.
+ * @param {Boolean} [aEvent.shiftKey]
+ * A Boolean value indicating whether or not the shift key was down when
+ * the touch event was fired
+ * @param {Touch[]} [aEvent.targetTouches]
+ * A TouchList of all the Touch objects that are both currently in
+ * contact with the touch surface and were also started on the same
+ * element that is the target of the event
+ * @param {Touch[]} [aEvent.touches]
+ * A TouchList of all the Touch objects representing all current points
+ * of contact with the surface, regardless of target or changed status
+ * @param {Number} [aEvent.type=*|touchstart|touchend|touchmove|touchenter|touchleave|touchcancel]
+ * The type of touch event that occurred
+ * @param {Element} [aEvent.target]
+ * The target of the touches associated with this event. This target
+ * corresponds to the target of all the touches in the targetTouches
+ * attribute, but note that other touches in this event may have a
+ * different target. To be careful, you should use the target associated
+ * with individual touches
+ */
+MozMillElement.prototype.touchEvent = function (aOffsetX, aOffsetY, aEvent) {
+ if (!this.element) {
+ throw new Error(arguments.callee.name + ": could not find element " + this.getInfo());
+ }
+
+ if ("document" in this.element) {
+ throw new Error("A window cannot be a target for touch events.");
+ }
+
+ var rect = this.element.getBoundingClientRect();
+
+ if (!aOffsetX || isNaN(aOffsetX)) {
+ aOffsetX = rect.width / 2;
+ }
+
+ if (!aOffsetY || isNaN(aOffsetY)) {
+ aOffsetY = rect.height / 2;
+ }
+
+ // Scroll element into view otherwise the click will fail
+ if ("scrollIntoView" in this.element) {
+ this.element.scrollIntoView();
+ }
+
+ EventUtils.synthesizeTouch(this.element, aOffsetX, aOffsetY, aEvent,
+ this.element.ownerDocument.defaultView);
+
+ return true;
+};
+
+/**
+ * Synthesize a touch tap event on the given element
+ *
+ * @param {Number} [aOffsetX=aElement.width / 2]
+ * Left offset in px where the event is triggered
+ * @param {Number} [aOffsetY=aElement.height / 2]
+ * Top offset in px where the event is triggered
+ * @param {Object} [aExpectedEvent]
+ * Information about the expected event to occur
+ * @param {MozMillElement} [aExpectedEvent.target=this.element]
+ * Element which should receive the event
+ * @param {MozMillElement} [aExpectedEvent.type]
+ * Type of the expected mouse event
+ */
+MozMillElement.prototype.tap = function (aOffsetX, aOffsetY, aExpectedEvent) {
+ this.mouseEvent(aOffsetX, aOffsetY, {
+ clickCount: 1,
+ inputSource: Ci.nsIDOMMouseEvent.MOZ_SOURCE_TOUCH
+ }, aExpectedEvent);
+
+ broker.pass({'function':'MozMillElement.tap()'});
+
+ return true;
+};
+
+/**
+ * Synthesize a double tap on the given element
+ *
+ * @param {Number} [aOffsetX=aElement.width / 2]
+ * Left offset in px where the event is triggered
+ * @param {Number} [aOffsetY=aElement.height / 2]
+ * Top offset in px where the event is triggered
+ * @param {Object} [aExpectedEvent]
+ * Information about the expected event to occur
+ * @param {MozMillElement} [aExpectedEvent.target=this.element]
+ * Element which should receive the event
+ * @param {MozMillElement} [aExpectedEvent.type]
+ * Type of the expected mouse event
+ */
+MozMillElement.prototype.doubleTap = function (aOffsetX, aOffsetY, aExpectedEvent) {
+ this.mouseEvent(aOffsetX, aOffsetY, {
+ clickCount: 2,
+ inputSource: Ci.nsIDOMMouseEvent.MOZ_SOURCE_TOUCH
+ }, aExpectedEvent);
+
+ broker.pass({'function':'MozMillElement.doubleTap()'});
+
+ return true;
+};
+
+/**
+ * Synthesize a long press
+ *
+ * @param {Number} aOffsetX
+ * Left offset in px where the event is triggered
+ * @param {Number} aOffsetY
+ * Top offset in px where the event is triggered
+ * @param {Number} [aTime=1000]
+ * Duration of the "press" event in ms
+ */
+MozMillElement.prototype.longPress = function (aOffsetX, aOffsetY, aTime) {
+ var time = aTime || 1000;
+
+ this.touchStart(aOffsetX, aOffsetY);
+ utils.sleep(time);
+ this.touchEnd(aOffsetX, aOffsetY);
+
+ broker.pass({'function':'MozMillElement.longPress()'});
+
+ return true;
+};
+
+/**
+ * Synthesize a touch & drag event on the given element
+ *
+ * @param {Number} aOffsetX1
+ * Left offset of the start position
+ * @param {Number} aOffsetY1
+ * Top offset of the start position
+ * @param {Number} aOffsetX2
+ * Left offset of the end position
+ * @param {Number} aOffsetY2
+ * Top offset of the end position
+ */
+MozMillElement.prototype.touchDrag = function (aOffsetX1, aOffsetY1, aOffsetX2, aOffsetY2) {
+ this.touchStart(aOffsetX1, aOffsetY1);
+ this.touchMove(aOffsetX2, aOffsetY2);
+ this.touchEnd(aOffsetX2, aOffsetY2);
+
+ broker.pass({'function':'MozMillElement.move()'});
+
+ return true;
+};
+
+/**
+ * Synthesize a press / touchstart event on the given element
+ *
+ * @param {Number} aOffsetX
+ * Left offset where the event is triggered
+ * @param {Number} aOffsetY
+ * Top offset where the event is triggered
+ */
+MozMillElement.prototype.touchStart = function (aOffsetX, aOffsetY) {
+ this.touchEvent(aOffsetX, aOffsetY, { type: "touchstart" });
+
+ broker.pass({'function':'MozMillElement.touchStart()'});
+
+ return true;
+};
+
+/**
+ * Synthesize a release / touchend event on the given element
+ *
+ * @param {Number} aOffsetX
+ * Left offset where the event is triggered
+ * @param {Number} aOffsetY
+ * Top offset where the event is triggered
+ */
+MozMillElement.prototype.touchEnd = function (aOffsetX, aOffsetY) {
+ this.touchEvent(aOffsetX, aOffsetY, { type: "touchend" });
+
+ broker.pass({'function':'MozMillElement.touchEnd()'});
+
+ return true;
+};
+
+/**
+ * Synthesize a touchMove event on the given element
+ *
+ * @param {Number} aOffsetX
+ * Left offset where the event is triggered
+ * @param {Number} aOffsetY
+ * Top offset where the event is triggered
+ */
+MozMillElement.prototype.touchMove = function (aOffsetX, aOffsetY) {
+ this.touchEvent(aOffsetX, aOffsetY, { type: "touchmove" });
+
+ broker.pass({'function':'MozMillElement.touchMove()'});
+
+ return true;
+};
+
+MozMillElement.prototype.waitForElement = function (timeout, interval) {
+ var elem = this;
+
+ assert.waitFor(function () {
+ return elem.exists();
+ }, "Element.waitForElement(): Element '" + this.getInfo() +
+ "' has been found", timeout, interval);
+
+ broker.pass({'function':'MozMillElement.waitForElement()'});
+};
+
+MozMillElement.prototype.waitForElementNotPresent = function (timeout, interval) {
+ var elem = this;
+
+ assert.waitFor(function () {
+ return !elem.exists();
+ }, "Element.waitForElementNotPresent(): Element '" + this.getInfo() +
+ "' has not been found", timeout, interval);
+
+ broker.pass({'function':'MozMillElement.waitForElementNotPresent()'});
+};
+
+MozMillElement.prototype.waitThenClick = function (timeout, interval,
+ aOffsetX, aOffsetY, aExpectedEvent) {
+ this.waitForElement(timeout, interval);
+ this.click(aOffsetX, aOffsetY, aExpectedEvent);
+};
+
+/**
+ * Waits for the element to be available in the DOM, then trigger a tap event
+ *
+ * @param {Number} [aTimeout=5000]
+ * Time to wait for the element to be available
+ * @param {Number} [aInterval=100]
+ * Interval to check for availability
+ * @param {Number} [aOffsetX=aElement.width / 2]
+ * Left offset where the event is triggered
+ * @param {Number} [aOffsetY=aElement.height / 2]
+ * Top offset where the event is triggered
+ * @param {Object} [aExpectedEvent]
+ * Information about the expected event to occur
+ * @param {MozMillElement} [aExpectedEvent.target=this.element]
+ * Element which should receive the event
+ * @param {MozMillElement} [aExpectedEvent.type]
+ * Type of the expected mouse event
+ */
+MozMillElement.prototype.waitThenTap = function (aTimeout, aInterval,
+ aOffsetX, aOffsetY, aExpectedEvent) {
+ this.waitForElement(aTimeout, aInterval);
+ this.tap(aOffsetX, aOffsetY, aExpectedEvent);
+};
+
+// Dispatches an HTMLEvent
+MozMillElement.prototype.dispatchEvent = function (eventType, canBubble, modifiers) {
+ canBubble = canBubble || true;
+ modifiers = modifiers || { };
+
+ let document = 'ownerDocument' in this.element ? this.element.ownerDocument
+ : this.element.document;
+
+ let evt = document.createEvent('HTMLEvents');
+ evt.shiftKey = modifiers["shift"];
+ evt.metaKey = modifiers["meta"];
+ evt.altKey = modifiers["alt"];
+ evt.ctrlKey = modifiers["ctrl"];
+ evt.initEvent(eventType, canBubble, true);
+
+ this.element.dispatchEvent(evt);
+};
+
+
+/**
+ * MozMillCheckBox, which inherits from MozMillElement
+ */
+function MozMillCheckBox(locatorType, locator, args) {
+ MozMillElement.call(this, locatorType, locator, args);
+}
+
+
+MozMillCheckBox.prototype = Object.create(MozMillElement.prototype, {
+ check : {
+ /**
+ * Enable/Disable a checkbox depending on the target state
+ *
+ * @param {boolean} state State to set
+ * @return {boolean} Success state
+ */
+ value : function MMCB_check(state) {
+ var result = false;
+
+ if (!this.element) {
+ throw new Error("could not find element " + this.getInfo());
+ }
+
+ // If we have a XUL element, unwrap its XPCNativeWrapper
+ if (this.element.namespaceURI == NAMESPACE_XUL) {
+ this.element = utils.unwrapNode(this.element);
+ }
+
+ state = (typeof(state) == "boolean") ? state : false;
+ if (state != this.element.checked) {
+ this.click();
+ var element = this.element;
+
+ assert.waitFor(function () {
+ return element.checked == state;
+ }, "CheckBox.check(): Checkbox " + this.getInfo() + " could not be checked/unchecked", 500);
+
+ result = true;
+ }
+
+ broker.pass({'function':'MozMillCheckBox.check(' + this.getInfo() +
+ ', state: ' + state + ')'});
+
+ return result;
+ }
+ }
+});
+
+
+/**
+ * Returns true if node is of type MozMillCheckBox
+ *
+ * @static
+ * @param {DOMNode} node Node to check for its type
+ * @return {boolean} True if node is of type checkbox
+ */
+MozMillCheckBox.isType = function MMCB_isType(node) {
+ return ((node.localName.toLowerCase() == "input" && node.getAttribute("type") == "checkbox") ||
+ (node.localName.toLowerCase() == 'toolbarbutton' && node.getAttribute('type') == 'checkbox') ||
+ (node.localName.toLowerCase() == 'checkbox'));
+};
+
+
+/**
+ * MozMillRadio, which inherits from MozMillElement
+ */
+function MozMillRadio(locatorType, locator, args) {
+ MozMillElement.call(this, locatorType, locator, args);
+}
+
+
+MozMillRadio.prototype = Object.create(MozMillElement.prototype, {
+ select : {
+ /**
+ * Select the given radio button
+ *
+ * @param {number} [index=0]
+ * Specifies which radio button in the group to select (only
+ * applicable to radiogroup elements)
+ * @return {boolean} Success state
+ */
+ value : function MMR_select(index) {
+ if (!this.element) {
+ throw new Error("could not find element " + this.getInfo());
+ }
+
+ if (this.element.localName.toLowerCase() == "radiogroup") {
+ var element = this.element.getElementsByTagName("radio")[index || 0];
+ new MozMillRadio("Elem", element).click();
+ } else {
+ var element = this.element;
+ this.click();
+ }
+
+ assert.waitFor(function () {
+ // If we have a XUL element, unwrap its XPCNativeWrapper
+ if (element.namespaceURI == NAMESPACE_XUL) {
+ element = utils.unwrapNode(element);
+ return element.selected == true;
+ }
+
+ return element.checked == true;
+ }, "Radio.select(): Radio button " + this.getInfo() + " has been selected", 500);
+
+ broker.pass({'function':'MozMillRadio.select(' + this.getInfo() + ')'});
+
+ return true;
+ }
+ }
+});
+
+
+/**
+ * Returns true if node is of type MozMillRadio
+ *
+ * @static
+ * @param {DOMNode} node Node to check for its type
+ * @return {boolean} True if node is of type radio
+ */
+MozMillRadio.isType = function MMR_isType(node) {
+ return ((node.localName.toLowerCase() == 'input' && node.getAttribute('type') == 'radio') ||
+ (node.localName.toLowerCase() == 'toolbarbutton' && node.getAttribute('type') == 'radio') ||
+ (node.localName.toLowerCase() == 'radio') ||
+ (node.localName.toLowerCase() == 'radiogroup'));
+};
+
+
+/**
+ * MozMillDropList, which inherits from MozMillElement
+ */
+function MozMillDropList(locatorType, locator, args) {
+ MozMillElement.call(this, locatorType, locator, args);
+}
+
+
+MozMillDropList.prototype = Object.create(MozMillElement.prototype, {
+ select : {
+ /**
+ * Select the specified option and trigger the relevant events of the element
+ * @return {boolean}
+ */
+ value : function MMDL_select(index, option, value) {
+ if (!this.element){
+ throw new Error("Could not find element " + this.getInfo());
+ }
+
+ //if we have a select drop down
+ if (this.element.localName.toLowerCase() == "select"){
+ var item = null;
+
+ // The selected item should be set via its index
+ if (index != undefined) {
+ // Resetting a menulist has to be handled separately
+ if (index == -1) {
+ this.dispatchEvent('focus', false);
+ this.element.selectedIndex = index;
+ this.dispatchEvent('change', true);
+
+ broker.pass({'function':'MozMillDropList.select()'});
+
+ return true;
+ } else {
+ item = this.element.options.item(index);
+ }
+ } else {
+ for (var i = 0; i < this.element.options.length; i++) {
+ var entry = this.element.options.item(i);
+ if (option != undefined && entry.innerHTML == option ||
+ value != undefined && entry.value == value) {
+ item = entry;
+ break;
+ }
+ }
+ }
+
+ // Click the item
+ try {
+ // EventUtils.synthesizeMouse doesn't work.
+ this.dispatchEvent('focus', false);
+ item.selected = true;
+ this.dispatchEvent('change', true);
+
+ var self = this;
+ var selected = index || option || value;
+ assert.waitFor(function () {
+ switch (selected) {
+ case index:
+ return selected === self.element.selectedIndex;
+ break;
+ case option:
+ return selected === item.label;
+ break;
+ case value:
+ return selected === item.value;
+ break;
+ }
+ }, "DropList.select(): The correct item has been selected");
+
+ broker.pass({'function':'MozMillDropList.select()'});
+
+ return true;
+ } catch (e) {
+ throw new Error("No item selected for element " + this.getInfo());
+ }
+ }
+ //if we have a xul menupopup select accordingly
+ else if (this.element.namespaceURI.toLowerCase() == NAMESPACE_XUL) {
+ var ownerDoc = this.element.ownerDocument;
+ // Unwrap the XUL element's XPCNativeWrapper
+ this.element = utils.unwrapNode(this.element);
+ // Get the list of menuitems
+ var menuitems = this.element.
+ getElementsByTagNameNS(NAMESPACE_XUL, "menupopup")[0].
+ getElementsByTagNameNS(NAMESPACE_XUL, "menuitem");
+
+ var item = null;
+
+ if (index != undefined) {
+ if (index == -1) {
+ this.dispatchEvent('focus', false);
+ this.element.boxObject.activeChild = null;
+ this.dispatchEvent('change', true);
+
+ broker.pass({'function':'MozMillDropList.select()'});
+
+ return true;
+ } else {
+ item = menuitems[index];
+ }
+ } else {
+ for (var i = 0; i < menuitems.length; i++) {
+ var entry = menuitems[i];
+ if (option != undefined && entry.label == option ||
+ value != undefined && entry.value == value) {
+ item = entry;
+ break;
+ }
+ }
+ }
+
+ // Click the item
+ try {
+ item.click();
+
+ var self = this;
+ var selected = index || option || value;
+ assert.waitFor(function () {
+ switch (selected) {
+ case index:
+ return selected === self.element.selectedIndex;
+ break;
+ case option:
+ return selected === self.element.label;
+ break;
+ case value:
+ return selected === self.element.value;
+ break;
+ }
+ }, "DropList.select(): The correct item has been selected");
+
+ broker.pass({'function':'MozMillDropList.select()'});
+
+ return true;
+ } catch (e) {
+ throw new Error('No item selected for element ' + this.getInfo());
+ }
+ }
+ }
+ }
+});
+
+
+/**
+ * Returns true if node is of type MozMillDropList
+ *
+ * @static
+ * @param {DOMNode} node Node to check for its type
+ * @return {boolean} True if node is of type dropdown list
+ */
+MozMillDropList.isType = function MMR_isType(node) {
+ return ((node.localName.toLowerCase() == 'toolbarbutton' &&
+ (node.getAttribute('type') == 'menu' || node.getAttribute('type') == 'menu-button')) ||
+ (node.localName.toLowerCase() == 'menu') ||
+ (node.localName.toLowerCase() == 'menulist') ||
+ (node.localName.toLowerCase() == 'select' ));
+};
+
+
+/**
+ * MozMillTextBox, which inherits from MozMillElement
+ */
+function MozMillTextBox(locatorType, locator, args) {
+ MozMillElement.call(this, locatorType, locator, args);
+}
+
+
+MozMillTextBox.prototype = Object.create(MozMillElement.prototype, {
+ sendKeys : {
+ /**
+ * Synthesize keypress events for each character on the given element
+ *
+ * @param {string} aText
+ * The text to send as single keypress events
+ * @param {object} aModifiers
+ * Information about the modifier keys to send
+ * Elements: accelKey - Hold down the accelerator key (ctrl/meta)
+ * [optional - default: false]
+ * altKey - Hold down the alt key
+ * [optional - default: false]
+ * ctrlKey - Hold down the ctrl key
+ * [optional - default: false]
+ * metaKey - Hold down the meta key (command key on Mac)
+ * [optional - default: false]
+ * shiftKey - Hold down the shift key
+ * [optional - default: false]
+ * @param {object} aExpectedEvent
+ * Information about the expected event to occur
+ * Elements: target - Element which should receive the event
+ * [optional - default: current element]
+ * type - Type of the expected key event
+ * @return {boolean} Success state
+ */
+ value : function MMTB_sendKeys(aText, aModifiers, aExpectedEvent) {
+ if (!this.element) {
+ throw new Error("could not find element " + this.getInfo());
+ }
+
+ var element = this.element;
+ Array.forEach(aText, function (letter) {
+ var win = element.ownerDocument ? element.ownerDocument.defaultView
+ : element;
+ element.focus();
+
+ if (aExpectedEvent) {
+ if (!aExpectedEvent.type) {
+ throw new Error(arguments.callee.name + ": Expected event type not specified");
+ }
+
+ var target = aExpectedEvent.target ? aExpectedEvent.target.getNode()
+ : element;
+ EventUtils.synthesizeKeyExpectEvent(letter, aModifiers || {}, target,
+ aExpectedEvent.type,
+ "MozMillTextBox.sendKeys()", win);
+ } else {
+ EventUtils.synthesizeKey(letter, aModifiers || {}, win);
+ }
+ });
+
+ broker.pass({'function':'MozMillTextBox.type()'});
+
+ return true;
+ }
+ }
+});
+
+
+/**
+ * Returns true if node is of type MozMillTextBox
+ *
+ * @static
+ * @param {DOMNode} node Node to check for its type
+ * @return {boolean} True if node is of type textbox
+ */
+MozMillTextBox.isType = function MMR_isType(node) {
+ return ((node.localName.toLowerCase() == 'input' &&
+ (node.getAttribute('type') == 'text' || node.getAttribute('type') == 'search')) ||
+ (node.localName.toLowerCase() == 'textarea') ||
+ (node.localName.toLowerCase() == 'textbox'));
+};
diff --git a/services/sync/tps/extensions/mozmill/resource/modules/mozmill.js b/services/sync/tps/extensions/mozmill/resource/driver/mozmill.js
index 96fe6c92e..283c9bfb4 100644
--- a/services/sync/tps/extensions/mozmill/resource/modules/mozmill.js
+++ b/services/sync/tps/extensions/mozmill/resource/driver/mozmill.js
@@ -1,6 +1,6 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+ * file, you can obtain one at http://mozilla.org/MPL/2.0/. */
var EXPORTED_SYMBOLS = ["controller", "utils", "elementslib", "os",
"getBrowserController", "newBrowserController",
@@ -8,120 +8,153 @@ var EXPORTED_SYMBOLS = ["controller", "utils", "elementslib", "os",
"newMail3PaneController", "getMail3PaneController",
"wm", "platform", "getAddrbkController",
"getMsgComposeController", "getDownloadsController",
- "Application", "cleanQuit",
+ "Application", "findElement",
"getPlacesController", 'isMac', 'isLinux', 'isWindows',
- "firePythonCallback"
+ "firePythonCallback", "getAddons"
];
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cu = Components.utils;
+
+
+Cu.import("resource://gre/modules/AddonManager.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
// imports
-var controller = {}; Components.utils.import('resource://mozmill/modules/controller.js', controller);
-var utils = {}; Components.utils.import('resource://mozmill/modules/utils.js', utils);
-var elementslib = {}; Components.utils.import('resource://mozmill/modules/elementslib.js', elementslib);
-var frame = {}; Components.utils.import('resource://mozmill/modules/frame.js', frame);
-var os = {}; Components.utils.import('resource://mozmill/stdlib/os.js', os);
+var assertions = {}; Cu.import('resource://mozmill/modules/assertions.js', assertions);
+var broker = {}; Cu.import('resource://mozmill/driver/msgbroker.js', broker);
+var controller = {}; Cu.import('resource://mozmill/driver/controller.js', controller);
+var elementslib = {}; Cu.import('resource://mozmill/driver/elementslib.js', elementslib);
+var findElement = {}; Cu.import('resource://mozmill/driver/mozelement.js', findElement);
+var os = {}; Cu.import('resource://mozmill/stdlib/os.js', os);
+var utils = {}; Cu.import('resource://mozmill/stdlib/utils.js', utils);
+var windows = {}; Cu.import('resource://mozmill/modules/windows.js', windows);
+
-try {
- Components.utils.import("resource://gre/modules/AddonManager.jsm");
-} catch(e) { /* Firefox 4 only */ }
+const DEBUG = false;
+
+// This is a useful "check" timer. See utils.js, good for debugging
+if (DEBUG) {
+ utils.startTimer();
+}
+
+var assert = new assertions.Assert();
// platform information
var platform = os.getPlatform();
var isMac = false;
var isWindows = false;
var isLinux = false;
+
if (platform == "darwin"){
isMac = true;
}
+
if (platform == "winnt"){
isWindows = true;
}
+
if (platform == "linux"){
isLinux = true;
}
-var wm = Components.classes["@mozilla.org/appshell/window-mediator;1"]
- .getService(Components.interfaces.nsIWindowMediator);
+var wm = Services.wm;
-var appInfo = Components.classes["@mozilla.org/xre/app-info;1"]
- .getService(Components.interfaces.nsIXULAppInfo);
+var appInfo = Services.appinfo;
+var Application = utils.applicationName;
-var locale = Components.classes["@mozilla.org/chrome/chrome-registry;1"]
- .getService(Components.interfaces.nsIXULChromeRegistry)
- .getSelectedLocale("global");
-var aConsoleService = Components.classes["@mozilla.org/consoleservice;1"].
- getService(Components.interfaces.nsIConsoleService);
+/**
+ * Retrieves the list with information about installed add-ons.
+ *
+ * @returns {String} JSON data of installed add-ons
+ */
+function getAddons() {
+ var addons = null;
+
+ AddonManager.getAllAddons(function (addonList) {
+ var tmp_list = [ ];
+
+ addonList.forEach(function (addon) {
+ var tmp = { };
+
+ // We have to filter out properties of type 'function' of the addon
+ // object, which will break JSON.stringify() and result in incomplete
+ // addon information.
+ for (var key in addon) {
+ if (typeof(addon[key]) !== "function") {
+ tmp[key] = addon[key];
+ }
+ }
+ tmp_list.push(tmp);
+ });
-applicationDictionary = {
- "{718e30fb-e89b-41dd-9da7-e25a45638b28}": "Sunbird",
- "{92650c4d-4b8e-4d2a-b7eb-24ecf4f6b63a}": "SeaMonkey",
- "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}": "Firefox",
- "{8de7fcbb-c55c-4fbe-bfc5-fc555c87dbc4}": "PaleMoon",
- "{3550f703-e582-4d05-9a08-453d09bdfdc6}": 'Thunderbird',
+ addons = tmp_list;
+ });
+
+ try {
+ // Sychronize with getAllAddons so we do not return too early
+ assert.waitFor(function () {
+ return !!addons;
+ })
+
+ return addons;
+ } catch (e) {
+ return null;
+ }
}
-var Application = applicationDictionary[appInfo.ID];
+/**
+ * Retrieves application details for the Mozmill report
+ *
+ * @return {String} JSON data of application details
+ */
+function getApplicationDetails() {
+ var locale = Cc["@mozilla.org/chrome/chrome-registry;1"]
+ .getService(Ci.nsIXULChromeRegistry)
+ .getSelectedLocale("global");
+
+ // Put all our necessary information into JSON and return it:
+ // appinfo, startupinfo, and addons
+ var details = {
+ application_id: appInfo.ID,
+ application_name: Application,
+ application_version: appInfo.version,
+ application_locale: locale,
+ platform_buildid: appInfo.platformBuildID,
+ platform_version: appInfo.platformVersion,
+ addons: getAddons(),
+ startupinfo: getStartupInfo(),
+ paths: {
+ appdata: Services.dirsvc.get('UAppData', Ci.nsIFile).path,
+ profile: Services.dirsvc.get('ProfD', Ci.nsIFile).path
+ }
+ };
-if (Application == undefined) {
- // Default to Firefox
- var Application = 'PaleMoon';
+ return JSON.stringify(details);
}
// get startup time if available
// see http://blog.mozilla.com/tglek/2011/04/26/measuring-startup-speed-correctly/
-var startupInfo = {};
-try {
- var _startupInfo = Components.classes["@mozilla.org/toolkit/app-startup;1"]
- .getService(Components.interfaces.nsIAppStartup).getStartupInfo();
- for (var i in _startupInfo) {
- startupInfo[i] = _startupInfo[i].getTime(); // convert from Date object to ms since epoch
+function getStartupInfo() {
+ var startupInfo = {};
+
+ try {
+ var _startupInfo = Services.startup.getStartupInfo();
+ for (var time in _startupInfo) {
+ // convert from Date object to ms since epoch
+ startupInfo[time] = _startupInfo[time].getTime();
}
-} catch(e) {
+ } catch (e) {
startupInfo = null;
-}
-
-
-// keep list of installed addons to send to jsbridge for test run report
-var addons = "null"; // this will be JSON parsed
-if(typeof AddonManager != "undefined") {
- AddonManager.getAllAddons(function(addonList) {
- var converter = Components.classes["@mozilla.org/intl/scriptableunicodeconverter"]
- .createInstance(Components.interfaces.nsIScriptableUnicodeConverter);
- converter.charset = 'utf-8';
-
- function replacer(key, value) {
- if (typeof(value) == "string") {
- try {
- return converter.ConvertToUnicode(value);
- } catch(e) {
- var newstring = '';
- for (var i=0; i < value.length; i++) {
- replacement = '';
- if ((32 <= value.charCodeAt(i)) && (value.charCodeAt(i) < 127)) {
- // eliminate non-convertable characters;
- newstring += value.charAt(i);
- } else {
- newstring += replacement;
- }
- }
- return newstring;
- }
- }
- return value;
- }
+ }
- addons = converter.ConvertToUnicode(JSON.stringify(addonList, replacer))
- });
+ return startupInfo;
}
-function cleanQuit () {
- utils.getMethodInWindows('goQuitApplication')();
-}
-function addHttpResource (directory, namespace) {
- return 'http://localhost:4545/'+namespace;
-}
function newBrowserController () {
return new controller.MozMillController(utils.getMethodInWindows('OpenBrowserWindow')());
@@ -129,34 +162,39 @@ function newBrowserController () {
function getBrowserController () {
var browserWindow = wm.getMostRecentWindow("navigator:browser");
+
if (browserWindow == null) {
return newBrowserController();
- }
- else {
+ } else {
return new controller.MozMillController(browserWindow);
}
}
function getPlacesController () {
utils.getMethodInWindows('PlacesCommandHook').showPlacesOrganizer('AllBookmarks');
+
return new controller.MozMillController(wm.getMostRecentWindow(''));
}
function getAddonsController () {
if (Application == 'SeaMonkey') {
utils.getMethodInWindows('toEM')();
- } else if (Application == 'Thunderbird') {
+ }
+ else if (Application == 'Thunderbird') {
utils.getMethodInWindows('openAddonsMgr')();
- } else if (Application == 'Sunbird') {
+ }
+ else if (Application == 'Sunbird') {
utils.getMethodInWindows('goOpenAddons')();
} else {
utils.getMethodInWindows('BrowserOpenAddonsMgr')();
}
+
return new controller.MozMillController(wm.getMostRecentWindow(''));
}
function getDownloadsController() {
utils.getMethodInWindows('BrowserDownloadsUI')();
+
return new controller.MozMillController(wm.getMostRecentWindow(''));
}
@@ -166,6 +204,7 @@ function getPreferencesController() {
} else {
utils.getMethodInWindows('openPreferences')();
}
+
return new controller.MozMillController(wm.getMostRecentWindow(''));
}
@@ -176,10 +215,10 @@ function newMail3PaneController () {
function getMail3PaneController () {
var mail3PaneWindow = wm.getMostRecentWindow("mail:3pane");
+
if (mail3PaneWindow == null) {
return newMail3PaneController();
- }
- else {
+ } else {
return new controller.MozMillController(mail3PaneWindow);
}
}
@@ -189,6 +228,7 @@ function newAddrbkController () {
utils.getMethodInWindows("toAddressBook")();
utils.sleep(2000);
var addyWin = wm.getMostRecentWindow("mail:addressbook");
+
return new controller.MozMillController(addyWin);
}
@@ -196,35 +236,50 @@ function getAddrbkController () {
var addrbkWindow = wm.getMostRecentWindow("mail:addressbook");
if (addrbkWindow == null) {
return newAddrbkController();
- }
- else {
+ } else {
return new controller.MozMillController(addrbkWindow);
}
}
function firePythonCallback (filename, method, args, kwargs) {
obj = {'filename': filename, 'method': method};
- obj['test'] = frame.events.currentModule.__file__;
obj['args'] = args || [];
obj['kwargs'] = kwargs || {};
- frame.events.fireEvent("firePythonCallback", obj);
+
+ broker.sendMessage("firePythonCallback", obj);
}
function timer (name) {
this.name = name;
this.timers = {};
- frame.timers.push(this);
this.actions = [];
+
+ frame.timers.push(this);
}
+
timer.prototype.start = function (name) {
this.timers[name].startTime = (new Date).getTime();
}
+
timer.prototype.stop = function (name) {
var t = this.timers[name];
+
t.endTime = (new Date).getTime();
t.totalTime = (t.endTime - t.startTime);
}
+
timer.prototype.end = function () {
frame.events.fireEvent("timer", this);
frame.timers.remove(this);
}
+
+// Initialization
+
+/**
+ * Initialize Mozmill
+ */
+function initialize() {
+ windows.init();
+}
+
+initialize();
diff --git a/services/sync/tps/extensions/mozmill/resource/driver/msgbroker.js b/services/sync/tps/extensions/mozmill/resource/driver/msgbroker.js
new file mode 100644
index 000000000..95e431f08
--- /dev/null
+++ b/services/sync/tps/extensions/mozmill/resource/driver/msgbroker.js
@@ -0,0 +1,58 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, you can obtain one at http://mozilla.org/MPL/2.0/. */
+
+var EXPORTED_SYMBOLS = ['addListener', 'addObject',
+ 'removeListener',
+ 'sendMessage', 'log', 'pass', 'fail'];
+
+var listeners = {};
+
+// add a listener for a specific message type
+function addListener(msgType, listener) {
+ if (listeners[msgType] === undefined) {
+ listeners[msgType] = [];
+ }
+
+ listeners[msgType].push(listener);
+}
+
+// add each method in an object as a message listener
+function addObject(object) {
+ for (var msgType in object) {
+ addListener(msgType, object[msgType]);
+ }
+}
+
+// remove a listener for all message types
+function removeListener(listener) {
+ for (var msgType in listeners) {
+ for (let i = 0; i < listeners.length; ++i) {
+ if (listeners[msgType][i] == listener) {
+ listeners[msgType].splice(i, 1); // remove listener from array
+ }
+ }
+ }
+}
+
+function sendMessage(msgType, obj) {
+ if (listeners[msgType] === undefined) {
+ return;
+ }
+
+ for (let i = 0; i < listeners[msgType].length; ++i) {
+ listeners[msgType][i](obj);
+ }
+}
+
+function log(obj) {
+ sendMessage('log', obj);
+}
+
+function pass(obj) {
+ sendMessage('pass', obj);
+}
+
+function fail(obj) {
+ sendMessage('fail', obj);
+}
diff --git a/services/sync/tps/extensions/mozmill/resource/modules/assertions.js b/services/sync/tps/extensions/mozmill/resource/modules/assertions.js
index 1f0b92b8a..b49502057 100644
--- a/services/sync/tps/extensions/mozmill/resource/modules/assertions.js
+++ b/services/sync/tps/extensions/mozmill/resource/modules/assertions.js
@@ -1,42 +1,174 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+ * file, you can obtain one at http://mozilla.org/MPL/2.0/. */
-// Use the frame module of Mozmill to raise non-fatal failures
-var mozmillFrame = {};
-Cu.import('resource://mozmill/modules/frame.js', mozmillFrame);
+var EXPORTED_SYMBOLS = ['Assert', 'Expect'];
+const Cu = Components.utils;
+
+Cu.import("resource://gre/modules/Services.jsm");
+
+var broker = {}; Cu.import('resource://mozmill/driver/msgbroker.js', broker);
+var errors = {}; Cu.import('resource://mozmill/modules/errors.js', errors);
+var stack = {}; Cu.import('resource://mozmill/modules/stack.js', stack);
/**
* @name assertions
* @namespace Defines expect and assert methods to be used for assertions.
*/
-var assertions = exports;
+/**
+ * The Assert class implements fatal assertions, and can be used in cases
+ * when a failing test has to directly abort the current test function. All
+ * remaining tasks will not be performed.
+ *
+ */
+var Assert = function () {}
+
+Assert.prototype = {
+
+ // The following deepEquals implementation is from Narwhal under this license:
+
+ // http://wiki.commonjs.org/wiki/Unit_Testing/1.0
+ //
+ // THIS IS NOT TESTED NOR LIKELY TO WORK OUTSIDE V8!
+ //
+ // Originally from narwhal.js (http://narwhaljs.org)
+ // Copyright (c) 2009 Thomas Robinson <280north.com>
+ //
+ // Permission is hereby granted, free of charge, to any person obtaining a copy
+ // of this software and associated documentation files (the 'Software'), to
+ // deal in the Software without restriction, including without limitation the
+ // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ // sell copies of the Software, and to permit persons to whom the Software is
+ // furnished to do so, subject to the following conditions:
+ //
+ // The above copyright notice and this permission notice shall be included in
+ // all copies or substantial portions of the Software.
+ //
+ // THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ // AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ // ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+ // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+ _deepEqual: function (actual, expected) {
+ // 7.1. All identical values are equivalent, as determined by ===.
+ if (actual === expected) {
+ return true;
+
+ // 7.2. If the expected value is a Date object, the actual value is
+ // equivalent if it is also a Date object that refers to the same time.
+ } else if (actual instanceof Date && expected instanceof Date) {
+ return actual.getTime() === expected.getTime();
+
+ // 7.3. Other pairs that do not both pass typeof value == 'object',
+ // equivalence is determined by ==.
+ } else if (typeof actual != 'object' && typeof expected != 'object') {
+ return actual == expected;
+
+ // 7.4. For all other Object pairs, including Array objects, equivalence is
+ // determined by having the same number of owned properties (as verified
+ // with Object.prototype.hasOwnProperty.call), the same set of keys
+ // (although not necessarily the same order), equivalent values for every
+ // corresponding key, and an identical 'prototype' property. Note: this
+ // accounts for both named and indexed properties on Arrays.
+ } else {
+ return this._objEquiv(actual, expected);
+ }
+ },
-/* non-fatal assertions */
-var Expect = function() {}
+ _objEquiv: function (a, b) {
+ if (a == null || a == undefined || b == null || b == undefined)
+ return false;
+ // an identical 'prototype' property.
+ if (a.prototype !== b.prototype) return false;
+
+ function isArguments(object) {
+ return Object.prototype.toString.call(object) == '[object Arguments]';
+ }
+
+ //~~~I've managed to break Object.keys through screwy arguments passing.
+ // Converting to array solves the problem.
+ if (isArguments(a)) {
+ if (!isArguments(b)) {
+ return false;
+ }
+ a = pSlice.call(a);
+ b = pSlice.call(b);
+ return _deepEqual(a, b);
+ }
+ try {
+ var ka = Object.keys(a),
+ kb = Object.keys(b),
+ key, i;
+ } catch (e) {//happens when one is a string literal and the other isn't
+ return false;
+ }
+ // having the same number of owned properties (keys incorporates
+ // hasOwnProperty)
+ if (ka.length != kb.length)
+ return false;
+ //the same set of keys (although not necessarily the same order),
+ ka.sort();
+ kb.sort();
+ //~~~cheap key test
+ for (i = ka.length - 1; i >= 0; i--) {
+ if (ka[i] != kb[i])
+ return false;
+ }
+ //equivalent values for every corresponding key, and
+ //~~~possibly expensive deep test
+ for (i = ka.length - 1; i >= 0; i--) {
+ key = ka[i];
+ if (!this._deepEqual(a[key], b[key])) return false;
+ }
+ return true;
+ },
-Expect.prototype = {
+ _expectedException : function Assert__expectedException(actual, expected) {
+ if (!actual || !expected) {
+ return false;
+ }
+
+ if (expected instanceof RegExp) {
+ return expected.test(actual);
+ } else if (actual instanceof expected) {
+ return true;
+ } else if (expected.call({}, actual) === true) {
+ return true;
+ } else if (actual.name === expected.name) {
+ return true;
+ }
+
+ return false;
+ },
/**
- * Log a test as failing by adding a fail frame.
+ * Log a test as failing by throwing an AssertionException.
*
* @param {object} aResult
* Test result details used for reporting.
* <dl>
* <dd>fileName</dd>
* <dt>Name of the file in which the assertion failed.</dt>
- * <dd>function</dd>
+ * <dd>functionName</dd>
* <dt>Function in which the assertion failed.</dt>
* <dd>lineNumber</dd>
* <dt>Line number of the file in which the assertion failed.</dt>
* <dd>message</dd>
* <dt>Message why the assertion failed.</dt>
* </dl>
+ * @throws {errors.AssertionError}
+ *
*/
- _logFail: function Expect__logFail(aResult) {
- mozmillFrame.events.fail({fail: aResult});
+ _logFail: function Assert__logFail(aResult) {
+ throw new errors.AssertionError(aResult.message,
+ aResult.fileName,
+ aResult.lineNumber,
+ aResult.functionName,
+ aResult.name);
},
/**
@@ -47,7 +179,7 @@ Expect.prototype = {
* <dl>
* <dd>fileName</dd>
* <dt>Name of the file in which the assertion failed.</dt>
- * <dd>function</dd>
+ * <dd>functionName</dd>
* <dt>Function in which the assertion failed.</dt>
* <dd>lineNumber</dd>
* <dt>Line number of the file in which the assertion failed.</dt>
@@ -55,8 +187,8 @@ Expect.prototype = {
* <dt>Message why the assertion failed.</dt>
* </dl>
*/
- _logPass: function Expect__logPass(aResult) {
- mozmillFrame.events.pass({pass: aResult});
+ _logPass: function Assert__logPass(aResult) {
+ broker.pass({pass: aResult});
},
/**
@@ -68,9 +200,11 @@ Expect.prototype = {
* Message to show for the test result
* @param {string} aDiagnosis
* Diagnose message to show for the test result
+ * @throws {errors.AssertionError}
+ *
* @returns {boolean} Result of the test.
*/
- _test: function Expect__test(aCondition, aMessage, aDiagnosis) {
+ _test: function Assert__test(aCondition, aMessage, aDiagnosis) {
let diagnosis = aDiagnosis || "";
let message = aMessage || "";
@@ -78,19 +212,23 @@ Expect.prototype = {
message = aMessage ? message + " - " + diagnosis : diagnosis;
// Build result data
- let frame = Components.stack;
+ let frame = stack.findCallerFrame(Components.stack);
+
let result = {
- 'fileName' : frame.filename.replace(/(.*)-> /, ""),
- 'function' : frame.name,
- 'lineNumber' : frame.lineNumber,
- 'message' : message
+ 'fileName' : frame.filename.replace(/(.*)-> /, ""),
+ 'functionName' : frame.name,
+ 'lineNumber' : frame.lineNumber,
+ 'message' : message
};
// Log test result
- if (aCondition)
+ if (aCondition) {
this._logPass(result);
- else
+ }
+ else {
+ result.stack = Components.stack;
this._logFail(result);
+ }
return aCondition;
},
@@ -102,7 +240,7 @@ Expect.prototype = {
* Message to show for the test result.
* @returns {boolean} Always returns true.
*/
- pass: function Expect_pass(aMessage) {
+ pass: function Assert_pass(aMessage) {
return this._test(true, aMessage, undefined);
},
@@ -111,9 +249,11 @@ Expect.prototype = {
*
* @param {string} aMessage
* Message to show for the test result.
+ * @throws {errors.AssertionError}
+ *
* @returns {boolean} Always returns false.
*/
- fail: function Expect_fail(aMessage) {
+ fail: function Assert_fail(aMessage) {
return this._test(false, aMessage, undefined);
},
@@ -124,16 +264,18 @@ Expect.prototype = {
* Value to test.
* @param {string} aMessage
* Message to show for the test result.
+ * @throws {errors.AssertionError}
+ *
* @returns {boolean} Result of the test.
*/
- ok: function Expect_ok(aValue, aMessage) {
+ ok: function Assert_ok(aValue, aMessage) {
let condition = !!aValue;
let diagnosis = "got '" + aValue + "'";
return this._test(condition, aMessage, diagnosis);
},
- /**
+ /**
* Test if both specified values are identical.
*
* @param {boolean|string|number|object} aValue
@@ -142,16 +284,18 @@ Expect.prototype = {
* Value to strictly compare with.
* @param {string} aMessage
* Message to show for the test result
+ * @throws {errors.AssertionError}
+ *
* @returns {boolean} Result of the test.
*/
- equal: function Expect_equal(aValue, aExpected, aMessage) {
+ equal: function Assert_equal(aValue, aExpected, aMessage) {
let condition = (aValue === aExpected);
- let diagnosis = "got '" + aValue + "', expected '" + aExpected + "'";
+ let diagnosis = "'" + aValue + "' should equal '" + aExpected + "'";
return this._test(condition, aMessage, diagnosis);
},
- /**
+ /**
* Test if both specified values are not identical.
*
* @param {boolean|string|number|object} aValue
@@ -160,16 +304,82 @@ Expect.prototype = {
* Value to strictly compare with.
* @param {string} aMessage
* Message to show for the test result
+ * @throws {errors.AssertionError}
+ *
* @returns {boolean} Result of the test.
*/
- notEqual: function Expect_notEqual(aValue, aExpected, aMessage) {
+ notEqual: function Assert_notEqual(aValue, aExpected, aMessage) {
let condition = (aValue !== aExpected);
- let diagnosis = "got '" + aValue + "', not expected '" + aExpected + "'";
+ let diagnosis = "'" + aValue + "' should not equal '" + aExpected + "'";
return this._test(condition, aMessage, diagnosis);
},
/**
+ * Test if an object equals another object
+ *
+ * @param {object} aValue
+ * The object to test.
+ * @param {object} aExpected
+ * The object to strictly compare with.
+ * @param {string} aMessage
+ * Message to show for the test result
+ * @throws {errors.AssertionError}
+ *
+ * @returns {boolean} Result of the test.
+ */
+ deepEqual: function equal(aValue, aExpected, aMessage) {
+ let condition = this._deepEqual(aValue, aExpected);
+ try {
+ var aValueString = JSON.stringify(aValue);
+ } catch (e) {
+ var aValueString = String(aValue);
+ }
+ try {
+ var aExpectedString = JSON.stringify(aExpected);
+ } catch (e) {
+ var aExpectedString = String(aExpected);
+ }
+
+ let diagnosis = "'" + aValueString + "' should equal '" +
+ aExpectedString + "'";
+
+ return this._test(condition, aMessage, diagnosis);
+ },
+
+ /**
+ * Test if an object does not equal another object
+ *
+ * @param {object} aValue
+ * The object to test.
+ * @param {object} aExpected
+ * The object to strictly compare with.
+ * @param {string} aMessage
+ * Message to show for the test result
+ * @throws {errors.AssertionError}
+ *
+ * @returns {boolean} Result of the test.
+ */
+ notDeepEqual: function notEqual(aValue, aExpected, aMessage) {
+ let condition = !this._deepEqual(aValue, aExpected);
+ try {
+ var aValueString = JSON.stringify(aValue);
+ } catch (e) {
+ var aValueString = String(aValue);
+ }
+ try {
+ var aExpectedString = JSON.stringify(aExpected);
+ } catch (e) {
+ var aExpectedString = String(aExpected);
+ }
+
+ let diagnosis = "'" + aValueString + "' should not equal '" +
+ aExpectedString + "'";
+
+ return this._test(condition, aMessage, diagnosis);
+ },
+
+ /**
* Test if the regular expression matches the string.
*
* @param {string} aString
@@ -178,9 +388,11 @@ Expect.prototype = {
* Regular expression to use for testing that a match exists.
* @param {string} aMessage
* Message to show for the test result
+ * @throws {errors.AssertionError}
+ *
* @returns {boolean} Result of the test.
*/
- match: function Expect_match(aString, aRegex, aMessage) {
+ match: function Assert_match(aString, aRegex, aMessage) {
// XXX Bug 634948
// Regex objects are transformed to strings when evaluated in a sandbox
// For now lets re-create the regex from its string representation
@@ -190,8 +402,7 @@ Expect.prototype = {
pattern = matches[1];
flags = matches[2];
- }
- catch (ex) {
+ } catch (e) {
}
let regex = new RegExp(pattern, flags);
@@ -210,9 +421,11 @@ Expect.prototype = {
* Regular expression to use for testing that a match does not exist.
* @param {string} aMessage
* Message to show for the test result
+ * @throws {errors.AssertionError}
+ *
* @returns {boolean} Result of the test.
*/
- notMatch: function Expect_notMatch(aString, aRegex, aMessage) {
+ notMatch: function Assert_notMatch(aString, aRegex, aMessage) {
// XXX Bug 634948
// Regex objects are transformed to strings when evaluated in a sandbox
// For now lets re-create the regex from its string representation
@@ -222,8 +435,7 @@ Expect.prototype = {
pattern = matches[1];
flags = matches[2];
- }
- catch (ex) {
+ } catch (e) {
}
let regex = new RegExp(pattern, flags);
@@ -243,9 +455,11 @@ Expect.prototype = {
* the expected error class
* @param {string} message
* message to present if assertion fails
+ * @throws {errors.AssertionError}
+ *
* @returns {boolean} Result of the test.
*/
- throws : function Expect_throws(block, /*optional*/error, /*optional*/message) {
+ throws : function Assert_throws(block, /*optional*/error, /*optional*/message) {
return this._throws.apply(this, [true].concat(Array.prototype.slice.call(arguments)));
},
@@ -258,9 +472,11 @@ Expect.prototype = {
* the expected error class
* @param {string} message
* message to present if assertion fails
+ * @throws {errors.AssertionError}
+ *
* @returns {boolean} Result of the test.
*/
- doesNotThrow : function Expect_doesNotThrow(block, /*optional*/error, /*optional*/message) {
+ doesNotThrow : function Assert_doesNotThrow(block, /*optional*/error, /*optional*/message) {
return this._throws.apply(this, [false].concat(Array.prototype.slice.call(arguments)));
},
@@ -270,7 +486,7 @@ Expect.prototype = {
adapted from node.js's assert._throws()
https://github.com/joyent/node/blob/master/lib/assert.js
*/
- _throws : function Expect__throws(shouldThrow, block, expected, message) {
+ _throws : function Assert__throws(shouldThrow, block, expected, message) {
var actual;
if (typeof expected === 'string') {
@@ -299,80 +515,153 @@ Expect.prototype = {
!this._expectedException(actual, expected)) || (!shouldThrow && actual)) {
throw actual;
}
+
return this._test(true, message);
},
- _expectedException : function Expect__expectedException(actual, expected) {
- if (!actual || !expected) {
- return false;
- }
+ /**
+ * Test if the string contains the pattern.
+ *
+ * @param {String} aString String to test.
+ * @param {String} aPattern Pattern to look for in the string
+ * @param {String} aMessage Message to show for the test result
+ * @throws {errors.AssertionError}
+ *
+ * @returns {Boolean} Result of the test.
+ */
+ contain: function Assert_contain(aString, aPattern, aMessage) {
+ let condition = (aString.indexOf(aPattern) !== -1);
+ let diagnosis = "'" + aString + "' should contain '" + aPattern + "'";
- if (expected instanceof RegExp) {
- return expected.test(actual);
- } else if (actual instanceof expected) {
- return true;
- } else if (expected.call({}, actual) === true) {
- return true;
+ return this._test(condition, aMessage, diagnosis);
+ },
+
+ /**
+ * Test if the string does not contain the pattern.
+ *
+ * @param {String} aString String to test.
+ * @param {String} aPattern Pattern to look for in the string
+ * @param {String} aMessage Message to show for the test result
+ * @throws {errors.AssertionError}
+ *
+ * @returns {Boolean} Result of the test.
+ */
+ notContain: function Assert_notContain(aString, aPattern, aMessage) {
+ let condition = (aString.indexOf(aPattern) === -1);
+ let diagnosis = "'" + aString + "' should not contain '" + aPattern + "'";
+
+ return this._test(condition, aMessage, diagnosis);
+ },
+
+ /**
+ * Waits for the callback evaluates to true
+ *
+ * @param {Function} aCallback
+ * Callback for evaluation
+ * @param {String} aMessage
+ * Message to show for result
+ * @param {Number} aTimeout
+ * Timeout in waiting for evaluation
+ * @param {Number} aInterval
+ * Interval between evaluation attempts
+ * @param {Object} aThisObject
+ * this object
+ * @throws {errors.AssertionError}
+ *
+ * @returns {Boolean} Result of the test.
+ */
+ waitFor: function Assert_waitFor(aCallback, aMessage, aTimeout, aInterval, aThisObject) {
+ var timeout = aTimeout || 5000;
+ var interval = aInterval || 100;
+
+ var self = {
+ timeIsUp: false,
+ result: aCallback.call(aThisObject)
+ };
+ var deadline = Date.now() + timeout;
+
+ function wait() {
+ if (self.result !== true) {
+ self.result = aCallback.call(aThisObject);
+ self.timeIsUp = Date.now() > deadline;
+ }
}
- return false;
- }
-}
+ var hwindow = Services.appShell.hiddenDOMWindow;
+ var timeoutInterval = hwindow.setInterval(wait, interval);
+ var thread = Services.tm.currentThread;
-/**
-* AssertionError
-*
-* Error object thrown by failing assertions
-*/
-function AssertionError(message, fileName, lineNumber) {
- var err = new Error();
- if (err.stack) {
- this.stack = err.stack;
- }
- this.message = message === undefined ? err.message : message;
- this.fileName = fileName === undefined ? err.fileName : fileName;
- this.lineNumber = lineNumber === undefined ? err.lineNumber : lineNumber;
-};
-AssertionError.prototype = new Error();
-AssertionError.prototype.constructor = AssertionError;
-AssertionError.prototype.name = 'AssertionError';
+ while (self.result !== true && !self.timeIsUp) {
+ thread.processNextEvent(true);
+ let type = typeof(self.result);
+ if (type !== 'boolean')
+ throw TypeError("waitFor() callback has to return a boolean" +
+ " instead of '" + type + "'");
+ }
-var Assert = function() {}
+ hwindow.clearInterval(timeoutInterval);
-Assert.prototype = new Expect();
+ if (self.result !== true && self.timeIsUp) {
+ aMessage = aMessage || arguments.callee.name + ": Timeout exceeded for '" + aCallback + "'";
+ throw new errors.TimeoutError(aMessage);
+ }
-Assert.prototype.AssertionError = AssertionError;
+ broker.pass({'function':'assert.waitFor()'});
+ return true;
+ }
+}
-/**
- * The Assert class implements fatal assertions, and can be used in cases
- * when a failing test has to directly abort the current test function. All
- * remaining tasks will not be performed.
- *
- */
+/* non-fatal assertions */
+var Expect = function () {}
+
+Expect.prototype = new Assert();
/**
- * Log a test as failing by throwing an AssertionException.
+ * Log a test as failing by adding a fail frame.
*
* @param {object} aResult
* Test result details used for reporting.
* <dl>
* <dd>fileName</dd>
* <dt>Name of the file in which the assertion failed.</dt>
- * <dd>function</dd>
+ * <dd>functionName</dd>
* <dt>Function in which the assertion failed.</dt>
* <dd>lineNumber</dd>
* <dt>Line number of the file in which the assertion failed.</dt>
* <dd>message</dd>
* <dt>Message why the assertion failed.</dt>
* </dl>
- * @throws {AssertionError }
*/
-Assert.prototype._logFail = function Assert__logFail(aResult) {
- throw new AssertionError(aResult);
+Expect.prototype._logFail = function Expect__logFail(aResult) {
+ broker.fail({fail: aResult});
}
+/**
+ * Waits for the callback evaluates to true
+ *
+ * @param {Function} aCallback
+ * Callback for evaluation
+ * @param {String} aMessage
+ * Message to show for result
+ * @param {Number} aTimeout
+ * Timeout in waiting for evaluation
+ * @param {Number} aInterval
+ * Interval between evaluation attempts
+ * @param {Object} aThisObject
+ * this object
+ */
+Expect.prototype.waitFor = function Expect_waitFor(aCallback, aMessage, aTimeout, aInterval, aThisObject) {
+ let condition = true;
+ let message = aMessage;
+
+ try {
+ Assert.prototype.waitFor.apply(this, arguments);
+ }
+ catch (ex if ex instanceof errors.AssertionError) {
+ message = ex.message;
+ condition = false;
+ }
-// Export of variables
-assertions.Expect = Expect;
-assertions.Assert = Assert;
+ return this._test(condition, message);
+}
diff --git a/services/sync/tps/extensions/mozmill/resource/modules/controller.js b/services/sync/tps/extensions/mozmill/resource/modules/controller.js
deleted file mode 100644
index a703ce958..000000000
--- a/services/sync/tps/extensions/mozmill/resource/modules/controller.js
+++ /dev/null
@@ -1,1002 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-var EXPORTED_SYMBOLS = ["MozMillController", "globalEventRegistry", "sleep"];
-
-var EventUtils = {}; Components.utils.import('resource://mozmill/stdlib/EventUtils.js', EventUtils);
-
-var utils = {}; Components.utils.import('resource://mozmill/modules/utils.js', utils);
-var elementslib = {}; Components.utils.import('resource://mozmill/modules/elementslib.js', elementslib);
-var mozelement = {}; Components.utils.import('resource://mozmill/modules/mozelement.js', mozelement);
-var frame = {}; Components.utils.import('resource://mozmill/modules/frame.js', frame);
-
-var hwindow = Components.classes["@mozilla.org/appshell/appShellService;1"]
- .getService(Components.interfaces.nsIAppShellService)
- .hiddenDOMWindow;
-var aConsoleService = Components.classes["@mozilla.org/consoleservice;1"].
- getService(Components.interfaces.nsIConsoleService);
-
-// Declare most used utils functions in the controller namespace
-var sleep = utils.sleep;
-var assert = utils.assert;
-var waitFor = utils.waitFor;
-
-waitForEvents = function() {}
-
-waitForEvents.prototype = {
- /**
- * Initialize list of events for given node
- */
- init : function waitForEvents_init(node, events) {
- if (node.getNode != undefined)
- node = node.getNode();
-
- this.events = events;
- this.node = node;
- node.firedEvents = {};
- this.registry = {};
-
- for each(e in events) {
- var listener = function(event) {
- this.firedEvents[event.type] = true;
- }
- this.registry[e] = listener;
- this.registry[e].result = false;
- this.node.addEventListener(e, this.registry[e], true);
- }
- },
-
- /**
- * Wait until all assigned events have been fired
- */
- wait : function waitForEvents_wait(timeout, interval)
- {
- for (var e in this.registry) {
- utils.waitFor(function() {
- return this.node.firedEvents[e] == true;
- }, "Timeout happened before event '" + ex +"' was fired.", timeout, interval);
-
- this.node.removeEventListener(e, this.registry[e], true);
- }
- }
-}
-
-/**
- * Class to handle menus and context menus
- *
- * @constructor
- * @param {MozMillController} controller
- * Mozmill controller of the window under test
- * @param {string} menuSelector
- * jQuery like selector string of the element
- * @param {object} document
- * Document to use for finding the menu
- * [optional - default: aController.window.document]
- */
-var Menu = function(controller, menuSelector, document) {
- this._controller = controller;
- this._menu = null;
-
- document = document || controller.window.document;
- var node = document.querySelector(menuSelector);
- if (node) {
- // We don't unwrap nodes automatically yet (Bug 573185)
- node = node.wrappedJSObject || node;
- this._menu = new mozelement.Elem(node);
- }
- else {
- throw new Error("Menu element '" + menuSelector + "' not found.");
- }
-}
-
-Menu.prototype = {
-
- /**
- * Open and populate the menu
- *
- * @param {ElemBase} contextElement
- * Element whose context menu has to be opened
- * @returns {Menu} The Menu instance
- */
- open : function(contextElement) {
- // We have to open the context menu
- var menu = this._menu.getNode();
- if ((menu.localName == "popup" || menu.localName == "menupopup") &&
- contextElement && contextElement.exists()) {
- this._controller.rightClick(contextElement);
- this._controller.waitFor(function() {
- return menu.state == "open";
- }, "Context menu has been opened.");
- }
-
- // Run through the entire menu and populate with dynamic entries
- this._buildMenu(menu);
-
- return this;
- },
-
- /**
- * Close the menu
- *
- * @returns {Menu} The Menu instance
- */
- close : function() {
- var menu = this._menu.getNode();
-
- this._controller.keypress(this._menu, "VK_ESCAPE", {});
- this._controller.waitFor(function() {
- return menu.state == "closed";
- }, "Context menu has been closed.");
-
- return this;
- },
-
- /**
- * Retrieve the specified menu entry
- *
- * @param {string} itemSelector
- * jQuery like selector string of the menu item
- * @returns {ElemBase} Menu element
- * @throws Error If menu element has not been found
- */
- getItem : function(itemSelector) {
- var node = this._menu.getNode().querySelector(itemSelector);
-
- if (!node) {
- throw new Error("Menu entry '" + itemSelector + "' not found.");
- }
-
- return new mozelement.Elem(node);
- },
-
- /**
- * Click the specified menu entry
- *
- * @param {string} itemSelector
- * jQuery like selector string of the menu item
- *
- * @returns {Menu} The Menu instance
- */
- click : function(itemSelector) {
- this._controller.click(this.getItem(itemSelector));
-
- return this;
- },
-
- /**
- * Synthesize a keypress against the menu
- *
- * @param {string} key
- * Key to press
- * @param {object} modifier
- * Key modifiers
- * @see MozMillController#keypress
- *
- * @returns {Menu} The Menu instance
- */
- keypress : function(key, modifier) {
- this._controller.keypress(this._menu, key, modifier);
-
- return this;
- },
-
- /**
- * Opens the context menu, click the specified entry and
- * make sure that the menu has been closed.
- *
- * @param {string} itemSelector
- * jQuery like selector string of the element
- * @param {ElemBase} contextElement
- * Element whose context menu has to be opened
- *
- * @returns {Menu} The Menu instance
- */
- select : function(itemSelector, contextElement) {
- this.open(contextElement);
- this.click(itemSelector);
- this.close();
- },
-
- /**
- * Recursive function which iterates through all menu elements and
- * populates the menus with dynamic menu entries.
- *
- * @param {node} menu
- * Top menu node whose elements have to be populated
- */
- _buildMenu : function(menu) {
- var items = menu ? menu.childNodes : null;
-
- Array.forEach(items, function(item) {
- // When we have a menu node, fake a click onto it to populate
- // the sub menu with dynamic entries
- if (item.tagName == "menu") {
- var popup = item.querySelector("menupopup");
- if (popup) {
- if (popup.allowevents) {
- var popupEvent = this._controller.window.document.createEvent("MouseEvent");
- popupEvent.initMouseEvent("popupshowing", true, true, this._controller.window, 0,
- 0, 0, 0, 0, false, false, false, false, 0, null);
- popup.dispatchEvent(popupEvent);
- }
- this._buildMenu(popup);
- }
- }
- }, this);
- }
-};
-
-var MozMillController = function (window) {
- this.window = window;
-
- this.mozmillModule = {};
- Components.utils.import('resource://mozmill/modules/mozmill.js', this.mozmillModule);
-
- utils.waitFor(function() {
- return window != null && this.isLoaded();
- }, "controller(): Window could not be initialized.", undefined, undefined, this);
-
- if ( controllerAdditions[window.document.documentElement.getAttribute('windowtype')] != undefined ) {
- this.prototype = new utils.Copy(this.prototype);
- controllerAdditions[window.document.documentElement.getAttribute('windowtype')](this);
- this.windowtype = window.document.documentElement.getAttribute('windowtype');
- }
-}
-
-MozMillController.prototype.sleep = utils.sleep;
-
-// Open the specified url in the current tab
-MozMillController.prototype.open = function(url)
-{
- switch(this.mozmillModule.Application) {
- case "Firefox":
- this.window.gBrowser.loadURI(url);
- break;
- case "SeaMonkey":
- this.window.getBrowser().loadURI(url);
- break;
- default:
- throw new Error("MozMillController.open not supported.");
- }
-
- frame.events.pass({'function':'Controller.open()'});
-}
-
-/**
- * Take a screenshot of specified node
- *
- * @param {element} node
- * the window or DOM element to capture
- * @param {string} name
- * the name of the screenshot used in reporting and as filename
- * @param {boolean} save
- * if true saves the screenshot as 'name.png' in tempdir, otherwise returns a dataURL
- * @param {element list} highlights
- * a list of DOM elements to highlight by drawing a red rectangle around them
- */
-MozMillController.prototype.screenShot = function _screenShot(node, name, save, highlights) {
- if (!node) {
- throw new Error("node is undefined");
- }
-
- // Unwrap the node and highlights
- if ("getNode" in node) node = node.getNode();
- if (highlights) {
- for (var i = 0; i < highlights.length; ++i) {
- if ("getNode" in highlights[i]) {
- highlights[i] = highlights[i].getNode();
- }
- }
- }
-
- // If save is false, a dataURL is used
- // Include both in the report anyway to avoid confusion and make the report easier to parse
- var filepath, dataURL;
- try {
- if (save) {
- filepath = utils.takeScreenshot(node, name, highlights);
- } else {
- dataURL = utils.takeScreenshot(node, undefined, highlights);
- }
- } catch (e) {
- throw new Error("controller.screenShot() failed: " + e);
- }
-
- // Find the name of the test function
- for (var attr in frame.events.currentModule) {
- if (frame.events.currentModule[attr] == frame.events.currentTest) {
- var testName = attr;
- break;
- }
- }
-
- // Create a timestamp
- var d = new Date();
- // Report object
- var obj = { "filepath": filepath,
- "dataURL": dataURL,
- "name": name,
- "timestamp": d.toLocaleString(),
- "test_file": frame.events.currentModule.__file__,
- "test_name": testName,
- }
- // Send the screenshot object to python over jsbridge
- this.fireEvent("screenShot", obj);
-
- frame.events.pass({'function':'controller.screenShot()'});
-}
-
-/**
- * Checks if the specified window has been loaded
- *
- * @param {DOMWindow} [window=this.window] Window object to check for loaded state
- */
-MozMillController.prototype.isLoaded = function(window) {
- var win = window || this.window;
-
- return ("mozmillDocumentLoaded" in win) && win.mozmillDocumentLoaded;
-};
-
-MozMillController.prototype.waitFor = function(callback, message, timeout,
- interval, thisObject) {
- utils.waitFor(callback, message, timeout, interval, thisObject);
-
- frame.events.pass({'function':'controller.waitFor()'});
-}
-
-MozMillController.prototype.__defineGetter__("waitForEvents", function() {
- if (this._waitForEvents == undefined)
- this._waitForEvents = new waitForEvents();
- return this._waitForEvents;
-});
-
-/**
- * Wrapper function to create a new instance of a menu
- * @see Menu
- */
-MozMillController.prototype.getMenu = function (menuSelector, document) {
- return new Menu(this, menuSelector, document);
-};
-
-MozMillController.prototype.__defineGetter__("mainMenu", function() {
- return this.getMenu("menubar");
-});
-
-MozMillController.prototype.__defineGetter__("menus", function() {
- throw('controller.menus - DEPRECATED Use controller.mainMenu instead.');
-
-});
-
-MozMillController.prototype.waitForImage = function (elem, timeout, interval) {
- this.waitFor(function() {
- return elem.getNode().complete == true;
- }, "timeout exceeded for waitForImage " + elem.getInfo(), timeout, interval);
-
- frame.events.pass({'function':'Controller.waitForImage()'});
-}
-
-MozMillController.prototype.fireEvent = function (name, obj) {
- if (name == "userShutdown") {
- frame.events.toggleUserShutdown(obj);
- }
- frame.events.fireEvent(name, obj);
-}
-
-MozMillController.prototype.startUserShutdown = function (timeout, restart, next, resetProfile) {
- if (restart && resetProfile) {
- throw new Error("You can't have a user-restart and reset the profile; there is a race condition");
- }
- this.fireEvent('userShutdown', {'user': true,
- 'restart': Boolean(restart),
- 'next': next,
- 'resetProfile': Boolean(resetProfile)});
- this.window.setTimeout(this.fireEvent, timeout, 'userShutdown', 0);
-}
-
-MozMillController.prototype.restartApplication = function (next, resetProfile)
-{
- // restart the application via the python runner
- // - next : name of the next test function to run after restart
- // - resetProfile : whether to reset the profile after restart
- this.fireEvent('userShutdown', {'user': false,
- 'restart': true,
- 'next': next,
- 'resetProfile': Boolean(resetProfile)});
- utils.getMethodInWindows('goQuitApplication')();
-}
-
-MozMillController.prototype.stopApplication = function (resetProfile)
-{
- // stop the application via the python runner
- // - resetProfile : whether to reset the profile after shutdown
- this.fireEvent('userShutdown', {'user': false,
- 'restart': false,
- 'resetProfile': Boolean(resetProfile)});
- utils.getMethodInWindows('goQuitApplication')();
-}
-
-//Browser navigation functions
-MozMillController.prototype.goBack = function(){
- //this.window.focus();
- this.window.content.history.back();
- frame.events.pass({'function':'Controller.goBack()'});
- return true;
-}
-MozMillController.prototype.goForward = function(){
- //this.window.focus();
- this.window.content.history.forward();
- frame.events.pass({'function':'Controller.goForward()'});
- return true;
-}
-MozMillController.prototype.refresh = function(){
- //this.window.focus();
- this.window.content.location.reload(true);
- frame.events.pass({'function':'Controller.refresh()'});
- return true;
-}
-
-function logDeprecated(funcName, message) {
- frame.log({'function': funcName + '() - DEPRECATED', 'message': funcName + '() is deprecated' + message});
-}
-
-function logDeprecatedAssert(funcName) {
- logDeprecated('controller.' + funcName, '. use the generic `assert` module instead');
-}
-
-MozMillController.prototype.assertText = function (el, text) {
- logDeprecatedAssert("assertText");
- //this.window.focus();
- var n = el.getNode();
-
- if (n && n.innerHTML == text){
- frame.events.pass({'function':'Controller.assertText()'});
- return true;
- }
-
- throw new Error("could not validate element " + el.getInfo()+" with text "+ text);
- return false;
-
-};
-
-//Assert that a specified node exists
-MozMillController.prototype.assertNode = function (el) {
- logDeprecatedAssert("assertNode");
-
- //this.window.focus();
- var element = el.getNode();
- if (!element){
- throw new Error("could not find element " + el.getInfo());
- return false;
- }
- frame.events.pass({'function':'Controller.assertNode()'});
- return true;
-};
-
-// Assert that a specified node doesn't exist
-MozMillController.prototype.assertNodeNotExist = function (el) {
- logDeprecatedAssert("assertNodeNotExist");
-
- //this.window.focus();
- try {
- var element = el.getNode();
- } catch(err){
- frame.events.pass({'function':'Controller.assertNodeNotExist()'});
- return true;
- }
-
- if (element) {
- throw new Error("Unexpectedly found element " + el.getInfo());
- return false;
- } else {
- frame.events.pass({'function':'Controller.assertNodeNotExist()'});
- return true;
- }
-};
-
-//Assert that a form element contains the expected value
-MozMillController.prototype.assertValue = function (el, value) {
- logDeprecatedAssert("assertValue");
-
- //this.window.focus();
- var n = el.getNode();
-
- if (n && n.value == value){
- frame.events.pass({'function':'Controller.assertValue()'});
- return true;
- }
- throw new Error("could not validate element " + el.getInfo()+" with value "+ value);
- return false;
-};
-
-/**
- * Check if the callback function evaluates to true
- */
-MozMillController.prototype.assert = function(callback, message, thisObject)
-{
- logDeprecatedAssert("assert");
- utils.assert(callback, message, thisObject);
-
- frame.events.pass({'function': ": controller.assert('" + callback + "')"});
- return true;
-}
-
-//Assert that a provided value is selected in a select element
-MozMillController.prototype.assertSelected = function (el, value) {
- logDeprecatedAssert("assertSelected");
-
- //this.window.focus();
- var n = el.getNode();
- var validator = value;
-
- if (n && n.options[n.selectedIndex].value == validator){
- frame.events.pass({'function':'Controller.assertSelected()'});
- return true;
- }
- throw new Error("could not assert value for element " + el.getInfo()+" with value "+ value);
- return false;
-};
-
-//Assert that a provided checkbox is checked
-MozMillController.prototype.assertChecked = function (el) {
- logDeprecatedAssert("assertChecked");
-
- //this.window.focus();
- var element = el.getNode();
-
- if (element && element.checked == true){
- frame.events.pass({'function':'Controller.assertChecked()'});
- return true;
- }
- throw new Error("assert failed for checked element " + el.getInfo());
- return false;
-};
-
-// Assert that a provided checkbox is not checked
-MozMillController.prototype.assertNotChecked = function (el) {
- logDeprecatedAssert("assertNotChecked");
-
- var element = el.getNode();
-
- if (!element) {
- throw new Error("Could not find element" + el.getInfo());
- }
-
- if (!element.hasAttribute("checked") || element.checked != true){
- frame.events.pass({'function':'Controller.assertNotChecked()'});
- return true;
- }
- throw new Error("assert failed for not checked element " + el.getInfo());
- return false;
-};
-
-/**
- * Assert that an element's javascript property exists or has a particular value
- *
- * if val is undefined, will return true if the property exists.
- * if val is specified, will return true if the property exists and has the correct value
- */
-MozMillController.prototype.assertJSProperty = function(el, attrib, val) {
- logDeprecatedAssert("assertJSProperty");
-
- var element = el.getNode();
- if (!element){
- throw new Error("could not find element " + el.getInfo());
- return false;
- }
- var value = element[attrib];
- var res = (value !== undefined && (val === undefined ? true : String(value) == String(val)));
- if (res) {
- frame.events.pass({'function':'Controller.assertJSProperty("' + el.getInfo() + '") : ' + val});
- } else {
- throw new Error("Controller.assertJSProperty(" + el.getInfo() + ") : " +
- (val === undefined ? "property '" + attrib + "' doesn't exist" : val + " == " + value));
- }
- return res;
-};
-
-/**
- * Assert that an element's javascript property doesn't exist or doesn't have a particular value
- *
- * if val is undefined, will return true if the property doesn't exist.
- * if val is specified, will return true if the property doesn't exist or doesn't have the specified value
- */
-MozMillController.prototype.assertNotJSProperty = function(el, attrib, val) {
- logDeprecatedAssert("assertNotJSProperty");
-
- var element = el.getNode();
- if (!element){
- throw new Error("could not find element " + el.getInfo());
- return false;
- }
- var value = element[attrib];
- var res = (val === undefined ? value === undefined : String(value) != String(val));
- if (res) {
- frame.events.pass({'function':'Controller.assertNotProperty("' + el.getInfo() + '") : ' + val});
- } else {
- throw new Error("Controller.assertNotJSProperty(" + el.getInfo() + ") : " +
- (val === undefined ? "property '" + attrib + "' exists" : val + " != " + value));
- }
- return res;
-};
-
-/**
- * Assert that an element's dom property exists or has a particular value
- *
- * if val is undefined, will return true if the property exists.
- * if val is specified, will return true if the property exists and has the correct value
- */
-MozMillController.prototype.assertDOMProperty = function(el, attrib, val) {
- logDeprecatedAssert("assertDOMProperty");
-
- var element = el.getNode();
- if (!element){
- throw new Error("could not find element " + el.getInfo());
- return false;
- }
- var value, res = element.hasAttribute(attrib);
- if (res && val !== undefined) {
- value = element.getAttribute(attrib);
- res = (String(value) == String(val));
- }
-
- if (res) {
- frame.events.pass({'function':'Controller.assertDOMProperty("' + el.getInfo() + '") : ' + val});
- } else {
- throw new Error("Controller.assertDOMProperty(" + el.getInfo() + ") : " +
- (val === undefined ? "property '" + attrib + "' doesn't exist" : val + " == " + value));
- }
- return res;
-};
-
-/**
- * Assert that an element's dom property doesn't exist or doesn't have a particular value
- *
- * if val is undefined, will return true if the property doesn't exist.
- * if val is specified, will return true if the property doesn't exist or doesn't have the specified value
- */
-MozMillController.prototype.assertNotDOMProperty = function(el, attrib, val) {
- logDeprecatedAssert("assertNotDOMProperty");
-
- var element = el.getNode();
- if (!element){
- throw new Error("could not find element " + el.getInfo());
- return false;
- }
- var value, res = element.hasAttribute(attrib);
- if (res && val !== undefined) {
- value = element.getAttribute(attrib);
- res = (String(value) == String(val));
- }
- if (!res) {
- frame.events.pass({'function':'Controller.assertNotDOMProperty("' + el.getInfo() + '") : ' + val});
- } else {
- throw new Error("Controller.assertNotDOMProperty(" + el.getInfo() + ") : " +
- (val == undefined ? "property '" + attrib + "' exists" : val + " == " + value));
- }
- return !res;
-};
-
-// deprecated - Use assertNotJSProperty or assertNotDOMProperty instead
-MozMillController.prototype.assertProperty = function(el, attrib, val) {
- logDeprecatedAssert("assertProperty");
- return this.assertJSProperty(el, attrib, val);
-};
-
-// deprecated - Use assertNotJSProperty or assertNotDOMProperty instead
-MozMillController.prototype.assertPropertyNotExist = function(el, attrib) {
- logDeprecatedAssert("assertPropertyNotExist");
- return this.assertNotJSProperty(el, attrib);
-};
-
-// Assert that a specified image has actually loaded
-// The Safari workaround results in additional requests
-// for broken images (in Safari only) but works reliably
-MozMillController.prototype.assertImageLoaded = function (el) {
- logDeprecatedAssert("assertImageLoaded");
-
- //this.window.focus();
- var img = el.getNode();
- if (!img || img.tagName != 'IMG') {
- throw new Error('Controller.assertImageLoaded() failed.')
- return false;
- }
- var comp = img.complete;
- var ret = null; // Return value
-
- // Workaround for Safari -- it only supports the
- // complete attrib on script-created images
- if (typeof comp == 'undefined') {
- test = new Image();
- // If the original image was successfully loaded,
- // src for new one should be pulled from cache
- test.src = img.src;
- comp = test.complete;
- }
-
- // Check the complete attrib. Note the strict
- // equality check -- we don't want undefined, null, etc.
- // --------------------------
- // False -- Img failed to load in IE/Safari, or is
- // still trying to load in FF
- if (comp === false) {
- ret = false;
- }
- // True, but image has no size -- image failed to
- // load in FF
- else if (comp === true && img.naturalWidth == 0) {
- ret = false;
- }
- // Otherwise all we can do is assume everything's
- // hunky-dory
- else {
- ret = true;
- }
- if (ret) {
- frame.events.pass({'function':'Controller.assertImageLoaded'});
- } else {
- throw new Error('Controller.assertImageLoaded() failed.')
- }
-
- return ret;
-};
-
-// Drag one element to the top x,y coords of another specified element
-MozMillController.prototype.mouseMove = function (doc, start, dest) {
- // if one of these elements couldn't be looked up
- if (typeof start != 'object'){
- throw new Error("received bad coordinates");
- return false;
- }
- if (typeof dest != 'object'){
- throw new Error("received bad coordinates");
- return false;
- }
-
- var triggerMouseEvent = function(element, clientX, clientY) {
- clientX = clientX ? clientX: 0;
- clientY = clientY ? clientY: 0;
-
- // make the mouse understand where it is on the screen
- var screenX = element.boxObject.screenX ? element.boxObject.screenX : 0;
- var screenY = element.boxObject.screenY ? element.boxObject.screenY : 0;
-
- var evt = element.ownerDocument.createEvent('MouseEvents');
- if (evt.initMouseEvent) {
- evt.initMouseEvent('mousemove', true, true, element.ownerDocument.defaultView, 1, screenX, screenY, clientX, clientY)
- }
- else {
- //LOG.warn("element doesn't have initMouseEvent; firing an event which should -- but doesn't -- have other mouse-event related attributes here, as well as controlKeyDown, altKeyDown, shiftKeyDown, metaKeyDown");
- evt.initEvent('mousemove', true, true);
- }
- element.dispatchEvent(evt);
- };
-
- // Do the initial move to the drag element position
- triggerMouseEvent(doc.body, start[0], start[1]);
- triggerMouseEvent(doc.body, dest[0], dest[1]);
- frame.events.pass({'function':'Controller.mouseMove()'});
- return true;
-}
-
-// Drag an element to the specified offset on another element, firing mouse and drag events.
-// Returns the captured dropEffect. Adapted from EventUtils' synthesizeDrop()
-MozMillController.prototype.dragToElement = function(src, dest, offsetX,
- offsetY, aWindow, dropEffect, dragData) {
- srcElement = src.getNode();
- destElement = dest.getNode();
- aWindow = aWindow || srcElement.ownerDocument.defaultView;
- offsetX = offsetX || 20;
- offsetY = offsetY || 20;
-
- var dataTransfer;
-
- var trapDrag = function(event) {
- dataTransfer = event.dataTransfer;
- if(!dragData)
- return;
-
- for (var i = 0; i < dragData.length; i++) {
- var item = dragData[i];
- for (var j = 0; j < item.length; j++) {
- dataTransfer.mozSetDataAt(item[j].type, item[j].data, i);
- }
- }
- dataTransfer.dropEffect = dropEffect || "move";
- event.preventDefault();
- event.stopPropagation();
- }
-
- aWindow.addEventListener("dragstart", trapDrag, true);
- EventUtils.synthesizeMouse(srcElement, 2, 2, { type: "mousedown" }, aWindow); // fire mousedown 2 pixels from corner of element
- EventUtils.synthesizeMouse(srcElement, 11, 11, { type: "mousemove" }, aWindow);
- EventUtils.synthesizeMouse(srcElement, offsetX, offsetY, { type: "mousemove" }, aWindow);
- aWindow.removeEventListener("dragstart", trapDrag, true);
-
- var event = aWindow.document.createEvent("DragEvents");
- event.initDragEvent("dragenter", true, true, aWindow, 0, 0, 0, 0, 0, false, false, false, false, 0, null, dataTransfer);
- destElement.dispatchEvent(event);
-
- var event = aWindow.document.createEvent("DragEvents");
- event.initDragEvent("dragover", true, true, aWindow, 0, 0, 0, 0, 0, false, false, false, false, 0, null, dataTransfer);
- if (destElement.dispatchEvent(event)) {
- EventUtils.synthesizeMouse(destElement, offsetX, offsetY, { type: "mouseup" }, aWindow);
- return "none";
- }
-
- event = aWindow.document.createEvent("DragEvents");
- event.initDragEvent("drop", true, true, aWindow, 0, 0, 0, 0, 0, false, false, false, false, 0, null, dataTransfer);
- destElement.dispatchEvent(event);
- EventUtils.synthesizeMouse(destElement, offsetX, offsetY, { type: "mouseup" }, aWindow);
-
- return dataTransfer.dropEffect;
-}
-
-function preferencesAdditions(controller) {
- var mainTabs = controller.window.document.getAnonymousElementByAttribute(controller.window.document.documentElement, 'anonid', 'selector');
- controller.tabs = {};
- for (var i = 0; i < mainTabs.childNodes.length; i++) {
- var node = mainTabs.childNodes[i];
- var obj = {'button':node}
- controller.tabs[i] = obj;
- var label = node.attributes.item('label').value.replace('pane', '');
- controller.tabs[label] = obj;
- }
- controller.prototype.__defineGetter__("activeTabButton",
- function () {return mainTabs.getElementsByAttribute('selected', true)[0];
- })
-}
-
-function Tabs (controller) {
- this.controller = controller;
-}
-Tabs.prototype.getTab = function(index) {
- return this.controller.window.gBrowser.browsers[index].contentDocument;
-}
-Tabs.prototype.__defineGetter__("activeTab", function() {
- return this.controller.window.gBrowser.selectedBrowser.contentDocument;
-})
-Tabs.prototype.selectTab = function(index) {
- // GO in to tab manager and grab the tab by index and call focus.
-}
-Tabs.prototype.findWindow = function (doc) {
- for (var i = 0; i <= (this.controller.window.frames.length - 1); i++) {
- if (this.controller.window.frames[i].document == doc) {
- return this.controller.window.frames[i];
- }
- }
- throw new Error("Cannot find window for document. Doc title == " + doc.title);
-}
-Tabs.prototype.getTabWindow = function(index) {
- return this.findWindow(this.getTab(index));
-}
-Tabs.prototype.__defineGetter__("activeTabWindow", function () {
- return this.findWindow(this.activeTab);
-})
-Tabs.prototype.__defineGetter__("length", function () {
- return this.controller.window.gBrowser.browsers.length;
-})
-Tabs.prototype.__defineGetter__("activeTabIndex", function() {
- return this.controller.window.gBrowser.tabContainer.selectedIndex;
-})
-Tabs.prototype.selectTabIndex = function(i) {
- this.controller.window.gBrowser.selectTabAtIndex(i);
-}
-
-function browserAdditions (controller) {
- controller.tabs = new Tabs(controller);
-
- controller.waitForPageLoad = function(aDocument, aTimeout, aInterval) {
- var timeout = aTimeout || 30000;
- var owner;
-
- // If a user tries to do waitForPageLoad(2000), this will assign the
- // interval the first arg which is most likely what they were expecting
- if (typeof(aDocument) == "number"){
- timeout = aDocument;
- }
-
- // If the document is a tab find the corresponding browser element.
- // Otherwise we have to handle an embedded web page.
- if (aDocument && typeof(aDocument) == "object") {
- owner = this.window.gBrowser.getBrowserForDocument(aDocument);
-
- if (!owner) {
- // If the document doesn't belong to a tab it will be a
- // HTML element (e.g. iframe) embedded inside a tab.
- // In such a case use the default window of the document.
- owner = aDocument.defaultView;
- }
- }
-
- // If no owner has been specified, fallback to the selected tab browser
- owner = owner || this.window.gBrowser.selectedBrowser;
-
- // Wait until the content in the tab has been loaded
- this.waitFor(function() {
- return this.isLoaded(owner);
- }, "controller.waitForPageLoad(): Timeout waiting for page loaded.",
- timeout, aInterval, this);
- frame.events.pass({'function':'controller.waitForPageLoad()'});
- }
-}
-
-controllerAdditions = {
- 'Browser:Preferences':preferencesAdditions,
- 'navigator:browser' :browserAdditions,
-}
-
-/**
- * DEPRECATION WARNING
- *
- * The following methods have all been DEPRECATED as of Mozmill 2.0
- * Use the MozMillElement object instead (https://developer.mozilla.org/en/Mozmill/Mozmill_Element_Object)
- */
-MozMillController.prototype.select = function (elem, index, option, value) {
- return elem.select(index, option, value);
-};
-
-MozMillController.prototype.keypress = function(aTarget, aKey, aModifiers, aExpectedEvent) {
- return aTarget.keypress(aKey, aModifiers, aExpectedEvent);
-}
-
-MozMillController.prototype.type = function (aTarget, aText, aExpectedEvent) {
- return aTarget.sendKeys(aText, aExpectedEvent);
-}
-
-MozMillController.prototype.mouseEvent = function(aTarget, aOffsetX, aOffsetY, aEvent, aExpectedEvent) {
- return aTarget.mouseEvent(aOffsetX, aOffsetY, aEvent, aExpectedEvent);
-}
-
-MozMillController.prototype.click = function(elem, left, top, expectedEvent) {
- return elem.click(left, top, expectedEvent);
-}
-
-MozMillController.prototype.doubleClick = function(elem, left, top, expectedEvent) {
- return elem.doubleClick(left, top, expectedEvent);
-}
-
-MozMillController.prototype.mouseDown = function (elem, button, left, top, expectedEvent) {
- return elem.mouseDown(button, left, top, expectedEvent);
-};
-
-MozMillController.prototype.mouseOut = function (elem, button, left, top, expectedEvent) {
- return elem.mouseOut(button, left, top, expectedEvent);
-};
-
-MozMillController.prototype.mouseOver = function (elem, button, left, top, expectedEvent) {
- return elem.mouseOver(button, left, top, expectedEvent);
-};
-
-MozMillController.prototype.mouseUp = function (elem, button, left, top, expectedEvent) {
- return elem.mouseUp(button, left, top, expectedEvent);
-};
-
-MozMillController.prototype.middleClick = function(elem, left, top, expectedEvent) {
- return elem.middleClick(elem, left, top, expectedEvent);
-}
-
-MozMillController.prototype.rightClick = function(elem, left, top, expectedEvent) {
- return elem.rightClick(left, top, expectedEvent);
-}
-
-MozMillController.prototype.check = function(elem, state) {
- return elem.check(state);
-}
-
-MozMillController.prototype.radio = function(elem) {
- return elem.select();
-}
-
-MozMillController.prototype.waitThenClick = function (elem, timeout, interval) {
- return elem.waitThenClick(timeout, interval);
-}
-
-MozMillController.prototype.waitForElement = function(elem, timeout, interval) {
- return elem.waitForElement(timeout, interval);
-}
-
-MozMillController.prototype.waitForElementNotPresent = function(elem, timeout, interval) {
- return elem.waitForElementNotPresent(timeout, interval);
-}
-
diff --git a/services/sync/tps/extensions/mozmill/resource/modules/driver.js b/services/sync/tps/extensions/mozmill/resource/modules/driver.js
new file mode 100644
index 000000000..17fcfbde6
--- /dev/null
+++ b/services/sync/tps/extensions/mozmill/resource/modules/driver.js
@@ -0,0 +1,290 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, you can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * @namespace Defines the Mozmill driver for global actions
+ */
+var driver = exports;
+
+Cu.import("resource://gre/modules/Services.jsm");
+
+// Temporarily include utils module to re-use sleep
+var assertions = {}; Cu.import('resource://mozmill/modules/assertions.js', assertions);
+var mozmill = {}; Cu.import("resource://mozmill/driver/mozmill.js", mozmill);
+var utils = {}; Cu.import('resource://mozmill/stdlib/utils.js', utils);
+
+/**
+ * Gets the topmost browser window. If there are none at that time, optionally
+ * opens one. Otherwise will raise an exception if none are found.
+ *
+ * @memberOf driver
+ * @param {Boolean] [aOpenIfNone=true] Open a new browser window if none are found.
+ * @returns {DOMWindow}
+ */
+function getBrowserWindow(aOpenIfNone) {
+ // Set default
+ if (typeof aOpenIfNone === 'undefined') {
+ aOpenIfNone = true;
+ }
+
+ // If implicit open is off, turn on strict checking, and vice versa.
+ let win = getTopmostWindowByType("navigator:browser", !aOpenIfNone);
+
+ // Can just assume automatic open here. If we didn't want it and nothing found,
+ // we already raised above when getTopmostWindow was called.
+ if (!win)
+ win = openBrowserWindow();
+
+ return win;
+}
+
+
+/**
+ * Retrieves the hidden window on OS X
+ *
+ * @memberOf driver
+ * @returns {DOMWindow} The hidden window
+ */
+function getHiddenWindow() {
+ return Services.appShell.hiddenDOMWindow;
+}
+
+
+/**
+ * Opens a new browser window
+ *
+ * @memberOf driver
+ * @returns {DOMWindow}
+ */
+function openBrowserWindow() {
+ // On OS X we have to be able to create a new browser window even with no other
+ // window open. Therefore we have to use the hidden window. On other platforms
+ // at least one remaining browser window has to exist.
+ var win = mozmill.isMac ? getHiddenWindow() :
+ getTopmostWindowByType("navigator:browser", true);
+ return win.OpenBrowserWindow();
+}
+
+
+/**
+ * Pause the test execution for the given amount of time
+ *
+ * @type utils.sleep
+ * @memberOf driver
+ */
+var sleep = utils.sleep;
+
+/**
+ * Wait until the given condition via the callback returns true.
+ *
+ * @type utils.waitFor
+ * @memberOf driver
+ */
+var waitFor = assertions.Assert.waitFor;
+
+//
+// INTERNAL WINDOW ENUMERATIONS
+//
+
+/**
+ * Internal function to build a list of DOM windows using a given enumerator
+ * and filter.
+ *
+ * @private
+ * @memberOf driver
+ * @param {nsISimpleEnumerator} aEnumerator Window enumerator to use.
+ * @param {Function} [aFilterCallback] Function which is used to filter windows.
+ * @param {Boolean} [aStrict=true] Throw an error if no windows found
+ *
+ * @returns {DOMWindow[]} The windows found, in the same order as the enumerator.
+ */
+function _getWindows(aEnumerator, aFilterCallback, aStrict) {
+ // Set default
+ if (typeof aStrict === 'undefined')
+ aStrict = true;
+
+ let windows = [];
+
+ while (aEnumerator.hasMoreElements()) {
+ let window = aEnumerator.getNext();
+
+ if (!aFilterCallback || aFilterCallback(window)) {
+ windows.push(window);
+ }
+ }
+
+ // If this list is empty and we're strict, throw an error
+ if (windows.length === 0 && aStrict) {
+ var message = 'No windows were found';
+
+ // We'll throw a more detailed error if a filter was used.
+ if (aFilterCallback && aFilterCallback.name)
+ message += ' using filter "' + aFilterCallback.name + '"';
+
+ throw new Error(message);
+ }
+
+ return windows;
+}
+
+//
+// FILTER CALLBACKS
+//
+
+/**
+ * Generator of a closure to filter a window based by a method
+ *
+ * @memberOf driver
+ * @param {String} aName Name of the method in the window object.
+ * @returns {Boolean} True if the condition is met.
+ */
+function windowFilterByMethod(aName) {
+ return function byMethod(aWindow) { return (aName in aWindow); }
+}
+
+
+/**
+ * Generator of a closure to filter a window based by the its title
+ *
+ * @param {String} aTitle Title of the window.
+ * @returns {Boolean} True if the condition is met.
+ */
+function windowFilterByTitle(aTitle) {
+ return function byTitle(aWindow) { return (aWindow.document.title === aTitle); }
+}
+
+
+/**
+ * Generator of a closure to filter a window based by the its type
+ *
+ * @memberOf driver
+ * @param {String} aType Type of the window.
+ * @returns {Boolean} True if the condition is met.
+ */
+function windowFilterByType(aType) {
+ return function byType(aWindow) {
+ var type = aWindow.document.documentElement.getAttribute("windowtype");
+ return (type === aType);
+ }
+}
+
+//
+// WINDOW LIST RETRIEVAL FUNCTIONS
+//
+
+/**
+ * Retrieves a sorted list of open windows based on their age (newest to oldest),
+ * optionally matching filter criteria.
+ *
+ * @memberOf driver
+ * @param {Function} [aFilterCallback] Function which is used to filter windows.
+ * @param {Boolean} [aStrict=true] Throw an error if no windows found
+ *
+ * @returns {DOMWindow[]} List of windows.
+ */
+function getWindowsByAge(aFilterCallback, aStrict) {
+ var windows = _getWindows(Services.wm.getEnumerator(""),
+ aFilterCallback, aStrict);
+
+ // Reverse the list, since naturally comes back old->new
+ return windows.reverse();
+}
+
+
+/**
+ * Retrieves a sorted list of open windows based on their z order (topmost first),
+ * optionally matching filter criteria.
+ *
+ * @memberOf driver
+ * @param {Function} [aFilterCallback] Function which is used to filter windows.
+ * @param {Boolean} [aStrict=true] Throw an error if no windows found
+ *
+ * @returns {DOMWindow[]} List of windows.
+ */
+function getWindowsByZOrder(aFilterCallback, aStrict) {
+ return _getWindows(Services.wm.getZOrderDOMWindowEnumerator("", true),
+ aFilterCallback, aStrict);
+}
+
+//
+// SINGLE WINDOW RETRIEVAL FUNCTIONS
+//
+
+/**
+ * Retrieves the last opened window, optionally matching filter criteria.
+ *
+ * @memberOf driver
+ * @param {Function} [aFilterCallback] Function which is used to filter windows.
+ * @param {Boolean} [aStrict=true] If true, throws error if no window found.
+ *
+ * @returns {DOMWindow} The window, or null if none found and aStrict == false
+ */
+function getNewestWindow(aFilterCallback, aStrict) {
+ var windows = getWindowsByAge(aFilterCallback, aStrict);
+ return windows.length ? windows[0] : null;
+}
+
+/**
+ * Retrieves the topmost window, optionally matching filter criteria.
+ *
+ * @memberOf driver
+ * @param {Function} [aFilterCallback] Function which is used to filter windows.
+ * @param {Boolean} [aStrict=true] If true, throws error if no window found.
+ *
+ * @returns {DOMWindow} The window, or null if none found and aStrict == false
+ */
+function getTopmostWindow(aFilterCallback, aStrict) {
+ var windows = getWindowsByZOrder(aFilterCallback, aStrict);
+ return windows.length ? windows[0] : null;
+}
+
+
+/**
+ * Retrieves the topmost window given by the window type
+ *
+ * XXX: Bug 462222
+ * This function has to be used instead of getTopmostWindow until the
+ * underlying platform bug has been fixed.
+ *
+ * @memberOf driver
+ * @param {String} [aWindowType=null] Window type to query for
+ * @param {Boolean} [aStrict=true] Throw an error if no windows found
+ *
+ * @returns {DOMWindow} The window, or null if none found and aStrict == false
+ */
+function getTopmostWindowByType(aWindowType, aStrict) {
+ if (typeof aStrict === 'undefined')
+ aStrict = true;
+
+ var win = Services.wm.getMostRecentWindow(aWindowType);
+
+ if (win === null && aStrict) {
+ var message = 'No windows of type "' + aWindowType + '" were found';
+ throw new errors.UnexpectedError(message);
+ }
+
+ return win;
+}
+
+
+// Export of functions
+driver.getBrowserWindow = getBrowserWindow;
+driver.getHiddenWindow = getHiddenWindow;
+driver.openBrowserWindow = openBrowserWindow;
+driver.sleep = sleep;
+driver.waitFor = waitFor;
+
+driver.windowFilterByMethod = windowFilterByMethod;
+driver.windowFilterByTitle = windowFilterByTitle;
+driver.windowFilterByType = windowFilterByType;
+
+driver.getWindowsByAge = getWindowsByAge;
+driver.getNewestWindow = getNewestWindow;
+driver.getTopmostWindowByType = getTopmostWindowByType;
+
+
+// XXX Bug: 462222
+// Currently those functions cannot be used. So they shouldn't be exported.
+//driver.getWindowsByZOrder = getWindowsByZOrder;
+//driver.getTopmostWindow = getTopmostWindow;
diff --git a/services/sync/tps/extensions/mozmill/resource/modules/errors.js b/services/sync/tps/extensions/mozmill/resource/modules/errors.js
new file mode 100644
index 000000000..58d1a918a
--- /dev/null
+++ b/services/sync/tps/extensions/mozmill/resource/modules/errors.js
@@ -0,0 +1,102 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, you can obtain one at http://mozilla.org/MPL/2.0/. */
+
+var EXPORTED_SYMBOLS = ['BaseError',
+ 'ApplicationQuitError',
+ 'AssertionError',
+ 'TimeoutError'];
+
+
+/**
+ * Creates a new instance of a base error
+ *
+ * @class Represents the base for custom errors
+ * @param {string} [aMessage=Error().message]
+ * The error message to show
+ * @param {string} [aFileName=Error().fileName]
+ * The file name where the error has been raised
+ * @param {string} [aLineNumber=Error().lineNumber]
+ * The line number of the file where the error has been raised
+ * @param {string} [aFunctionName=undefined]
+ * The function name in which the error has been raised
+ */
+function BaseError(aMessage, aFileName, aLineNumber, aFunctionName) {
+ this.name = this.constructor.name;
+
+ var err = new Error();
+ if (err.stack) {
+ this.stack = err.stack;
+ }
+
+ this.message = aMessage || err.message;
+ this.fileName = aFileName || err.fileName;
+ this.lineNumber = aLineNumber || err.lineNumber;
+ this.functionName = aFunctionName;
+}
+
+
+/**
+ * Creates a new instance of an application quit error used by Mozmill to
+ * indicate that the application is going to shutdown
+ *
+ * @class Represents an error object thrown when the application is going to shutdown
+ * @param {string} [aMessage=Error().message]
+ * The error message to show
+ * @param {string} [aFileName=Error().fileName]
+ * The file name where the error has been raised
+ * @param {string} [aLineNumber=Error().lineNumber]
+ * The line number of the file where the error has been raised
+ * @param {string} [aFunctionName=undefined]
+ * The function name in which the error has been raised
+ */
+function ApplicationQuitError(aMessage, aFileName, aLineNumber, aFunctionName) {
+ BaseError.apply(this, arguments);
+}
+
+ApplicationQuitError.prototype = Object.create(BaseError.prototype, {
+ constructor : { value : ApplicationQuitError }
+});
+
+
+/**
+ * Creates a new instance of an assertion error
+ *
+ * @class Represents an error object thrown by failing assertions
+ * @param {string} [aMessage=Error().message]
+ * The error message to show
+ * @param {string} [aFileName=Error().fileName]
+ * The file name where the error has been raised
+ * @param {string} [aLineNumber=Error().lineNumber]
+ * The line number of the file where the error has been raised
+ * @param {string} [aFunctionName=undefined]
+ * The function name in which the error has been raised
+ */
+function AssertionError(aMessage, aFileName, aLineNumber, aFunctionName) {
+ BaseError.apply(this, arguments);
+}
+
+AssertionError.prototype = Object.create(BaseError.prototype, {
+ constructor : { value : AssertionError }
+});
+
+/**
+ * Creates a new instance of a timeout error
+ *
+ * @class Represents an error object thrown by failing assertions
+ * @param {string} [aMessage=Error().message]
+ * The error message to show
+ * @param {string} [aFileName=Error().fileName]
+ * The file name where the error has been raised
+ * @param {string} [aLineNumber=Error().lineNumber]
+ * The line number of the file where the error has been raised
+ * @param {string} [aFunctionName=undefined]
+ * The function name in which the error has been raised
+ */
+function TimeoutError(aMessage, aFileName, aLineNumber, aFunctionName) {
+ AssertionError.apply(this, arguments);
+}
+
+TimeoutError.prototype = Object.create(AssertionError.prototype, {
+ constructor : { value : TimeoutError }
+});
diff --git a/services/sync/tps/extensions/mozmill/resource/modules/frame.js b/services/sync/tps/extensions/mozmill/resource/modules/frame.js
index 59f8b68c6..799e81d55 100644
--- a/services/sync/tps/extensions/mozmill/resource/modules/frame.js
+++ b/services/sync/tps/extensions/mozmill/resource/modules/frame.js
@@ -1,132 +1,90 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-var EXPORTED_SYMBOLS = ['loadFile','Collector','Runner','events',
- 'jsbridge', 'runTestFile', 'log', 'getThread',
- 'timers', 'persisted'];
-
-var httpd = {}; Components.utils.import('resource://mozmill/stdlib/httpd.js', httpd);
-var os = {}; Components.utils.import('resource://mozmill/stdlib/os.js', os);
-var strings = {}; Components.utils.import('resource://mozmill/stdlib/strings.js', strings);
-var arrays = {}; Components.utils.import('resource://mozmill/stdlib/arrays.js', arrays);
-var withs = {}; Components.utils.import('resource://mozmill/stdlib/withs.js', withs);
-var utils = {}; Components.utils.import('resource://mozmill/modules/utils.js', utils);
-var securableModule = {}; Components.utils.import('resource://mozmill/stdlib/securable-module.js', securableModule);
-
-var aConsoleService = Components.classes["@mozilla.org/consoleservice;1"].
- getService(Components.interfaces.nsIConsoleService);
-var ios = Components.classes["@mozilla.org/network/io-service;1"]
- .getService(Components.interfaces.nsIIOService);
-var subscriptLoader = Components.classes["@mozilla.org/moz/jssubscript-loader;1"]
- .getService(Components.interfaces.mozIJSSubScriptLoader);
-var uuidgen = Components.classes["@mozilla.org/uuid-generator;1"]
- .getService(Components.interfaces.nsIUUIDGenerator);
+ * file, you can obtain one at http://mozilla.org/MPL/2.0/. */
+var EXPORTED_SYMBOLS = ['Collector','Runner','events', 'runTestFile', 'log',
+ 'timers', 'persisted', 'shutdownApplication'];
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cu = Components.utils;
+
+const TIMEOUT_SHUTDOWN_HTTPD = 15000;
+
+Cu.import("resource://gre/modules/Services.jsm");
+
+Cu.import('resource://mozmill/stdlib/httpd.js');
+
+var broker = {}; Cu.import('resource://mozmill/driver/msgbroker.js', broker);
+var assertions = {}; Cu.import('resource://mozmill/modules/assertions.js', assertions);
+var errors = {}; Cu.import('resource://mozmill/modules/errors.js', errors);
+var os = {}; Cu.import('resource://mozmill/stdlib/os.js', os);
+var strings = {}; Cu.import('resource://mozmill/stdlib/strings.js', strings);
+var arrays = {}; Cu.import('resource://mozmill/stdlib/arrays.js', arrays);
+var withs = {}; Cu.import('resource://mozmill/stdlib/withs.js', withs);
+var utils = {}; Cu.import('resource://mozmill/stdlib/utils.js', utils);
+
+var securableModule = {};
+Cu.import('resource://mozmill/stdlib/securable-module.js', securableModule);
+
+var uuidgen = Cc["@mozilla.org/uuid-generator;1"].getService(Ci.nsIUUIDGenerator);
+
+var httpd = null;
var persisted = {};
-var moduleLoader = new securableModule.Loader({
- rootPaths: ["resource://mozmill/modules/"],
- defaultPrincipal: "system",
- globals : { Cc: Components.classes,
- Ci: Components.interfaces,
- Cu: Components.utils,
- Cr: Components.results}
-});
-
-arrayRemove = function(array, from, to) {
- var rest = array.slice((to || from) + 1 || array.length);
- array.length = from < 0 ? array.length + from : from;
- return array.push.apply(array, rest);
-};
+var assert = new assertions.Assert();
+var expect = new assertions.Expect();
-mozmill = undefined; mozelement = undefined;
+var mozmill = undefined;
+var mozelement = undefined;
+var modules = undefined;
-var loadTestResources = function () {
- // load resources we want in our tests
- if (mozmill == undefined) {
- mozmill = {};
- Components.utils.import("resource://mozmill/modules/mozmill.js", mozmill);
- }
- if (mozelement == undefined) {
- mozelement = {};
- Components.utils.import("resource://mozmill/modules/mozelement.js", mozelement);
- }
-}
+var timers = [];
-var loadFile = function(path, collector) {
- // load a test module from a file and add some candy
- var file = Components.classes["@mozilla.org/file/local;1"]
- .createInstance(Components.interfaces.nsILocalFile);
- file.initWithPath(path);
- var uri = ios.newFileURI(file).spec;
-
- loadTestResources();
- var assertions = moduleLoader.require("./assertions");
- var module = {
- collector: collector,
- mozmill: mozmill,
- elementslib: mozelement,
- findElement: mozelement,
- persisted: persisted,
- Cc: Components.classes,
- Ci: Components.interfaces,
- Cu: Components.utils,
- Cr: Components.results,
- log: log,
- assert: new assertions.Assert(),
- expect: new assertions.Expect()
- }
- module.require = function (mod) {
- var loader = new securableModule.Loader({
- rootPaths: [ios.newFileURI(file.parent).spec,
- "resource://mozmill/modules/"],
- defaultPrincipal: "system",
- globals : { mozmill: mozmill,
- elementslib: mozelement, // This a quick hack to maintain backwards compatibility with 1.5.x
- findElement: mozelement,
- persisted: persisted,
- Cc: Components.classes,
- Ci: Components.interfaces,
- Cu: Components.utils,
- log: log }
- });
- return loader.require(mod);
- }
+/**
+ * Shutdown or restart the application
+ *
+ * @param {boolean} [aFlags=undefined]
+ * Additional flags how to handle the shutdown or restart. The attributes
+ * eRestarti386 and eRestartx86_64 have not been documented yet.
+ * @see https://developer.mozilla.org/nsIAppStartup#Attributes
+ */
+function shutdownApplication(aFlags) {
+ var flags = Ci.nsIAppStartup.eForceQuit;
- if (collector != undefined) {
- collector.current_file = file;
- collector.current_path = path;
+ if (aFlags) {
+ flags |= aFlags;
}
- try {
- subscriptLoader.loadSubScript(uri, module, "UTF-8");
- } catch(e) {
- events.fail(e);
- var obj = {
- 'filename':path,
- 'passed':false,
- 'failed':true,
- 'passes':0,
- 'fails' :1,
- 'name' :'Unknown Test',
- };
- events.fireEvent('endTest', obj);
- Components.utils.reportError(e);
+
+ // Send a request to shutdown the application. That will allow us and other
+ // components to finish up with any shutdown code. Please note that we don't
+ // care if other components or add-ons want to prevent this via cancelQuit,
+ // we really force the shutdown.
+ let cancelQuit = Components.classes["@mozilla.org/supports-PRBool;1"].
+ createInstance(Components.interfaces.nsISupportsPRBool);
+ Services.obs.notifyObservers(cancelQuit, "quit-application-requested", null);
+
+ // Use a timer to trigger the application restart, which will allow us to
+ // send an ACK packet via jsbridge if the method has been called via Python.
+ var event = {
+ notify: function(timer) {
+ Services.startup.quit(flags);
+ }
}
- module.__file__ = path;
- module.__uri__ = uri;
- return module;
+ var timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+ timer.initWithCallback(event, 100, Ci.nsITimer.TYPE_ONE_SHOT);
}
-function stateChangeBase (possibilties, restrictions, target, cmeta, v) {
+function stateChangeBase(possibilties, restrictions, target, cmeta, v) {
if (possibilties) {
if (!arrays.inArray(possibilties, v)) {
// TODO Error value not in this.poss
return;
}
}
+
if (restrictions) {
for (var i in restrictions) {
var r = restrictions[i];
@@ -136,87 +94,160 @@ function stateChangeBase (possibilties, restrictions, target, cmeta, v) {
}
}
}
+
// Fire jsbridge notification, logging notification, listener notifications
events[target] = v;
events.fireEvent(cmeta, target);
}
-timers = [];
var events = {
- 'currentState' : null,
- 'currentModule': null,
- 'currentTest' : null,
- 'userShutdown' : false,
- 'appQuit' : false,
- 'listeners' : {},
+ appQuit : false,
+ currentModule : null,
+ currentState : null,
+ currentTest : null,
+ shutdownRequested : false,
+ userShutdown : null,
+ userShutdownTimer : null,
+
+ listeners : {},
+ globalListeners : []
}
+
events.setState = function (v) {
- return stateChangeBase(['dependencies', 'setupModule', 'teardownModule',
- 'setupTest', 'teardownTest', 'test', 'collection'],
- null, 'currentState', 'setState', v);
+ return stateChangeBase(['dependencies', 'setupModule', 'teardownModule',
+ 'test', 'setupTest', 'teardownTest', 'collection'],
+ null, 'currentState', 'setState', v);
}
+
events.toggleUserShutdown = function (obj){
- if (this.userShutdown) {
- this.fail({'function':'frame.events.toggleUserShutdown', 'message':'Shutdown expected but none detected before timeout', 'userShutdown': obj});
+ if (!this.userShutdown) {
+ this.userShutdown = obj;
+
+ var event = {
+ notify: function(timer) {
+ events.toggleUserShutdown(obj);
+ }
+ }
+
+ this.userShutdownTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+ this.userShutdownTimer.initWithCallback(event, obj.timeout, Ci.nsITimer.TYPE_ONE_SHOT);
+
+ } else {
+ this.userShutdownTimer.cancel();
+
+ // If the application is not going to shutdown, the user shutdown failed and
+ // we have to force a shutdown.
+ if (!events.appQuit) {
+ this.fail({'function':'events.toggleUserShutdown',
+ 'message':'Shutdown expected but none detected before timeout',
+ 'userShutdown': obj});
+
+ var flags = Ci.nsIAppStartup.eAttemptQuit;
+ if (events.isRestartShutdown()) {
+ flags |= Ci.nsIAppStartup.eRestart;
+ }
+
+ shutdownApplication(flags);
+ }
}
- this.userShutdown = obj;
}
+
events.isUserShutdown = function () {
- return Boolean(this.userShutdown);
+ return this.userShutdown ? this.userShutdown["user"] : false;
+}
+
+events.isRestartShutdown = function () {
+ return this.userShutdown.restart;
+}
+
+events.startShutdown = function (obj) {
+ events.fireEvent('shutdown', obj);
+
+ if (obj["user"]) {
+ events.toggleUserShutdown(obj);
+ } else {
+ shutdownApplication(obj.flags);
+ }
}
-events.setTest = function (test, invokedFromIDE) {
+
+events.setTest = function (test) {
+ test.__start__ = Date.now();
test.__passes__ = [];
test.__fails__ = [];
- test.__invokedFromIDE__ = invokedFromIDE;
+
events.currentTest = test;
- test.__start__ = Date.now();
- var obj = {'filename':events.currentModule.__file__,
- 'name':test.__name__,
- }
+
+ var obj = {'filename': events.currentModule.__file__,
+ 'name': test.__name__}
events.fireEvent('setTest', obj);
}
+
events.endTest = function (test) {
+ // use the current test unless specified
+ if (test === undefined) {
+ test = events.currentTest;
+ }
+
+ // If no test is set it has already been reported. Beside that we don't want
+ // to report it a second time.
+ if (!test || test.status === 'done')
+ return;
+
// report the end of a test
- test.status = 'done';
- events.currentTest = null;
test.__end__ = Date.now();
- var obj = {'filename':events.currentModule.__file__,
- 'passed':test.__passes__.length,
- 'failed':test.__fails__.length,
- 'passes':test.__passes__,
- 'fails' :test.__fails__,
- 'name' :test.__name__,
- 'time_start':test.__start__,
- 'time_end':test.__end__
- }
+ test.status = 'done';
+
+ var obj = {'filename': events.currentModule.__file__,
+ 'passed': test.__passes__.length,
+ 'failed': test.__fails__.length,
+ 'passes': test.__passes__,
+ 'fails' : test.__fails__,
+ 'name' : test.__name__,
+ 'time_start': test.__start__,
+ 'time_end': test.__end__}
+
if (test.skipped) {
obj['skipped'] = true;
obj.skipped_reason = test.skipped_reason;
}
+
if (test.meta) {
obj.meta = test.meta;
}
- // Report the test result only if the test is a true test or if it is a
- // failing setup/teardown
- var shouldSkipReporting = false;
- if (test.__passes__ &&
- (test.__name__ == 'setupModule' ||
- test.__name__ == 'setupTest' ||
- test.__name__ == 'teardownTest' ||
- test.__name__ == 'teardownModule')) {
- shouldSkipReporting = true;
- }
-
- if (!shouldSkipReporting) {
+ // Report the test result only if the test is a true test or if it is failing
+ if (withs.startsWith(test.__name__, "test") || test.__fails__.length > 0) {
events.fireEvent('endTest', obj);
}
}
-events.setModule = function (v) {
- return stateChangeBase( null, [function (v) {return (v.__file__ != undefined)}],
- 'currentModule', 'setModule', v);
+events.setModule = function (aModule) {
+ aModule.__start__ = Date.now();
+ aModule.__status__ = 'running';
+
+ var result = stateChangeBase(null,
+ [function (aModule) {return (aModule.__file__ != undefined)}],
+ 'currentModule', 'setModule', aModule);
+
+ return result;
+}
+
+events.endModule = function (aModule) {
+ // It should only reported once, so check if it already has been done
+ if (aModule.__status__ === 'done')
+ return;
+
+ aModule.__end__ = Date.now();
+ aModule.__status__ = 'done';
+
+ var obj = {
+ 'filename': aModule.__file__,
+ 'time_start': aModule.__start__,
+ 'time_end': aModule.__end__
+ }
+
+ events.fireEvent('endModule', obj);
}
events.pass = function (obj) {
@@ -224,17 +255,22 @@ events.pass = function (obj) {
if (events.currentTest) {
events.currentTest.__passes__.push(obj);
}
- for each(var timer in timers) {
+
+ for each (var timer in timers) {
timer.actions.push(
- {"currentTest":events.currentModule.__file__+"::"+events.currentTest.__name__, "obj":obj,
- "result":"pass"}
+ {"currentTest": events.currentModule.__file__ + "::" + events.currentTest.__name__,
+ "obj": obj,
+ "result": "pass"}
);
}
+
events.fireEvent('pass', obj);
}
+
events.fail = function (obj) {
var error = obj.exception;
- if(error) {
+
+ if (error) {
// Error objects aren't enumerable https://bugzilla.mozilla.org/show_bug.cgi?id=637207
obj.exception = {
name: error.name,
@@ -244,147 +280,216 @@ events.fail = function (obj) {
stack: error.stack
};
}
+
// a low level event, such as a keystroke, fails
if (events.currentTest) {
events.currentTest.__fails__.push(obj);
}
- for each(var time in timers) {
+
+ for each (var time in timers) {
timer.actions.push(
- {"currentTest":events.currentModule.__file__+"::"+events.currentTest.__name__, "obj":obj,
- "result":"fail"}
+ {"currentTest": events.currentModule.__file__ + "::" + events.currentTest.__name__,
+ "obj": obj,
+ "result": "fail"}
);
}
+
events.fireEvent('fail', obj);
}
+
events.skip = function (reason) {
- // this is used to report skips associated with setupModule and setupTest
- // and nothing else
+ // this is used to report skips associated with setupModule and nothing else
events.currentTest.skipped = true;
events.currentTest.skipped_reason = reason;
- for each(var timer in timers) {
+
+ for (var timer of timers) {
timer.actions.push(
- {"currentTest":events.currentModule.__file__+"::"+events.currentTest.__name__, "obj":reason,
- "result":"skip"}
+ {"currentTest": events.currentModule.__file__ + "::" + events.currentTest.__name__,
+ "obj": reason,
+ "result": "skip"}
);
}
+
events.fireEvent('skip', reason);
}
+
events.fireEvent = function (name, obj) {
+ if (events.appQuit) {
+ // dump('* Event discarded: ' + name + ' ' + JSON.stringify(obj) + '\n');
+ return;
+ }
+
if (this.listeners[name]) {
for (var i in this.listeners[name]) {
this.listeners[name][i](obj);
}
}
+
for each(var listener in this.globalListeners) {
listener(name, obj);
}
}
-events.globalListeners = [];
+
events.addListener = function (name, listener) {
if (this.listeners[name]) {
this.listeners[name].push(listener);
- } else if (name =='') {
+ } else if (name == '') {
this.globalListeners.push(listener)
} else {
this.listeners[name] = [listener];
}
}
-events.removeListener = function(listener) {
+
+events.removeListener = function (listener) {
for (var listenerIndex in this.listeners) {
var e = this.listeners[listenerIndex];
+
for (var i in e){
if (e[i] == listener) {
- this.listeners[listenerIndex] = arrayRemove(e, i);
+ this.listeners[listenerIndex] = arrays.remove(e, i);
}
}
}
+
for (var i in this.globalListeners) {
if (this.globalListeners[i] == listener) {
- this.globalListeners = arrayRemove(this.globalListeners, i);
+ this.globalListeners = arrays.remove(this.globalListeners, i);
+ }
+ }
+}
+
+events.persist = function () {
+ try {
+ events.fireEvent('persist', persisted);
+ } catch (e) {
+ events.fireEvent('error', "persist serialization failed.")
+ }
+}
+
+events.firePythonCallback = function (obj) {
+ obj['test'] = events.currentModule.__file__;
+ events.fireEvent('firePythonCallback', obj);
+}
+
+events.screenshot = function (obj) {
+ // Find the name of the test function
+ for (var attr in events.currentModule) {
+ if (events.currentModule[attr] == events.currentTest) {
+ var testName = attr;
+ break;
}
}
+
+ obj['test_file'] = events.currentModule.__file__;
+ obj['test_name'] = testName;
+ events.fireEvent('screenshot', obj);
}
var log = function (obj) {
events.fireEvent('log', obj);
}
+// Register the listeners
+broker.addObject({'endTest': events.endTest,
+ 'fail': events.fail,
+ 'firePythonCallback': events.firePythonCallback,
+ 'log': log,
+ 'pass': events.pass,
+ 'persist': events.persist,
+ 'screenshot': events.screenshot,
+ 'shutdown': events.startShutdown,
+ });
+
try {
- var jsbridge = {}; Components.utils.import('resource://jsbridge/modules/events.js', jsbridge);
-} catch(err) {
- var jsbridge = null;
+ Cu.import('resource://jsbridge/modules/Events.jsm');
- aConsoleService.logStringMessage("jsbridge not available.");
+ events.addListener('', function (name, obj) {
+ Events.fireEvent('mozmill.' + name, obj);
+ });
+} catch (e) {
+ Services.console.logStringMessage("Event module of JSBridge not available.");
}
-if (jsbridge) {
- events.addListener('', function (name, obj) {jsbridge.fireEvent('mozmill.'+name, obj)} );
-}
-function Collector () {
- // the collector handles HTTPD and initilizing the module
- this.test_modules_by_filename = {};
- this.testing = [];
- this.httpd_started = false;
- this.http_port = 43336;
- this.http_server = httpd.getServer(this.http_port);
-}
+/**
+ * Observer for notifications when the application is going to shutdown
+ */
+function AppQuitObserver() {
+ this.runner = null;
-Collector.prototype.startHttpd = function () {
- while (this.httpd == undefined) {
- try {
- this.http_server.start(this.http_port);
- this.httpd = this.http_server;
- } catch(e) { // Failure most likely due to port conflict
- this.http_port++;
- this.http_server = httpd.getServer(this.http_port);
- };
- }
+ Services.obs.addObserver(this, "quit-application-requested", false);
}
-Collector.prototype.stopHttpd = function () {
- if (this.httpd) {
- this.httpd.stop(function(){}); // Callback needed to pause execution until the server has been properly shutdown
- this.httpd = null;
+
+AppQuitObserver.prototype = {
+ observe: function (aSubject, aTopic, aData) {
+ switch (aTopic) {
+ case "quit-application-requested":
+ Services.obs.removeObserver(this, "quit-application-requested");
+
+ // If we observe a quit notification make sure to send the
+ // results of the current test. In those cases we don't reach
+ // the equivalent code in runTestModule()
+ events.pass({'message': 'AppQuitObserver: ' + JSON.stringify(aData),
+ 'userShutdown': events.userShutdown});
+
+ if (this.runner) {
+ this.runner.end();
+ }
+
+ if (httpd) {
+ httpd.stop();
+ }
+
+ events.appQuit = true;
+
+ break;
+ }
}
}
-Collector.prototype.addHttpResource = function (directory, ns) {
- if (!this.httpd) {
- this.startHttpd();
- }
- if (!ns) {
- ns = '/';
- } else {
- ns = '/' + ns + '/';
- }
+var appQuitObserver = new AppQuitObserver();
- var lp = Components.classes["@mozilla.org/file/local;1"].
- createInstance(Components.interfaces.nsILocalFile);
- lp.initWithPath(os.abspath(directory, this.current_file));
- this.httpd.registerDirectory(ns, lp);
+/**
+ * The collector handles HTTPd.js and initilizing the module
+ */
+function Collector() {
+ this.test_modules_by_filename = {};
+ this.testing = [];
+}
- return 'http://localhost:' + this.http_port + ns
+Collector.prototype.addHttpResource = function (aDirectory, aPath) {
+ var fp = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsILocalFile);
+ fp.initWithPath(os.abspath(aDirectory, this.current_file));
+
+ return httpd.addHttpResource(fp, aPath);
}
-Collector.prototype.initTestModule = function (filename, name) {
- var test_module = loadFile(filename, this);
+Collector.prototype.initTestModule = function (filename, testname) {
+ var test_module = this.loadFile(filename, this);
+ var has_restarted = !(testname == null);
test_module.__tests__ = [];
+
for (var i in test_module) {
if (typeof(test_module[i]) == "function") {
test_module[i].__name__ = i;
- if (i == "setupTest") {
- test_module.__setupTest__ = test_module[i];
- } else if (i == "setupModule") {
+
+ // Only run setupModule if we are a single test OR if we are the first
+ // test of a restart chain (don't run it prior to members in a restart
+ // chain)
+ if (i == "setupModule" && !has_restarted) {
test_module.__setupModule__ = test_module[i];
+ } else if (i == "setupTest") {
+ test_module.__setupTest__ = test_module[i];
} else if (i == "teardownTest") {
test_module.__teardownTest__ = test_module[i];
} else if (i == "teardownModule") {
test_module.__teardownModule__ = test_module[i];
} else if (withs.startsWith(i, "test")) {
- if (name && (i != name)) {
- continue;
+ if (testname && (i != testname)) {
+ continue;
}
- name = null;
+
+ testname = null;
test_module.__tests__.push(test_module[i]);
}
}
@@ -392,171 +497,292 @@ Collector.prototype.initTestModule = function (filename, name) {
test_module.collector = this;
test_module.status = 'loaded';
+
this.test_modules_by_filename[filename] = test_module;
+
return test_module;
}
-// Observer which gets notified when the application quits
-function AppQuitObserver() {
- this.register();
+Collector.prototype.loadFile = function (path, collector) {
+ var moduleLoader = new securableModule.Loader({
+ rootPaths: ["resource://mozmill/modules/"],
+ defaultPrincipal: "system",
+ globals : { Cc: Cc,
+ Ci: Ci,
+ Cu: Cu,
+ Cr: Components.results}
+ });
+
+ // load a test module from a file and add some candy
+ var file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsILocalFile);
+ file.initWithPath(path);
+ var uri = Services.io.newFileURI(file).spec;
+
+ this.loadTestResources();
+
+ var systemPrincipal = Services.scriptSecurityManager.getSystemPrincipal();
+ var module = new Components.utils.Sandbox(systemPrincipal);
+ module.assert = assert;
+ module.Cc = Cc;
+ module.Ci = Ci;
+ module.Cr = Components.results;
+ module.Cu = Cu;
+ module.collector = collector;
+ module.driver = moduleLoader.require("driver");
+ module.elementslib = mozelement;
+ module.errors = errors;
+ module.expect = expect;
+ module.findElement = mozelement;
+ module.log = log;
+ module.mozmill = mozmill;
+ module.persisted = persisted;
+
+ module.require = function (mod) {
+ var loader = new securableModule.Loader({
+ rootPaths: [Services.io.newFileURI(file.parent).spec,
+ "resource://mozmill/modules/"],
+ defaultPrincipal: "system",
+ globals : { assert: assert,
+ expect: expect,
+ mozmill: mozmill,
+ elementslib: mozelement, // This a quick hack to maintain backwards compatibility with 1.5.x
+ findElement: mozelement,
+ persisted: persisted,
+ Cc: Cc,
+ Ci: Ci,
+ Cu: Cu,
+ log: log }
+ });
+
+ if (modules != undefined) {
+ loader.modules = modules;
+ }
+
+ var retval = loader.require(mod);
+ modules = loader.modules;
+
+ return retval;
+ }
+
+ if (collector != undefined) {
+ collector.current_file = file;
+ collector.current_path = path;
+ }
+
+ try {
+ Services.scriptloader.loadSubScript(uri, module, "UTF-8");
+ } catch (e) {
+ var obj = {
+ 'filename': path,
+ 'passed': 0,
+ 'failed': 1,
+ 'passes': [],
+ 'fails' : [{'exception' : {
+ message: e.message,
+ filename: e.filename,
+ lineNumber: e.lineNumber}}],
+ 'name' :'<TOP_LEVEL>'
+ };
+
+ events.fail({'exception': e});
+ events.fireEvent('endTest', obj);
+ }
+
+ module.__file__ = path;
+ module.__uri__ = uri;
+
+ return module;
}
-AppQuitObserver.prototype = {
- observe: function(subject, topic, data) {
- events.appQuit = true;
- },
- register: function() {
- var obsService = Components.classes["@mozilla.org/observer-service;1"]
- .getService(Components.interfaces.nsIObserverService);
- obsService.addObserver(this, "quit-application", false);
- },
- unregister: function() {
- var obsService = Components.classes["@mozilla.org/observer-service;1"]
- .getService(Components.interfaces.nsIObserverService);
- obsService.removeObserver(this, "quit-application");
+
+Collector.prototype.loadTestResources = function () {
+ // load resources we want in our tests
+ if (mozmill === undefined) {
+ mozmill = {};
+ Cu.import("resource://mozmill/driver/mozmill.js", mozmill);
+ }
+ if (mozelement === undefined) {
+ mozelement = {};
+ Cu.import("resource://mozmill/driver/mozelement.js", mozelement);
}
}
-function Runner (collector, invokedFromIDE) {
- this.collector = collector;
- this.invokedFromIDE = invokedFromIDE
- events.fireEvent('startRunner', true);
- var m = {}; Components.utils.import('resource://mozmill/modules/mozmill.js', m);
- this.platform = m.platform;
-}
+/**
+ *
+ */
+function Httpd(aPort) {
+ this.http_port = aPort;
-Runner.prototype.runTestFile = function (filename, name) {
- this.collector.initTestModule(filename, name);
- this.runTestModule(this.collector.test_modules_by_filename[filename]);
+ while (true) {
+ try {
+ var srv = new HttpServer();
+ srv.registerContentType("sjs", "sjs");
+ srv.identity.setPrimary("http", "localhost", this.http_port);
+ srv.start(this.http_port);
+
+ this._httpd = srv;
+ break;
+ }
+ catch (e) {
+ // Failure most likely due to port conflict
+ this.http_port++;
+ }
+ }
}
-Runner.prototype.end = function () {
+
+Httpd.prototype.addHttpResource = function (aDir, aPath) {
+ var path = aPath ? ("/" + aPath + "/") : "/";
+
try {
- events.fireEvent('persist', persisted);
- } catch(e) {
- events.fireEvent('error', "persist serialization failed.");
+ this._httpd.registerDirectory(path, aDir);
+ return 'http://localhost:' + this.http_port + path;
+ }
+ catch (e) {
+ throw Error("Failure to register directory: " + aDir.path);
+ }
+};
+
+Httpd.prototype.stop = function () {
+ if (!this._httpd) {
+ return;
}
- this.collector.stopHttpd();
- events.fireEvent('endRunner', true);
+
+ var shutdown = false;
+ this._httpd.stop(function () { shutdown = true; });
+
+ assert.waitFor(function () {
+ return shutdown;
+ }, "Local HTTP server has been stopped", TIMEOUT_SHUTDOWN_HTTPD);
+
+ this._httpd = null;
+};
+
+function startHTTPd() {
+ if (!httpd) {
+ // Ensure that we start the HTTP server only once during a session
+ httpd = new Httpd(43336);
+ }
+}
+
+
+function Runner() {
+ this.collector = new Collector();
+ this.ended = false;
+
+ var m = {}; Cu.import('resource://mozmill/driver/mozmill.js', m);
+ this.platform = m.platform;
+
+ events.fireEvent('startRunner', true);
}
-Runner.prototype.wrapper = function (func, arg) {
- thread = Components.classes["@mozilla.org/thread-manager;1"]
- .getService(Components.interfaces.nsIThreadManager)
- .currentThread;
+Runner.prototype.end = function () {
+ if (!this.ended) {
+ this.ended = true;
+
+ appQuitObserver.runner = null;
+
+ events.endTest();
+ events.endModule(events.currentModule);
+ events.fireEvent('endRunner', true);
+ events.persist();
+ }
+};
+
+Runner.prototype.runTestFile = function (filename, name) {
+ var module = this.collector.initTestModule(filename, name);
+ this.runTestModule(module);
+};
+
+Runner.prototype.runTestModule = function (module) {
+ appQuitObserver.runner = this;
+ events.setModule(module);
+
+ // If setupModule passes, run all the tests. Otherwise mark them as skipped.
+ if (this.execFunction(module.__setupModule__, module)) {
+ for (var test of module.__tests__) {
+ if (events.shutdownRequested) {
+ break;
+ }
+
+ // If setupTest passes, run the test. Otherwise mark it as skipped.
+ if (this.execFunction(module.__setupTest__, module)) {
+ this.execFunction(test);
+ } else {
+ this.skipFunction(test, module.__setupTest__.__name__ + " failed");
+ }
+
+ this.execFunction(module.__teardownTest__, module);
+ }
+
+ } else {
+ for (var test of module.__tests__) {
+ this.skipFunction(test, module.__setupModule__.__name__ + " failed");
+ }
+ }
+
+ this.execFunction(module.__teardownModule__, module);
+ events.endModule(module);
+};
+
+Runner.prototype.execFunction = function (func, arg) {
+ if (typeof func !== "function" || events.shutdownRequested) {
+ return true;
+ }
+
+ var isTest = withs.startsWith(func.__name__, "test");
+
+ events.setState(isTest ? "test" : func.__name);
+ events.setTest(func);
// skip excluded platforms
if (func.EXCLUDED_PLATFORMS != undefined) {
if (arrays.inArray(func.EXCLUDED_PLATFORMS, this.platform)) {
events.skip("Platform exclusion");
- return;
+ events.endTest(func);
+ return false;
}
}
// skip function if requested
if (func.__force_skip__ != undefined) {
events.skip(func.__force_skip__);
- return;
+ events.endTest(func);
+ return false;
}
// execute the test function
try {
- if (arg) {
- func(arg);
- } else {
- func();
- }
-
- // If a user shutdown was expected but the application hasn't quit, throw a failure
- if (events.isUserShutdown()) {
- utils.sleep(500); // Prevents race condition between mozrunner hard process kill and normal FFx shutdown
- if (events.userShutdown['user'] && !events.appQuit) {
- events.fail({'function':'Runner.wrapper',
- 'message':'Shutdown expected but none detected before end of test',
- 'userShutdown': events.userShutdown});
- }
- }
+ func(arg);
} catch (e) {
- // Allow the exception if a user shutdown was expected
- if (!events.isUserShutdown()) {
- events.fail({'exception': e, 'test':func})
- Components.utils.reportError(e);
+ if (e instanceof errors.ApplicationQuitError) {
+ events.shutdownRequested = true;
+ } else {
+ events.fail({'exception': e, 'test': func})
}
}
-}
-Runner.prototype.runTestModule = function (module) {
- events.setModule(module);
- module.__status__ = 'running';
- if (module.__setupModule__) {
- events.setState('setupModule');
- events.setTest(module.__setupModule__);
- this.wrapper(module.__setupModule__, module);
- var setupModulePassed = (events.currentTest.__fails__.length == 0 && !events.currentTest.skipped);
- events.endTest(module.__setupModule__);
- } else {
- var setupModulePassed = true;
- }
- if (setupModulePassed) {
- var observer = new AppQuitObserver();
- for (var i in module.__tests__) {
- events.appQuit = false;
- var test = module.__tests__[i];
-
- // TODO: introduce per-test timeout:
- // https://bugzilla.mozilla.org/show_bug.cgi?id=574871
-
- if (module.__setupTest__) {
- events.setState('setupTest');
- events.setTest(module.__setupTest__);
- this.wrapper(module.__setupTest__, test);
- var setupTestPassed = (events.currentTest.__fails__.length == 0 && !events.currentTest.skipped);
- events.endTest(module.__setupTest__);
- } else {
- var setupTestPassed = true;
- }
- events.setState('test');
- events.setTest(test, this.invokedFromIDE);
- if (setupTestPassed) {
- this.wrapper(test);
- if (events.userShutdown && !events.userShutdown['user']) {
- events.endTest(test);
- break;
- }
- } else {
- events.skip("setupTest failed.");
- }
- if (module.__teardownTest__) {
- events.setState('teardownTest');
- events.setTest(module.__teardownTest__);
- this.wrapper(module.__teardownTest__, test);
- events.endTest(module.__teardownTest__);
- }
- events.endTest(test)
- }
- observer.unregister();
- } else {
- for each(var test in module.__tests__) {
- events.setTest(test);
- events.skip("setupModule failed.");
- events.endTest(test);
- }
- }
- if (module.__teardownModule__) {
- events.setState('teardownModule');
- events.setTest(module.__teardownModule__);
- this.wrapper(module.__teardownModule__, module);
- events.endTest(module.__teardownModule__);
+ // If a user shutdown has been requested and the function already returned,
+ // we can assume that a shutdown will not happen anymore. We should force a
+ // shutdown then, to prevent the next test from being executed.
+ if (events.isUserShutdown()) {
+ events.shutdownRequested = true;
+ events.toggleUserShutdown(events.userShutdown);
}
- module.__status__ = 'done';
-}
-var runTestFile = function (filename, invokedFromIDE, name) {
- var runner = new Runner(new Collector(), invokedFromIDE);
+ events.endTest(func);
+ return events.currentTest.__fails__.length == 0;
+};
+
+function runTestFile(filename, name) {
+ var runner = new Runner();
runner.runTestFile(filename, name);
runner.end();
+
return true;
}
-var getThread = function () {
- return thread;
-}
+Runner.prototype.skipFunction = function (func, message) {
+ events.setTest(func);
+ events.skip(message);
+ events.endTest(func);
+};
diff --git a/services/sync/tps/extensions/mozmill/resource/modules/init.js b/services/sync/tps/extensions/mozmill/resource/modules/init.js
deleted file mode 100644
index 9ec4a4a29..000000000
--- a/services/sync/tps/extensions/mozmill/resource/modules/init.js
+++ /dev/null
@@ -1,177 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-var frame = {}; Components.utils.import('resource://mozmill/modules/frame.js', frame);
-
-/**
-* Console listener which listens for error messages in the console and forwards
-* them to the Mozmill reporting system for output.
-*/
-function ConsoleListener() {
- this.register();
-}
-ConsoleListener.prototype = {
- observe: function(aMessage) {
- var msg = aMessage.message;
- var re = /^\[.*Error:.*(chrome|resource):\/\/.*/i;
- if (msg.match(re)) {
- frame.events.fail(aMessage);
- }
- },
- QueryInterface: function (iid) {
- if (!iid.equals(Components.interfaces.nsIConsoleListener) && !iid.equals(Components.interfaces.nsISupports)) {
- throw Components.results.NS_ERROR_NO_INTERFACE;
- }
- return this;
- },
- register: function() {
- var aConsoleService = Components.classes["@mozilla.org/consoleservice;1"]
- .getService(Components.interfaces.nsIConsoleService);
- aConsoleService.registerListener(this);
- },
- unregister: function() {
- var aConsoleService = Components.classes["@mozilla.org/consoleservice;1"]
- .getService(Components.interfaces.nsIConsoleService);
- aConsoleService.unregisterListener(this);
- }
-}
-
-// start listening
-var consoleListener = new ConsoleListener();
-
-var EXPORTED_SYMBOLS = ["mozmill"];
-
-const Cc = Components.classes;
-const Ci = Components.interfaces;
-const Cu = Components.utils;
-
-var mozmill = Cu.import('resource://mozmill/modules/mozmill.js');
-
-// Observer for new top level windows
-var windowObserver = {
- observe: function(subject, topic, data) {
- attachEventListeners(subject);
- }
-};
-
-/**
- * Attach event listeners
- */
-function attachEventListeners(window) {
- // These are the event handlers
- function pageShowHandler(event) {
- var doc = event.originalTarget;
- var tab = window.gBrowser.getBrowserForDocument(doc);
-
- if (tab) {
- //log("*** Loaded tab: location=" + doc.location + ", baseURI=" + doc.baseURI + "\n");
- tab.mozmillDocumentLoaded = true;
- } else {
- //log("*** Loaded HTML location=" + doc.location + ", baseURI=" + doc.baseURI + "\n");
- doc.defaultView.mozmillDocumentLoaded = true;
- }
-
- // We need to add/remove the unload/pagehide event listeners to preserve caching.
- window.gBrowser.addEventListener("beforeunload", beforeUnloadHandler, true);
- window.gBrowser.addEventListener("pagehide", pageHideHandler, true);
- };
-
- var DOMContentLoadedHandler = function(event) {
- var errorRegex = /about:.+(error)|(blocked)\?/;
- if (errorRegex.exec(event.target.baseURI)) {
- // Wait about 1s to be sure the DOM is ready
- mozmill.utils.sleep(1000);
-
- var tab = window.gBrowser.getBrowserForDocument(event.target);
- if (tab)
- tab.mozmillDocumentLoaded = true;
-
- // We need to add/remove the unload event listener to preserve caching.
- window.gBrowser.addEventListener("beforeunload", beforeUnloadHandler, true);
- }
- };
-
- // beforeunload is still needed because pagehide doesn't fire before the page is unloaded.
- // still use pagehide for cases when beforeunload doesn't get fired
- function beforeUnloadHandler(event) {
- var doc = event.originalTarget;
- var tab = window.gBrowser.getBrowserForDocument(event.target);
-
- if (tab) {
- //log("*** Unload tab: location=" + doc.location + ", baseURI=" + doc.baseURI + "\n");
- tab.mozmillDocumentLoaded = false;
- } else {
- //log("*** Unload HTML location=" + doc.location + ", baseURI=" + doc.baseURI + "\n");
- doc.defaultView.mozmillDocumentLoaded = false;
- }
-
- window.gBrowser.removeEventListener("beforeunload", beforeUnloadHandler, true);
- };
-
- var pageHideHandler = function(event) {
- // If event.persisted is false, the beforeUnloadHandler should fire
- // and there is no need for this event handler.
- if (event.persisted) {
- var doc = event.originalTarget;
- var tab = window.gBrowser.getBrowserForDocument(event.target);
-
- if (tab) {
- //log("*** Unload tab: location=" + doc.location + ", baseURI=" + doc.baseURI + "\n");
- tab.mozmillDocumentLoaded = false;
- } else {
- //log("*** Unload HTML location=" + doc.location + ", baseURI=" + doc.baseURI + "\n");
- doc.defaultView.mozmillDocumentLoaded = false;
- }
-
- window.gBrowser.removeEventListener("beforeunload", beforeUnloadHandler, true);
- }
-
- };
-
- // Add the event handlers to the tabbedbrowser once its window has loaded
- window.addEventListener("load", function(event) {
- window.mozmillDocumentLoaded = true;
-
-
- if (window.gBrowser) {
- // Page is ready
- window.gBrowser.addEventListener("pageshow", pageShowHandler, true);
-
- // Note: Error pages will never fire a "load" event. For those we
- // have to wait for the "DOMContentLoaded" event. That's the final state.
- // Error pages will always have a baseURI starting with
- // "about:" followed by "error" or "blocked".
- window.gBrowser.addEventListener("DOMContentLoaded", DOMContentLoadedHandler, true);
-
- // Leave page (use caching)
- window.gBrowser.addEventListener("pagehide", pageHideHandler, true);
- }
- }, false);
-}
-
-/**
- * Initialize Mozmill
- */
-function initialize() {
- // Activate observer for new top level windows
- var observerService = Cc["@mozilla.org/observer-service;1"].
- getService(Ci.nsIObserverService);
- observerService.addObserver(windowObserver, "toplevel-window-ready", false);
-
- // Attach event listeners to all open windows
- var enumerator = Cc["@mozilla.org/appshell/window-mediator;1"].
- getService(Ci.nsIWindowMediator).getEnumerator("");
- while (enumerator.hasMoreElements()) {
- var win = enumerator.getNext();
- attachEventListeners(win);
-
- // For windows or dialogs already open we have to explicitly set the property
- // otherwise windows which load really quick never gets the property set and
- // we fail to create the controller
- win.mozmillDocumentLoaded = true;
- };
-}
-
-initialize();
-
diff --git a/services/sync/tps/extensions/mozmill/resource/modules/inspection.js b/services/sync/tps/extensions/mozmill/resource/modules/inspection.js
deleted file mode 100644
index 399952f12..000000000
--- a/services/sync/tps/extensions/mozmill/resource/modules/inspection.js
+++ /dev/null
@@ -1,363 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-var EXPORTED_SYMBOLS = ["inspectElement"]
-
-var elementslib = {}; Components.utils.import('resource://mozmill/modules/elementslib.js', elementslib);
-var mozmill = {}; Components.utils.import('resource://mozmill/modules/mozmill.js', mozmill);
-var utils = {}; Components.utils.import('resource://mozmill/modules/utils.js', utils);
-
-var arrays = {}; Components.utils.import('resource://mozmill/stdlib/arrays.js', arrays);
-var dom = {}; Components.utils.import('resource://mozmill/stdlib/dom.js', dom);
-var objects = {}; Components.utils.import('resource://mozmill/stdlib/objects.js', objects);
-var json2 = {}; Components.utils.import('resource://mozmill/stdlib/json2.js', json2);
-var withs = {}; Components.utils.import('resource://mozmill/stdlib/withs.js', withs);
-
-var wm = Components.classes["@mozilla.org/appshell/window-mediator;1"]
- .getService(Components.interfaces.nsIWindowMediator);
-
-var isNotAnonymous = function (elem, result) {
- if (result == undefined) {
- var result = true;
- }
- if ( elem.parentNode ) {
- var p = elem.parentNode;
- return isNotAnonymous(p, result == arrays.inArray(p.childNodes, elem) == true);
- } else {
- return result;
- }
-}
-
-var elemIsAnonymous = function (elem) {
- if (elem.getAttribute('anonid') || !arrays.inArray(elem.parentNode.childNodes, elem)) {
- return true;
- }
- return false;
-}
-
-var getXPath = function (node, path) {
- path = path || [];
-
- if(node.parentNode) {
- path = getXPath(node.parentNode, path);
- }
-
- if(node.previousSibling) {
- var count = 1;
- var sibling = node.previousSibling
- do {
- if(sibling.nodeType == 1 && sibling.nodeName == node.nodeName) {count++;}
- sibling = sibling.previousSibling;
- } while(sibling);
- if(count == 1) {count = null;}
- } else if(node.nextSibling) {
- var sibling = node.nextSibling;
- do {
- if(sibling.nodeType == 1 && sibling.nodeName == node.nodeName) {
- var count = 1;
- sibling = null;
- } else {
- var count = null;
- sibling = sibling.previousSibling;
- }
- } while(sibling);
- }
-
- if(node.nodeType == 1) {
- // if ($('absXpaths').checked){
- path.push(node.nodeName.toLowerCase() + (node.id ? "[@id='"+node.id+"']" : count > 0 ? "["+count+"]" : ''));
- // }
- // else{
- // path.push(node.nodeName.toLowerCase() + (node.id ? "" : count > 0 ? "["+count+"]" : ''));
- // }
- }
- return path;
-};
-
-function getXSPath(node){
- var xpArray = getXPath(node);
- var stringXpath = xpArray.join('/');
- stringXpath = '/'+stringXpath;
- stringXpath = stringXpath.replace('//','/');
- return stringXpath;
-}
-function getXULXpath (el, xml) {
- var xpath = '';
- var pos, tempitem2;
-
- while(el !== xml.documentElement) {
- pos = 0;
- tempitem2 = el;
- while(tempitem2) {
- if (tempitem2.nodeType === 1 && tempitem2.nodeName === el.nodeName) {
- // If it is ELEMENT_NODE of the same name
- pos += 1;
- }
- tempitem2 = tempitem2.previousSibling;
- }
-
- xpath = "*[name()='"+el.nodeName+"' and namespace-uri()='"+(el.namespaceURI===null?'':el.namespaceURI)+"']["+pos+']'+'/'+xpath;
-
- el = el.parentNode;
- }
- xpath = '/*'+"[name()='"+xml.documentElement.nodeName+"' and namespace-uri()='"+(el.namespaceURI===null?'':el.namespaceURI)+"']"+'/'+xpath;
- xpath = xpath.replace(/\/$/, '');
- return xpath;
-}
-
-var getDocument = function (elem) {
- while (elem.parentNode) {
- var elem = elem.parentNode;
- }
- return elem;
-}
-
-var getTopWindow = function(doc) {
- return utils.getChromeWindow(doc.defaultView);
-}
-
-var attributeToIgnore = ['focus', 'focused', 'selected', 'select', 'flex', // General Omissions
- 'linkedpanel', 'last-tab', 'afterselected', // From Tabs UI, thanks Farhad
- 'style', // Gets set dynamically all the time, also effected by dx display code
- ];
-
-var getUniqueAttributesReduction = function (attributes, node) {
- for (var i in attributes) {
- if ( node.getAttribute(i) == attributes[i] || arrays.inArray(attributeToIgnore, i) || arrays.inArray(attributeToIgnore, attributes[i]) || i == 'id') {
- delete attributes[i];
- }
- }
- return attributes;
-}
-
-var getLookupExpression = function (_document, elem) {
- expArray = [];
- while ( elem.parentNode ) {
- var exp = getLookupForElem(_document, elem);
- expArray.push(exp);
- var elem = elem.parentNode;
- }
- expArray.reverse();
- return '/' + expArray.join('/');
-}
-
-var getLookupForElem = function (_document, elem) {
- if ( !elemIsAnonymous(elem) ) {
- if (elem.id != "" && !withs.startsWith(elem.id, 'panel')) {
- identifier = {'name':'id', 'value':elem.id};
- } else if ((elem.name != "") && (typeof(elem.name) != "undefined")) {
- identifier = {'name':'name', 'value':elem.name};
- } else {
- identifier = null;
- }
-
- if (identifier) {
- var result = {'id':elementslib._byID, 'name':elementslib._byName}[identifier.name](_document, elem.parentNode, identifier.value);
- if ( typeof(result != 'array') ) {
- return identifier.name+'('+json2.JSON.stringify(identifier.value)+')';
- }
- }
-
- // At this point there is either no identifier or it returns multiple
- var parse = [n for each (n in elem.parentNode.childNodes) if
- (n.getAttribute && n != elem)
- ];
- parse.unshift(dom.getAttributes(elem));
- var uniqueAttributes = parse.reduce(getUniqueAttributesReduction);
-
- if (!result) {
- var result = elementslib._byAttrib(elem.parentNode, uniqueAttributes);
- }
-
- if (!identifier && typeof(result) == 'array' ) {
- return json2.JSON.stringify(uniqueAttributes) + '['+arrays.indexOf(result, elem)+']'
- } else {
- var aresult = elementslib._byAttrib(elem.parentNode, uniqueAttributes);
- if ( typeof(aresult != 'array') ) {
- if (objects.getLength(uniqueAttributes) == 0) {
- return '['+arrays.indexOf(elem.parentNode.childNodes, elem)+']'
- }
- return json2.JSON.stringify(uniqueAttributes)
- } else if ( result.length > aresult.length ) {
- return json2.JSON.stringify(uniqueAttributes) + '['+arrays.indexOf(aresult, elem)+']'
- } else {
- return identifier.name+'('+json2.JSON.stringify(identifier.value)+')' + '['+arrays.indexOf(result, elem)+']'
- }
- }
-
- } else {
- // Handle Anonymous Nodes
- var parse = [n for each (n in _document.getAnonymousNodes(elem.parentNode)) if
- (n.getAttribute && n != elem)
- ];
- parse.unshift(dom.getAttributes(elem));
- var uniqueAttributes = parse.reduce(getUniqueAttributesReduction);
- if (uniqueAttributes.anonid && typeof(elementslib._byAnonAttrib(_document,
- elem.parentNode, {'anonid':uniqueAttributes.anonid})) != 'array') {
- uniqueAttributes = {'anonid':uniqueAttributes.anonid};
- }
-
- if (objects.getLength(uniqueAttributes) == 0) {
- return 'anon(['+arrays.indexOf(_document.getAnonymousNodes(elem.parentNode), elem)+'])';
- } else if (arrays.inArray(uniqueAttributes, 'anonid')) {
- return 'anon({"anonid":"'+uniqueAttributes['anonid']+'"})';
- } else {
- return 'anon('+json2.JSON.stringify(uniqueAttributes)+')';
- }
-
- }
- return 'broken '+elemIsAnonymous(elem)
-}
-
-var removeHTMLTags = function(str){
- str = str.replace(/&(lt|gt);/g, function (strMatch, p1){
- return (p1 == "lt")? "<" : ">";
- });
- var strTagStrippedText = str.replace(/<\/?[^>]+(>|$)/g, "");
- strTagStrippedText = strTagStrippedText.replace(/&nbsp;/g,"");
- return strTagStrippedText;
-}
-
-var isMagicAnonymousDiv = function (_document, node) {
- if (node.getAttribute && node.getAttribute('class') == 'anonymous-div') {
- if (!arrays.inArray(node.parentNode.childNodes, node) && (_document.getAnonymousNodes(node) == null ||
- !arrays.inArray(_document.getAnonymousNodes(node), node) ) ) {
- return true;
- }
- }
- return false;
-}
-
-var copyToClipboard = function(str){
- const gClipboardHelper = Components.classes["@mozilla.org/widget/clipboardhelper;1"] .getService(Components.interfaces.nsIClipboardHelper);
- gClipboardHelper.copyString(str, _window.document);
-}
-
-var getControllerAndDocument = function (_document, _window) {
- var windowtype = _window.document.documentElement.getAttribute('windowtype');
- var controllerString, documentString, activeTab;
-
- // TODO replace with object based cases
- switch(windowtype) {
- case 'navigator:browser':
- controllerString = 'mozmill.getBrowserController()';
- activeTab = mozmill.getBrowserController().tabs.activeTab;
- break;
- case 'Browser:Preferences':
- controllerString = 'mozmill.getPreferencesController()';
- break;
- case 'Extension:Manager':
- controllerString = 'mozmill.getAddonsController()';
- break;
- default:
- if(windowtype)
- controllerString = 'new mozmill.controller.MozMillController(mozmill.utils.getWindowByType("' + windowtype + '"))';
- else if(_window.document.title)
- controllerString = 'new mozmill.controller.MozMillController(mozmill.utils.getWindowByTitle("'+_window.document.title+'"))';
- else
- controllerString = 'Cannot find window';
- break;
- }
-
- if(activeTab == _document) {
- documentString = 'controller.tabs.activeTab';
- } else if(activeTab == _document.defaultView.top.document) {
- // if this document is from an iframe in the active tab
- var stub = getDocumentStub(_document, activeTab.defaultView);
- documentString = 'controller.tabs.activeTab.defaultView' + stub;
- } else {
- var stub = getDocumentStub(_document, _window);
- if(stub)
- documentString = 'controller.window' + stub;
- else
- documentString = 'Cannot find document';
- }
- return {'controllerString':controllerString, 'documentString':documentString}
-}
-
-getDocumentStub = function( _document, _window) {
- if(_window.document == _document)
- return '.document';
- for(var i = 0; i < _window.frames.length; i++) {
- var stub = getDocumentStub(_document, _window.frames[i]);
- if (stub)
- return '.frames['+i+']' + stub;
- }
- return '';
-}
-
-var inspectElement = function(e){
- if (e.originalTarget != undefined) {
- target = e.originalTarget;
- } else {
- target = e.target;
- }
-
- //Element highlighting
- try {
- if (this.lastEvent)
- this.lastEvent.target.style.outline = "";
- } catch(err) {}
-
- this.lastEvent = e;
-
- try {
- e.target.style.outline = "1px solid darkblue";
- } catch(err){}
-
- var _document = getDocument(target);
-
-
- if (isMagicAnonymousDiv(_document, target)) {
- target = target.parentNode;
- }
-
- var windowtype = _document.documentElement.getAttribute('windowtype');
- var _window = getTopWindow(_document);
- r = getControllerAndDocument(_document, _window);
-
- // displayText = "Controller: " + r.controllerString + '\n\n';
- if ( isNotAnonymous(target) ) {
- // Logic for which identifier to use is duplicated above
- if (target.id != "" && !withs.startsWith(target.id, 'panel')) {
- elemText = "new elementslib.ID("+ r.documentString + ', "' + target.id + '")';
- var telem = new elementslib.ID(_document, target.id);
- } else if ((target.name != "") && (typeof(target.name) != "undefined")) {
- elemText = "new elementslib.Name("+ r.documentString + ', "' + target.name + '")';
- var telem = new elementslib.Name(_document, target.name);
- } else if (target.nodeName == "A") {
- var linkText = removeHTMLTags(target.innerHTML);
- elemText = "new elementslib.Link("+ r.documentString + ', "' + linkText + '")';
- var telem = new elementslib.Link(_document, linkText);
- }
- }
- // Fallback on XPath
- if (telem == undefined || telem.getNode() != target) {
- if (windowtype == null) {
- var stringXpath = getXSPath(target);
- } else {
- var stringXpath = getXULXpath(target, _document);
- }
- var telem = new elementslib.XPath(_document, stringXpath);
- if ( telem.getNode() == target ) {
- elemText = "new elementslib.XPath("+ r.documentString + ', "' + stringXpath + '")';
- }
- }
- // Fallback to Lookup
- if (telem == undefined || telem.getNode() != target) {
- var exp = getLookupExpression(_document, target);
- elemText = "new elementslib.Lookup("+ r.documentString + ", '" + exp + "')";
- var telem = new elementslib.Lookup(_document, exp);
- }
-
- return {'validation':( target == telem.getNode() ),
- 'elementText':elemText,
- 'elementType':telem.constructor.name,
- 'controllerText':r.controllerString,
- 'documentString':r.documentString,
- }
-}
-
-
-
diff --git a/services/sync/tps/extensions/mozmill/resource/modules/jum.js b/services/sync/tps/extensions/mozmill/resource/modules/jum.js
deleted file mode 100644
index b451a97a0..000000000
--- a/services/sync/tps/extensions/mozmill/resource/modules/jum.js
+++ /dev/null
@@ -1,231 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-var EXPORTED_SYMBOLS = ["assert", "assertTrue", "assertFalse", "assertEquals", "assertNotEquals",
- "assertNull", "assertNotNull", "assertUndefined", "assertNotUndefined",
- "assertNaN", "assertNotNaN", "assertArrayContains", "fail", "pass"];
-
-
-// Array.isArray comes with JavaScript 1.8.5 (Firefox 4)
-// cf. https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Array/isArray
-Array.isArray = Array.isArray || function(o) { return Object.prototype.toString.call(o) === '[object Array]'; };
-
-var frame = {}; Components.utils.import("resource://mozmill/modules/frame.js", frame);
-
-var ifJSONable = function (v) {
- if (typeof(v) == 'function') {
- return undefined;
- } else {
- return v;
- }
-}
-
-var assert = function (booleanValue, comment) {
- if (booleanValue) {
- frame.events.pass({'function':'jum.assert', 'value':ifJSONable(booleanValue), 'comment':comment});
- return true;
- } else {
- frame.events.fail({'function':'jum.assert', 'value':ifJSONable(booleanValue), 'comment':comment});
- return false;
- }
-}
-
-var assertTrue = function (booleanValue, comment) {
- if (typeof(booleanValue) != 'boolean') {
- frame.events.fail({'function':'jum.assertTrue', 'value':ifJSONable(booleanValue),
- 'message':'Bad argument, value type '+typeof(booleanValue)+' != "boolean"',
- 'comment':comment});
- return false;
- }
-
- if (booleanValue) {
- frame.events.pass({'function':'jum.assertTrue', 'value':ifJSONable(booleanValue),
- 'comment':comment});
- return true;
- } else {
- frame.events.fail({'function':'jum.assertTrue', 'value':ifJSONable(booleanValue),
- 'comment':comment});
- return false;
- }
-}
-
-var assertFalse = function (booleanValue, comment) {
- if (typeof(booleanValue) != 'boolean') {
- frame.events.fail({'function':'jum.assertFalse', 'value':ifJSONable(booleanValue),
- 'message':'Bad argument, value type '+typeof(booleanValue)+' != "boolean"',
- 'comment':comment});
- return false;
- }
-
- if (!booleanValue) {
- frame.events.pass({'function':'jum.assertFalse', 'value':ifJSONable(booleanValue),
- 'comment':comment});
- return true;
- } else {
- frame.events.fail({'function':'jum.assertFalse', 'value':ifJSONable(booleanValue),
- 'comment':comment});
- return false;
- }
-}
-
-var assertEquals = function (value1, value2, comment) {
- // Case where value1 is an array
- if (Array.isArray(value1)) {
-
- if (!Array.isArray(value2)) {
- frame.events.fail({'function':'jum.assertEquals', 'comment':comment,
- 'message':'Bad argument, value1 is an array and value2 type ' +
- typeof(value2)+' != "array"',
- 'value2':ifJSONable(value2)});
- return false;
- }
-
- if (value1.length != value2.length) {
- frame.events.fail({'function':'jum.assertEquals', 'comment':comment,
- 'message':"The arrays do not have the same length",
- 'value1':ifJSONable(value1), 'value2':ifJSONable(value2)});
- return false;
- }
-
- for (var i = 0; i < value1.length; i++) {
- if (value1[i] !== value2[i]) {
- frame.events.fail(
- {'function':'jum.assertEquals', 'comment':comment,
- 'message':"The element of the arrays are different at index " + i,
- 'value1':ifJSONable(value1), 'value2':ifJSONable(value2)});
- return false;
- }
- }
- frame.events.pass({'function':'jum.assertEquals', 'comment':comment,
- 'value1':ifJSONable(value1), 'value2':ifJSONable(value2)});
- return true;
- }
-
- // Case where value1 is not an array
- if (value1 == value2) {
- frame.events.pass({'function':'jum.assertEquals', 'comment':comment,
- 'value1':ifJSONable(value1), 'value2':ifJSONable(value2)});
- return true;
- } else {
- frame.events.fail({'function':'jum.assertEquals', 'comment':comment,
- 'value1':ifJSONable(value1), 'value2':ifJSONable(value2)});
- return false;
- }
-}
-
-var assertNotEquals = function (value1, value2, comment) {
- if (value1 != value2) {
- frame.events.pass({'function':'jum.assertNotEquals', 'comment':comment,
- 'value1':ifJSONable(value1), 'value2':ifJSONable(value2)});
- return true;
- } else {
- frame.events.fail({'function':'jum.assertNotEquals', 'comment':comment,
- 'value1':ifJSONable(value1), 'value2':ifJSONable(value2)});
- return false;
- }
-}
-
-var assertNull = function (value, comment) {
- if (value == null) {
- frame.events.pass({'function':'jum.assertNull', 'comment':comment,
- 'value':ifJSONable(value)});
- return true;
- } else {
- frame.events.fail({'function':'jum.assertNull', 'comment':comment,
- 'value':ifJSONable(value)});
- return false;
- }
-}
-
-var assertNotNull = function (value, comment) {
- if (value != null) {
- frame.events.pass({'function':'jum.assertNotNull', 'comment':comment,
- 'value':ifJSONable(value)});
- return true;
- } else {
- frame.events.fail({'function':'jum.assertNotNull', 'comment':comment,
- 'value':ifJSONable(value)});
- return false;
- }
-}
-
-var assertUndefined = function (value, comment) {
- if (value == undefined) {
- frame.events.pass({'function':'jum.assertUndefined', 'comment':comment,
- 'value':ifJSONable(value)});
- return true;
- } else {
- frame.events.fail({'function':'jum.assertUndefined', 'comment':comment,
- 'value':ifJSONable(value)});
- return false;
- }
-}
-
-var assertNotUndefined = function (value, comment) {
- if (value != undefined) {
- frame.events.pass({'function':'jum.assertNotUndefined', 'comment':comment,
- 'value':ifJSONable(value)});
- return true;
- } else {
- frame.events.fail({'function':'jum.assertNotUndefined', 'comment':comment,
- 'value':ifJSONable(value)});
- return false;
- }
-}
-
-var assertNaN = function (value, comment) {
- if (isNaN(value)) {
- frame.events.pass({'function':'jum.assertNaN', 'comment':comment,
- 'value':ifJSONable(value)});
- return true;
- } else {
- frame.events.fail({'function':'jum.assertNaN', 'comment':comment,
- 'value':ifJSONable(value)});
- return false;
- }
-}
-
-var assertNotNaN = function (value, comment) {
- if (!isNaN(value)) {
- frame.events.pass({'function':'jum.assertNotNaN', 'comment':comment,
- 'value':ifJSONable(value)});
- return true;
- } else {
- frame.events.fail({'function':'jum.assertNotNaN', 'comment':comment,
- 'value':ifJSONable(value)});
- return false;
- }
-}
-
-var assertArrayContains = function(array, value, comment) {
- if (!Array.isArray(array)) {
- frame.events.fail({'function':'jum.assertArrayContains', 'comment':comment,
- 'message':'Bad argument, value type '+typeof(array)+' != "array"',
- 'value':ifJSONable(array)});
- return false;
- }
-
- for (var i = 0; i < array.length; i++) {
- if (array[i] === value) {
- frame.events.pass({'function':'jum.assertArrayContains', 'comment':comment,
- 'value1':ifJSONable(array), 'value2':ifJSONable(value)});
- return true;
- }
- }
- frame.events.fail({'function':'jum.assertArrayContains', 'comment':comment,
- 'value1':ifJSONable(array), 'value2':ifJSONable(value)});
- return false;
-}
-
-var fail = function (comment) {
- frame.events.fail({'function':'jum.fail', 'comment':comment});
- return false;
-}
-
-var pass = function (comment) {
- frame.events.pass({'function':'jum.pass', 'comment':comment});
- return true;
-}
-
-
diff --git a/services/sync/tps/extensions/mozmill/resource/modules/l10n.js b/services/sync/tps/extensions/mozmill/resource/modules/l10n.js
index c764f7a71..63a355421 100644
--- a/services/sync/tps/extensions/mozmill/resource/modules/l10n.js
+++ b/services/sync/tps/extensions/mozmill/resource/modules/l10n.js
@@ -1,12 +1,14 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+ * file, you can obtain one at http://mozilla.org/MPL/2.0/. */
/**
* @namespace Defines useful methods to work with localized content
*/
var l10n = exports;
+Cu.import("resource://gre/modules/Services.jsm");
+
/**
* Retrieve the localized content for a given DTD entity
*
@@ -54,14 +56,11 @@ function getEntity(aDTDs, aEntityId) {
* @returns {String} Value of the requested property
*/
function getProperty(aURL, aProperty) {
- var sbs = Cc["@mozilla.org/intl/stringbundle;1"].
- getService(Ci.nsIStringBundleService);
- var bundle = sbs.createBundle(aURL);
+ var bundle = Services.strings.createBundle(aURL);
try {
return bundle.GetStringFromName(aProperty);
- }
- catch (ex) {
+ } catch (ex) {
throw new Error("Unkown property '" + aProperty + "'");
}
}
diff --git a/services/sync/tps/extensions/mozmill/resource/modules/mozelement.js b/services/sync/tps/extensions/mozmill/resource/modules/mozelement.js
deleted file mode 100644
index 07b122d24..000000000
--- a/services/sync/tps/extensions/mozmill/resource/modules/mozelement.js
+++ /dev/null
@@ -1,668 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-var EXPORTED_SYMBOLS = ["Elem", "Selector", "ID", "Link", "XPath", "Name", "Lookup",
- "MozMillElement", "MozMillCheckBox", "MozMillRadio", "MozMillDropList",
- "MozMillTextBox", "subclasses",
- ];
-
-var EventUtils = {}; Components.utils.import('resource://mozmill/stdlib/EventUtils.js', EventUtils);
-var frame = {}; Components.utils.import('resource://mozmill/modules/frame.js', frame);
-var utils = {}; Components.utils.import('resource://mozmill/modules/utils.js', utils);
-var elementslib = {}; Components.utils.import('resource://mozmill/modules/elementslib.js', elementslib);
-
-// A list of all the subclasses available. Shared modules can push their own subclasses onto this list
-var subclasses = [MozMillCheckBox, MozMillRadio, MozMillDropList, MozMillTextBox];
-
-/**
- * createInstance()
- *
- * Returns an new instance of a MozMillElement
- * The type of the element is automatically determined
- */
-function createInstance(locatorType, locator, elem) {
- if (elem) {
- var args = {"element":elem};
- for (var i = 0; i < subclasses.length; ++i) {
- if (subclasses[i].isType(elem)) {
- return new subclasses[i](locatorType, locator, args);
- }
- }
- if (MozMillElement.isType(elem)) return new MozMillElement(locatorType, locator, args);
- }
- throw new Error("could not find element " + locatorType + ": " + locator);
-};
-
-var Elem = function(node) {
- return createInstance("Elem", node, node);
-};
-
-var Selector = function(_document, selector, index) {
- return createInstance("Selector", selector, elementslib.Selector(_document, selector, index));
-};
-
-var ID = function(_document, nodeID) {
- return createInstance("ID", nodeID, elementslib.ID(_document, nodeID));
-};
-
-var Link = function(_document, linkName) {
- return createInstance("Link", linkName, elementslib.Link(_document, linkName));
-};
-
-var XPath = function(_document, expr) {
- return createInstance("XPath", expr, elementslib.XPath(_document, expr));
-};
-
-var Name = function(_document, nName) {
- return createInstance("Name", nName, elementslib.Name(_document, nName));
-};
-
-var Lookup = function(_document, expression) {
- return createInstance("Lookup", expression, elementslib.Lookup(_document, expression));
-};
-
-
-/**
- * MozMillElement
- * The base class for all mozmill elements
- */
-function MozMillElement(locatorType, locator, args) {
- args = args || {};
- this._locatorType = locatorType;
- this._locator = locator;
- this._element = args["element"];
- this._document = args["document"];
- this._owner = args["owner"];
- // Used to maintain backwards compatibility with controller.js
- this.isElement = true;
-}
-
-// Static method that returns true if node is of this element type
-MozMillElement.isType = function(node) {
- return true;
-};
-
-// This getter is the magic behind lazy loading (note distinction between _element and element)
-MozMillElement.prototype.__defineGetter__("element", function() {
- if (this._element == undefined) {
- if (elementslib[this._locatorType]) {
- this._element = elementslib[this._locatorType](this._document, this._locator);
- } else if (this._locatorType == "Elem") {
- this._element = this._locator;
- } else {
- throw new Error("Unknown locator type: " + this._locatorType);
- }
- }
- return this._element;
-});
-
-// Returns the actual wrapped DOM node
-MozMillElement.prototype.getNode = function() {
- return this.element;
-};
-
-MozMillElement.prototype.getInfo = function() {
- return this._locatorType + ": " + this._locator;
-};
-
-/**
- * Sometimes an element which once existed will no longer exist in the DOM
- * This function re-searches for the element
- */
-MozMillElement.prototype.exists = function() {
- this._element = undefined;
- if (this.element) return true;
- return false;
-};
-
-/**
- * Synthesize a keypress event on the given element
- *
- * @param {string} aKey
- * Key to use for synthesizing the keypress event. It can be a simple
- * character like "k" or a string like "VK_ESCAPE" for command keys
- * @param {object} aModifiers
- * Information about the modifier keys to send
- * Elements: accelKey - Hold down the accelerator key (ctrl/meta)
- * [optional - default: false]
- * altKey - Hold down the alt key
- * [optional - default: false]
- * ctrlKey - Hold down the ctrl key
- * [optional - default: false]
- * metaKey - Hold down the meta key (command key on Mac)
- * [optional - default: false]
- * shiftKey - Hold down the shift key
- * [optional - default: false]
- * @param {object} aExpectedEvent
- * Information about the expected event to occur
- * Elements: target - Element which should receive the event
- * [optional - default: current element]
- * type - Type of the expected key event
- */
-MozMillElement.prototype.keypress = function(aKey, aModifiers, aExpectedEvent) {
- if (!this.element) {
- throw new Error("Could not find element " + this.getInfo());
- }
-
- var win = this.element.ownerDocument? this.element.ownerDocument.defaultView : this.element;
- this.element.focus();
-
- if (aExpectedEvent) {
- var target = aExpectedEvent.target? aExpectedEvent.target.getNode() : this.element;
- EventUtils.synthesizeKeyExpectEvent(aKey, aModifiers || {}, target, aExpectedEvent.type,
- "MozMillElement.keypress()", win);
- } else {
- EventUtils.synthesizeKey(aKey, aModifiers || {}, win);
- }
-
- frame.events.pass({'function':'MozMillElement.keypress()'});
- return true;
-};
-
-
-/**
- * Synthesize a general mouse event on the given element
- *
- * @param {ElemBase} aTarget
- * Element which will receive the mouse event
- * @param {number} aOffsetX
- * Relative x offset in the elements bounds to click on
- * @param {number} aOffsetY
- * Relative y offset in the elements bounds to click on
- * @param {object} aEvent
- * Information about the event to send
- * Elements: accelKey - Hold down the accelerator key (ctrl/meta)
- * [optional - default: false]
- * altKey - Hold down the alt key
- * [optional - default: false]
- * button - Mouse button to use
- * [optional - default: 0]
- * clickCount - Number of counts to click
- * [optional - default: 1]
- * ctrlKey - Hold down the ctrl key
- * [optional - default: false]
- * metaKey - Hold down the meta key (command key on Mac)
- * [optional - default: false]
- * shiftKey - Hold down the shift key
- * [optional - default: false]
- * type - Type of the mouse event ('click', 'mousedown',
- * 'mouseup', 'mouseover', 'mouseout')
- * [optional - default: 'mousedown' + 'mouseup']
- * @param {object} aExpectedEvent
- * Information about the expected event to occur
- * Elements: target - Element which should receive the event
- * [optional - default: current element]
- * type - Type of the expected mouse event
- */
-MozMillElement.prototype.mouseEvent = function(aOffsetX, aOffsetY, aEvent, aExpectedEvent) {
- if (!this.element) {
- throw new Error(arguments.callee.name + ": could not find element " + this.getInfo());
- }
-
- // If no offset is given we will use the center of the element to click on.
- var rect = this.element.getBoundingClientRect();
- if (isNaN(aOffsetX)) {
- aOffsetX = rect.width / 2;
- }
- if (isNaN(aOffsetY)) {
- aOffsetY = rect.height / 2;
- }
-
- // Scroll element into view otherwise the click will fail
- if (this.element.scrollIntoView) {
- this.element.scrollIntoView();
- }
-
- if (aExpectedEvent) {
- // The expected event type has to be set
- if (!aExpectedEvent.type)
- throw new Error(arguments.callee.name + ": Expected event type not specified");
-
- // If no target has been specified use the specified element
- var target = aExpectedEvent.target ? aExpectedEvent.target.getNode() : this.element;
- if (!target) {
- throw new Error(arguments.callee.name + ": could not find element " + aExpectedEvent.target.getInfo());
- }
-
- EventUtils.synthesizeMouseExpectEvent(this.element, aOffsetX, aOffsetY, aEvent,
- target, aExpectedEvent.event,
- "MozMillElement.mouseEvent()",
- this.element.ownerDocument.defaultView);
- } else {
- EventUtils.synthesizeMouse(this.element, aOffsetX, aOffsetY, aEvent,
- this.element.ownerDocument.defaultView);
- }
-};
-
-/**
- * Synthesize a mouse click event on the given element
- */
-MozMillElement.prototype.click = function(left, top, expectedEvent) {
- // Handle menu items differently
- if (this.element && this.element.tagName == "menuitem") {
- this.element.click();
- } else {
- this.mouseEvent(left, top, {}, expectedEvent);
- }
-
- frame.events.pass({'function':'MozMillElement.click()'});
-};
-
-/**
- * Synthesize a double click on the given element
- */
-MozMillElement.prototype.doubleClick = function(left, top, expectedEvent) {
- this.mouseEvent(left, top, {clickCount: 2}, expectedEvent);
-
- frame.events.pass({'function':'MozMillElement.doubleClick()'});
- return true;
-};
-
-/**
- * Synthesize a mouse down event on the given element
- */
-MozMillElement.prototype.mouseDown = function (button, left, top, expectedEvent) {
- this.mouseEvent(left, top, {button: button, type: "mousedown"}, expectedEvent);
-
- frame.events.pass({'function':'MozMillElement.mouseDown()'});
- return true;
-};
-
-/**
- * Synthesize a mouse out event on the given element
- */
-MozMillElement.prototype.mouseOut = function (button, left, top, expectedEvent) {
- this.mouseEvent(left, top, {button: button, type: "mouseout"}, expectedEvent);
-
- frame.events.pass({'function':'MozMillElement.mouseOut()'});
- return true;
-};
-
-/**
- * Synthesize a mouse over event on the given element
- */
-MozMillElement.prototype.mouseOver = function (button, left, top, expectedEvent) {
- this.mouseEvent(left, top, {button: button, type: "mouseover"}, expectedEvent);
-
- frame.events.pass({'function':'MozMillElement.mouseOver()'});
- return true;
-};
-
-/**
- * Synthesize a mouse up event on the given element
- */
-MozMillElement.prototype.mouseUp = function (button, left, top, expectedEvent) {
- this.mouseEvent(left, top, {button: button, type: "mouseup"}, expectedEvent);
-
- frame.events.pass({'function':'MozMillElement.mouseUp()'});
- return true;
-};
-
-/**
- * Synthesize a mouse middle click event on the given element
- */
-MozMillElement.prototype.middleClick = function(left, top, expectedEvent) {
- this.mouseEvent(left, top, {button: 1}, expectedEvent);
-
- frame.events.pass({'function':'MozMillElement.middleClick()'});
- return true;
-};
-
-/**
- * Synthesize a mouse right click event on the given element
- */
-MozMillElement.prototype.rightClick = function(left, top, expectedEvent) {
- this.mouseEvent(left, top, {type : "contextmenu", button: 2 }, expectedEvent);
-
- frame.events.pass({'function':'MozMillElement.rightClick()'});
- return true;
-};
-
-MozMillElement.prototype.waitForElement = function(timeout, interval) {
- var elem = this;
- utils.waitFor(function() {
- return elem.exists();
- }, "Timeout exceeded for waitForElement " + this.getInfo(), timeout, interval);
-
- frame.events.pass({'function':'MozMillElement.waitForElement()'});
-};
-
-MozMillElement.prototype.waitForElementNotPresent = function(timeout, interval) {
- var elem = this;
- utils.waitFor(function() {
- return !elem.exists();
- }, "Timeout exceeded for waitForElementNotPresent " + this.getInfo(), timeout, interval);
-
- frame.events.pass({'function':'MozMillElement.waitForElementNotPresent()'});
-};
-
-MozMillElement.prototype.waitThenClick = function (timeout, interval, left, top, expectedEvent) {
- this.waitForElement(timeout, interval);
- this.click(left, top, expectedEvent);
-};
-
-// Dispatches an HTMLEvent
-MozMillElement.prototype.dispatchEvent = function (eventType, canBubble, modifiers) {
- canBubble = canBubble || true;
- var evt = this.element.ownerDocument.createEvent('HTMLEvents');
- evt.shiftKey = modifiers["shift"];
- evt.metaKey = modifiers["meta"];
- evt.altKey = modifiers["alt"];
- evt.ctrlKey = modifiers["ctrl"];
- evt.initEvent(eventType, canBubble, true);
- this.element.dispatchEvent(evt);
-};
-
-
-//---------------------------------------------------------------------------------------------------------------------------------------
-
-
-/**
- * MozMillCheckBox
- * Checkbox element, inherits from MozMillElement
- */
-MozMillCheckBox.prototype = new MozMillElement();
-MozMillCheckBox.prototype.parent = MozMillElement.prototype;
-MozMillCheckBox.prototype.constructor = MozMillCheckBox;
-function MozMillCheckBox(locatorType, locator, args) {
- this.parent.constructor.call(this, locatorType, locator, args);
-}
-
-// Static method returns true if node is this type of element
-MozMillCheckBox.isType = function(node) {
- if ((node.localName.toLowerCase() == "input" && node.getAttribute("type") == "checkbox") ||
- (node.localName.toLowerCase() == 'toolbarbutton' && node.getAttribute('type') == 'checkbox') ||
- (node.localName.toLowerCase() == 'checkbox')) {
- return true;
- }
- return false;
-};
-
-/**
- * Enable/Disable a checkbox depending on the target state
- */
-MozMillCheckBox.prototype.check = function(state) {
- var result = false;
-
- if (!this.element) {
- throw new Error("could not find element " + this.getInfo());
- return false;
- }
-
- // If we have a XUL element, unwrap its XPCNativeWrapper
- if (this.element.namespaceURI == "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul") {
- this.element = utils.unwrapNode(this.element);
- }
-
- state = (typeof(state) == "boolean") ? state : false;
- if (state != this.element.checked) {
- this.click();
- var element = this.element;
- utils.waitFor(function() {
- return element.checked == state;
- }, "Checkbox " + this.getInfo() + " could not be checked/unchecked", 500);
-
- result = true;
- }
-
- frame.events.pass({'function':'MozMillCheckBox.check(' + this.getInfo() + ', state: ' + state + ')'});
- return result;
-};
-
-//----------------------------------------------------------------------------------------------------------------------------------------
-
-
-/**
- * MozMillRadio
- * Radio button inherits from MozMillElement
- */
-MozMillRadio.prototype = new MozMillElement();
-MozMillRadio.prototype.parent = MozMillElement.prototype;
-MozMillRadio.prototype.constructor = MozMillRadio;
-function MozMillRadio(locatorType, locator, args) {
- this.parent.constructor.call(this, locatorType, locator, args);
-}
-
-// Static method returns true if node is this type of element
-MozMillRadio.isType = function(node) {
- if ((node.localName.toLowerCase() == 'input' && node.getAttribute('type') == 'radio') ||
- (node.localName.toLowerCase() == 'toolbarbutton' && node.getAttribute('type') == 'radio') ||
- (node.localName.toLowerCase() == 'radio') ||
- (node.localName.toLowerCase() == 'radiogroup')) {
- return true;
- }
- return false;
-};
-
-/**
- * Select the given radio button
- *
- * index - Specifies which radio button in the group to select (only applicable to radiogroup elements)
- * Defaults to the first radio button in the group
- */
-MozMillRadio.prototype.select = function(index) {
- if (!this.element) {
- throw new Error("could not find element " + this.getInfo());
- }
-
- if (this.element.localName.toLowerCase() == "radiogroup") {
- var element = this.element.getElementsByTagName("radio")[index || 0];
- new MozMillRadio("Elem", element).click();
- } else {
- var element = this.element;
- this.click();
- }
-
- utils.waitFor(function() {
- // If we have a XUL element, unwrap its XPCNativeWrapper
- if (element.namespaceURI == "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul") {
- element = utils.unwrapNode(element);
- return element.selected == true;
- }
- return element.checked == true;
- }, "Radio button " + this.getInfo() + " could not be selected", 500);
-
- frame.events.pass({'function':'MozMillRadio.select(' + this.getInfo() + ')'});
- return true;
-};
-
-//----------------------------------------------------------------------------------------------------------------------------------------
-
-
-/**
- * MozMillDropList
- * DropList inherits from MozMillElement
- */
-MozMillDropList.prototype = new MozMillElement();
-MozMillDropList.prototype.parent = MozMillElement.prototype;
-MozMillDropList.prototype.constructor = MozMillDropList;
-function MozMillDropList(locatorType, locator, args) {
- this.parent.constructor.call(this, locatorType, locator, args);
-};
-
-// Static method returns true if node is this type of element
-MozMillDropList.isType = function(node) {
- if ((node.localName.toLowerCase() == 'toolbarbutton' && (node.getAttribute('type') == 'menu' || node.getAttribute('type') == 'menu-button')) ||
- (node.localName.toLowerCase() == 'menu') ||
- (node.localName.toLowerCase() == 'menulist') ||
- (node.localName.toLowerCase() == 'select' )) {
- return true;
- }
- return false;
-};
-
-/* Select the specified option and trigger the relevant events of the element */
-MozMillDropList.prototype.select = function (indx, option, value) {
- if (!this.element){
- throw new Error("Could not find element " + this.getInfo());
- }
-
- //if we have a select drop down
- if (this.element.localName.toLowerCase() == "select"){
- var item = null;
-
- // The selected item should be set via its index
- if (indx != undefined) {
- // Resetting a menulist has to be handled separately
- if (indx == -1) {
- this.dispatchEvent('focus', false);
- this.element.selectedIndex = indx;
- this.dispatchEvent('change', true);
-
- frame.events.pass({'function':'MozMillDropList.select()'});
- return true;
- } else {
- item = this.element.options.item(indx);
- }
- } else {
- for (var i = 0; i < this.element.options.length; i++) {
- var entry = this.element.options.item(i);
- if (option != undefined && entry.innerHTML == option ||
- value != undefined && entry.value == value) {
- item = entry;
- break;
- }
- }
- }
-
- // Click the item
- try {
- // EventUtils.synthesizeMouse doesn't work.
- this.dispatchEvent('focus', false);
- item.selected = true;
- this.dispatchEvent('change', true);
-
- frame.events.pass({'function':'MozMillDropList.select()'});
- return true;
- } catch (ex) {
- throw new Error("No item selected for element " + this.getInfo());
- return false;
- }
- }
- //if we have a xul menupopup select accordingly
- else if (this.element.namespaceURI.toLowerCase() == "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul") {
- var ownerDoc = this.element.ownerDocument;
- // Unwrap the XUL element's XPCNativeWrapper
- this.element = utils.unwrapNode(this.element);
- // Get the list of menuitems
- menuitems = this.element.getElementsByTagName("menupopup")[0].getElementsByTagName("menuitem");
-
- var item = null;
-
- if (indx != undefined) {
- if (indx == -1) {
- this.dispatchEvent('focus', false);
- this.element.boxObject.QueryInterface(Components.interfaces.nsIMenuBoxObject).activeChild = null;
- this.dispatchEvent('change', true);
-
- frame.events.pass({'function':'MozMillDropList.select()'});
- return true;
- } else {
- item = menuitems[indx];
- }
- } else {
- for (var i = 0; i < menuitems.length; i++) {
- var entry = menuitems[i];
- if (option != undefined && entry.label == option ||
- value != undefined && entry.value == value) {
- item = entry;
- break;
- }
- }
- }
-
- // Click the item
- try {
- EventUtils.synthesizeMouse(this.element, 1, 1, {}, ownerDoc.defaultView);
-
- // Scroll down until item is visible
- for (var i = 0; i <= menuitems.length; ++i) {
- var selected = this.element.boxObject.QueryInterface(Components.interfaces.nsIMenuBoxObject).activeChild;
- if (item == selected) {
- break;
- }
- EventUtils.synthesizeKey("VK_DOWN", {}, ownerDoc.defaultView);
- }
-
- EventUtils.synthesizeMouse(item, 1, 1, {}, ownerDoc.defaultView);
-
- frame.events.pass({'function':'MozMillDropList.select()'});
- return true;
- } catch (ex) {
- throw new Error('No item selected for element ' + this.getInfo());
- return false;
- }
- }
-};
-
-
-//----------------------------------------------------------------------------------------------------------------------------------------
-
-
-/**
- * MozMillTextBox
- * TextBox inherits from MozMillElement
- */
-MozMillTextBox.prototype = new MozMillElement();
-MozMillTextBox.prototype.parent = MozMillElement.prototype;
-MozMillTextBox.prototype.constructor = MozMillTextBox;
-function MozMillTextBox(locatorType, locator, args) {
- this.parent.constructor.call(this, locatorType, locator, args);
-};
-
-// Static method returns true if node is this type of element
-MozMillTextBox.isType = function(node) {
- if ((node.localName.toLowerCase() == 'input' && (node.getAttribute('type') == 'text' || node.getAttribute('type') == 'search')) ||
- (node.localName.toLowerCase() == 'textarea') ||
- (node.localName.toLowerCase() == 'textbox')) {
- return true;
- }
- return false;
-};
-
-/**
- * Synthesize keypress events for each character on the given element
- *
- * @param {string} aText
- * The text to send as single keypress events
- * @param {object} aModifiers
- * Information about the modifier keys to send
- * Elements: accelKey - Hold down the accelerator key (ctrl/meta)
- * [optional - default: false]
- * altKey - Hold down the alt key
- * [optional - default: false]
- * ctrlKey - Hold down the ctrl key
- * [optional - default: false]
- * metaKey - Hold down the meta key (command key on Mac)
- * [optional - default: false]
- * shiftKey - Hold down the shift key
- * [optional - default: false]
- * @param {object} aExpectedEvent
- * Information about the expected event to occur
- * Elements: target - Element which should receive the event
- * [optional - default: current element]
- * type - Type of the expected key event
- */
-MozMillTextBox.prototype.sendKeys = function (aText, aModifiers, aExpectedEvent) {
- if (!this.element) {
- throw new Error("could not find element " + this.getInfo());
- }
-
- var element = this.element;
- Array.forEach(aText, function(letter) {
- var win = element.ownerDocument? element.ownerDocument.defaultView : element;
- element.focus();
-
- if (aExpectedEvent) {
- var target = aExpectedEvent.target ? aExpectedEvent.target.getNode() : element;
- EventUtils.synthesizeKeyExpectEvent(letter, aModifiers || {}, target, aExpectedEvent.type,
- "MozMillTextBox.sendKeys()", win);
- } else {
- EventUtils.synthesizeKey(letter, aModifiers || {}, win);
- }
- });
-
- frame.events.pass({'function':'MozMillTextBox.type()'});
- return true;
-};
diff --git a/services/sync/tps/extensions/mozmill/resource/modules/stack.js b/services/sync/tps/extensions/mozmill/resource/modules/stack.js
new file mode 100644
index 000000000..889316bf1
--- /dev/null
+++ b/services/sync/tps/extensions/mozmill/resource/modules/stack.js
@@ -0,0 +1,43 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, you can obtain one at http://mozilla.org/MPL/2.0/. */
+
+var EXPORTED_SYMBOLS = ['findCallerFrame'];
+
+
+/**
+ * @namespace Defines utility methods for handling stack frames
+ */
+
+/**
+ * Find the frame to use for logging the test result. If a start frame has
+ * been specified, we walk down the stack until a frame with the same filename
+ * as the start frame has been found. The next file in the stack will be the
+ * frame to use for logging the result.
+ *
+ * @memberOf stack
+ * @param {Object} [aStartFrame=Components.stack] Frame to start from walking up the stack.
+ * @returns {Object} Frame of the stack to use for logging the result.
+ */
+function findCallerFrame(aStartFrame) {
+ let frame = Components.stack;
+ let filename = frame.filename.replace(/(.*)-> /, "");
+
+ // If a start frame has been specified, walk up the stack until we have
+ // found the corresponding file
+ if (aStartFrame) {
+ filename = aStartFrame.filename.replace(/(.*)-> /, "");
+
+ while (frame.caller &&
+ frame.filename && (frame.filename.indexOf(filename) == -1)) {
+ frame = frame.caller;
+ }
+ }
+
+ // Walk even up more until the next file has been found
+ while (frame.caller &&
+ (!frame.filename || (frame.filename.indexOf(filename) != -1)))
+ frame = frame.caller;
+
+ return frame;
+}
diff --git a/services/sync/tps/extensions/mozmill/resource/modules/utils.js b/services/sync/tps/extensions/mozmill/resource/modules/utils.js
deleted file mode 100644
index 92b860f5a..000000000
--- a/services/sync/tps/extensions/mozmill/resource/modules/utils.js
+++ /dev/null
@@ -1,522 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-var EXPORTED_SYMBOLS = ["openFile", "saveFile", "saveAsFile", "genBoiler",
- "getFile", "Copy", "getChromeWindow", "getWindows", "runEditor",
- "runFile", "getWindowByTitle", "getWindowByType", "tempfile",
- "getMethodInWindows", "getPreference", "setPreference",
- "sleep", "assert", "unwrapNode", "TimeoutError", "waitFor",
- "takeScreenshot",
- ];
-
-var hwindow = Components.classes["@mozilla.org/appshell/appShellService;1"]
- .getService(Components.interfaces.nsIAppShellService)
- .hiddenDOMWindow;
-
-var uuidgen = Components.classes["@mozilla.org/uuid-generator;1"]
- .getService(Components.interfaces.nsIUUIDGenerator);
-
-function Copy (obj) {
- for (var n in obj) {
- this[n] = obj[n];
- }
-}
-
-function getChromeWindow(aWindow) {
- var chromeWin = aWindow
- .QueryInterface(Components.interfaces.nsIInterfaceRequestor)
- .getInterface(Components.interfaces.nsIWebNavigation)
- .QueryInterface(Components.interfaces.nsIDocShellTreeItem)
- .rootTreeItem
- .QueryInterface(Components.interfaces.nsIInterfaceRequestor)
- .getInterface(Components.interfaces.nsIDOMWindow)
- .QueryInterface(Components.interfaces.nsIDOMChromeWindow);
- return chromeWin;
-}
-
-function getWindows(type) {
- if (type == undefined) {
- type = "";
- }
- var windows = []
- var enumerator = Components.classes["@mozilla.org/appshell/window-mediator;1"]
- .getService(Components.interfaces.nsIWindowMediator)
- .getEnumerator(type);
- while(enumerator.hasMoreElements()) {
- windows.push(enumerator.getNext());
- }
- if (type == "") {
- windows.push(hwindow);
- }
- return windows;
-}
-
-function getMethodInWindows (methodName) {
- for each(w in getWindows()) {
- if (w[methodName] != undefined) {
- return w[methodName];
- }
- }
- throw new Error("Method with name: '" + methodName + "' is not in any open window.");
-}
-
-function getWindowByTitle(title) {
- for each(w in getWindows()) {
- if (w.document.title && w.document.title == title) {
- return w;
- }
- }
-}
-
-function getWindowByType(type) {
- var wm = Components.classes["@mozilla.org/appshell/window-mediator;1"]
- .getService(Components.interfaces.nsIWindowMediator);
- return wm.getMostRecentWindow(type);
-}
-
-function tempfile(appention) {
- if (appention == undefined) {
- var appention = "mozmill.utils.tempfile"
- }
- var tempfile = Components.classes["@mozilla.org/file/directory_service;1"].getService(Components.interfaces.nsIProperties).get("TmpD", Components.interfaces.nsIFile);
- tempfile.append(uuidgen.generateUUID().toString().replace('-', '').replace('{', '').replace('}',''))
- tempfile.create(Components.interfaces.nsIFile.DIRECTORY_TYPE, 0777);
- tempfile.append(appention);
- tempfile.createUnique(Components.interfaces.nsIFile.NORMAL_FILE_TYPE, 0666);
- // do whatever you need to the created file
- return tempfile.clone()
-}
-
-var checkChrome = function() {
- var loc = window.document.location.href;
- try {
- loc = window.top.document.location.href;
- } catch (e) {}
-
- if (/^chrome:\/\//.test(loc)) { return true; }
- else { return false; }
-}
-
-
- var runFile = function(w){
- //define the interface
- var nsIFilePicker = Components.interfaces.nsIFilePicker;
- var fp = Components.classes["@mozilla.org/filepicker;1"].createInstance(nsIFilePicker);
- //define the file picker window
- fp.init(w, "Select a File", nsIFilePicker.modeOpen);
- fp.appendFilter("JavaScript Files","*.js");
- //show the window
- var res = fp.show();
- //if we got a file
- if (res == nsIFilePicker.returnOK){
- var thefile = fp.file;
- //create the paramObj with a files array attrib
- var paramObj = {};
- paramObj.files = [];
- paramObj.files.push(thefile.path);
- }
- };
-
- var saveFile = function(w, content, filename){
- //define the file interface
- var file = Components.classes["@mozilla.org/file/local;1"]
- .createInstance(Components.interfaces.nsILocalFile);
- //point it at the file we want to get at
- file.initWithPath(filename);
-
- // file is nsIFile, data is a string
- var foStream = Components.classes["@mozilla.org/network/file-output-stream;1"]
- .createInstance(Components.interfaces.nsIFileOutputStream);
-
- // use 0x02 | 0x10 to open file for appending.
- foStream.init(file, 0x02 | 0x08 | 0x20, 0666, 0);
- // write, create, truncate
- // In a c file operation, we have no need to set file mode with or operation,
- // directly using "r" or "w" usually.
-
- foStream.write(content, content.length);
- foStream.close();
- };
-
- var saveAsFile = function(w, content){
- //define the interface
- var nsIFilePicker = Components.interfaces.nsIFilePicker;
- var fp = Components.classes["@mozilla.org/filepicker;1"].createInstance(nsIFilePicker);
- //define the file picker window
- fp.init(w, "Select a File", nsIFilePicker.modeSave);
- fp.appendFilter("JavaScript Files","*.js");
- //show the window
- var res = fp.show();
- //if we got a file
- if ((res == nsIFilePicker.returnOK) || (res == nsIFilePicker.returnReplace)){
- var thefile = fp.file;
-
- //forcing the user to save as a .js file
- if (thefile.path.indexOf(".js") == -1){
- //define the file interface
- var file = Components.classes["@mozilla.org/file/local;1"]
- .createInstance(Components.interfaces.nsILocalFile);
- //point it at the file we want to get at
- file.initWithPath(thefile.path+".js");
- var thefile = file;
- }
-
- // file is nsIFile, data is a string
- var foStream = Components.classes["@mozilla.org/network/file-output-stream;1"]
- .createInstance(Components.interfaces.nsIFileOutputStream);
-
- // use 0x02 | 0x10 to open file for appending.
- foStream.init(thefile, 0x02 | 0x08 | 0x20, 0666, 0);
- // write, create, truncate
- // In a c file operation, we have no need to set file mode with or operation,
- // directly using "r" or "w" usually.
- foStream.write(content, content.length);
- foStream.close();
- return thefile.path;
- }
- };
-
- var openFile = function(w){
- //define the interface
- var nsIFilePicker = Components.interfaces.nsIFilePicker;
- var fp = Components.classes["@mozilla.org/filepicker;1"].createInstance(nsIFilePicker);
- //define the file picker window
- fp.init(w, "Select a File", nsIFilePicker.modeOpen);
- fp.appendFilter("JavaScript Files","*.js");
- //show the window
- var res = fp.show();
- //if we got a file
- if (res == nsIFilePicker.returnOK){
- var thefile = fp.file;
- //create the paramObj with a files array attrib
- var data = getFile(thefile.path);
-
- return {path:thefile.path, data:data};
- }
- };
-
- var getFile = function(path){
- //define the file interface
- var file = Components.classes["@mozilla.org/file/local;1"]
- .createInstance(Components.interfaces.nsILocalFile);
- //point it at the file we want to get at
- file.initWithPath(path);
- // define file stream interfaces
- var data = "";
- var fstream = Components.classes["@mozilla.org/network/file-input-stream;1"]
- .createInstance(Components.interfaces.nsIFileInputStream);
- var sstream = Components.classes["@mozilla.org/scriptableinputstream;1"]
- .createInstance(Components.interfaces.nsIScriptableInputStream);
- fstream.init(file, -1, 0, 0);
- sstream.init(fstream);
-
- //pull the contents of the file out
- var str = sstream.read(4096);
- while (str.length > 0) {
- data += str;
- str = sstream.read(4096);
- }
-
- sstream.close();
- fstream.close();
-
- //data = data.replace(/\r|\n|\r\n/g, "");
- return data;
- };
-
-/**
- * Called to get the state of an individual preference.
- *
- * @param aPrefName string The preference to get the state of.
- * @param aDefaultValue any The default value if preference was not found.
- *
- * @returns any The value of the requested preference
- *
- * @see setPref
- * Code by Henrik Skupin: <hskupin@gmail.com>
- */
-function getPreference(aPrefName, aDefaultValue) {
- try {
- var branch = Components.classes["@mozilla.org/preferences-service;1"].
- getService(Components.interfaces.nsIPrefBranch);
- switch (typeof aDefaultValue) {
- case ('boolean'):
- return branch.getBoolPref(aPrefName);
- case ('string'):
- return branch.getCharPref(aPrefName);
- case ('number'):
- return branch.getIntPref(aPrefName);
- default:
- return branch.getComplexValue(aPrefName);
- }
- } catch(e) {
- return aDefaultValue;
- }
-}
-
-/**
- * Called to set the state of an individual preference.
- *
- * @param aPrefName string The preference to set the state of.
- * @param aValue any The value to set the preference to.
- *
- * @returns boolean Returns true if value was successfully set.
- *
- * @see getPref
- * Code by Henrik Skupin: <hskupin@gmail.com>
- */
-function setPreference(aName, aValue) {
- try {
- var branch = Components.classes["@mozilla.org/preferences-service;1"].
- getService(Components.interfaces.nsIPrefBranch);
- switch (typeof aValue) {
- case ('boolean'):
- branch.setBoolPref(aName, aValue);
- break;
- case ('string'):
- branch.setCharPref(aName, aValue);
- break;
- case ('number'):
- branch.setIntPref(aName, aValue);
- break;
- default:
- branch.setComplexValue(aName, aValue);
- }
- } catch(e) {
- return false;
- }
-
- return true;
-}
-
-/**
- * Sleep for the given amount of milliseconds
- *
- * @param {number} milliseconds
- * Sleeps the given number of milliseconds
- */
-function sleep(milliseconds) {
- // We basically just call this once after the specified number of milliseconds
- var timeup = false;
- function wait() { timeup = true; }
- hwindow.setTimeout(wait, milliseconds);
-
- var thread = Components.classes["@mozilla.org/thread-manager;1"].
- getService().currentThread;
- while(!timeup) {
- thread.processNextEvent(true);
- }
-}
-
-/**
- * Check if the callback function evaluates to true
- */
-function assert(callback, message, thisObject) {
- var result = callback.call(thisObject);
-
- if (!result) {
- throw new Error(message || arguments.callee.name + ": Failed for '" + callback + "'");
- }
-
- return true;
-}
-
-/**
- * Unwraps a node which is wrapped into a XPCNativeWrapper or XrayWrapper
- *
- * @param {DOMnode} Wrapped DOM node
- * @returns {DOMNode} Unwrapped DOM node
- */
-function unwrapNode(aNode) {
- var node = aNode;
- if (node) {
- // unwrap is not available on older branches (3.5 and 3.6) - Bug 533596
- if ("unwrap" in XPCNativeWrapper) {
- node = XPCNativeWrapper.unwrap(node);
- }
- else if (node.wrappedJSObject != null) {
- node = node.wrappedJSObject;
- }
- }
- return node;
-}
-
-/**
- * TimeoutError
- *
- * Error object used for timeouts
- */
-function TimeoutError(message, fileName, lineNumber) {
- var err = new Error();
- if (err.stack) {
- this.stack = err.stack;
- }
- this.message = message === undefined ? err.message : message;
- this.fileName = fileName === undefined ? err.fileName : fileName;
- this.lineNumber = lineNumber === undefined ? err.lineNumber : lineNumber;
-};
-TimeoutError.prototype = new Error();
-TimeoutError.prototype.constructor = TimeoutError;
-TimeoutError.prototype.name = 'TimeoutError';
-
-/**
- * Waits for the callback evaluates to true
- */
-function waitFor(callback, message, timeout, interval, thisObject) {
- timeout = timeout || 5000;
- interval = interval || 100;
-
- var self = {counter: 0, result: callback.call(thisObject)};
-
- function wait() {
- self.counter += interval;
- self.result = callback.call(thisObject);
- }
-
- var timeoutInterval = hwindow.setInterval(wait, interval);
- var thread = Components.classes["@mozilla.org/thread-manager;1"].
- getService().currentThread;
-
- while((self.result != true) && (self.counter < timeout)) {
- thread.processNextEvent(true);
- }
-
- hwindow.clearInterval(timeoutInterval);
-
- if (self.counter >= timeout) {
- message = message || arguments.callee.name + ": Timeout exceeded for '" + callback + "'";
- throw new TimeoutError(message);
- }
-
- return true;
-}
-
-/**
- * Calculates the x and y chrome offset for an element
- * See https://developer.mozilla.org/en/DOM/window.innerHeight
- *
- * Note this function will not work if the user has custom toolbars (via extension) at the bottom or left/right of the screen
- */
-function getChromeOffset(elem) {
- var win = elem.ownerDocument.defaultView;
- // Calculate x offset
- var chromeWidth = 0;
- if (win["name"] != "sidebar") {
- chromeWidth = win.outerWidth - win.innerWidth;
- }
-
- // Calculate y offset
- var chromeHeight = win.outerHeight - win.innerHeight;
- // chromeHeight == 0 means elem is already in the chrome and doesn't need the addonbar offset
- if (chromeHeight > 0) {
- // window.innerHeight doesn't include the addon or find bar, so account for these if present
- var addonbar = win.document.getElementById("addon-bar");
- if (addonbar) {
- chromeHeight -= addonbar.scrollHeight;
- }
- var findbar = win.document.getElementById("FindToolbar");
- if (findbar) {
- chromeHeight -= findbar.scrollHeight;
- }
- }
-
- return {'x':chromeWidth, 'y':chromeHeight};
-}
-
-/**
- * Takes a screenshot of the specified DOM node
- */
-function takeScreenshot(node, name, highlights) {
- var rect, win, width, height, left, top, needsOffset;
- // node can be either a window or an arbitrary DOM node
- try {
- win = node.ownerDocument.defaultView; // node is an arbitrary DOM node
- rect = node.getBoundingClientRect();
- width = rect.width;
- height = rect.height;
- top = rect.top;
- left = rect.left;
- // offset for highlights not needed as they will be relative to this node
- needsOffset = false;
- } catch (e) {
- win = node; // node is a window
- width = win.innerWidth;
- height = win.innerHeight;
- top = 0;
- left = 0;
- // offset needed for highlights to take 'outerHeight' of window into account
- needsOffset = true;
- }
-
- var canvas = win.document.createElementNS("http://www.w3.org/1999/xhtml", "canvas");
- canvas.width = width;
- canvas.height = height;
-
- var ctx = canvas.getContext("2d");
- // Draws the DOM contents of the window to the canvas
- ctx.drawWindow(win, left, top, width, height, "rgb(255,255,255)");
-
- // This section is for drawing a red rectangle around each element passed in via the highlights array
- if (highlights) {
- ctx.lineWidth = "2";
- ctx.strokeStyle = "red";
- ctx.save();
-
- for (var i = 0; i < highlights.length; ++i) {
- var elem = highlights[i];
- rect = elem.getBoundingClientRect();
-
- var offsetY = 0, offsetX = 0;
- if (needsOffset) {
- var offset = getChromeOffset(elem);
- offsetX = offset.x;
- offsetY = offset.y;
- } else {
- // Don't need to offset the window chrome, just make relative to containing node
- offsetY = -top;
- offsetX = -left;
- }
-
- // Draw the rectangle
- ctx.strokeRect(rect.left + offsetX, rect.top + offsetY, rect.width, rect.height);
- }
- } // end highlights
-
- // if there is a name save the file, else return dataURL
- if (name) {
- return saveCanvas(canvas, name);
- }
- return canvas.toDataURL("image/png","");
-}
-
-/**
- * Takes a canvas as input and saves it to the file tempdir/name.png
- * Returns the filepath of the saved file
- */
-function saveCanvas(canvas, name) {
- var file = Components.classes["@mozilla.org/file/directory_service;1"]
- .getService(Components.interfaces.nsIProperties)
- .get("TmpD", Components.interfaces.nsIFile);
- file.append("mozmill_screens");
- file.append(name + ".png");
- file.createUnique(Components.interfaces.nsIFile.NORMAL_FILE_TYPE, 0666);
-
- // create a data url from the canvas and then create URIs of the source and targets
- var io = Components.classes["@mozilla.org/network/io-service;1"]
- .getService(Components.interfaces.nsIIOService);
- var source = io.newURI(canvas.toDataURL("image/png", ""), "UTF8", null);
- var target = io.newFileURI(file)
-
- // prepare to save the canvas data
- var persist = Components.classes["@mozilla.org/embedding/browser/nsWebBrowserPersist;1"]
- .createInstance(Components.interfaces.nsIWebBrowserPersist);
-
- persist.persistFlags = Components.interfaces.nsIWebBrowserPersist.PERSIST_FLAGS_REPLACE_EXISTING_FILES;
- persist.persistFlags |= Components.interfaces.nsIWebBrowserPersist.PERSIST_FLAGS_AUTODETECT_APPLY_CONVERSION;
-
- // save the canvas data to the file
- persist.saveURI(source, null, null, null, null, file);
-
- return file.path;
-}
diff --git a/services/sync/tps/extensions/mozmill/resource/modules/windows.js b/services/sync/tps/extensions/mozmill/resource/modules/windows.js
new file mode 100644
index 000000000..fe9cfaa01
--- /dev/null
+++ b/services/sync/tps/extensions/mozmill/resource/modules/windows.js
@@ -0,0 +1,292 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, you can obtain one at http://mozilla.org/MPL/2.0/. */
+
+var EXPORTED_SYMBOLS = ["init", "map"];
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cu = Components.utils;
+
+// imports
+var utils = {}; Cu.import('resource://mozmill/stdlib/utils.js', utils);
+
+var uuidgen = Cc["@mozilla.org/uuid-generator;1"].getService(Ci.nsIUUIDGenerator);
+
+/**
+ * The window map is used to store information about the current state of
+ * open windows, e.g. loaded state
+ */
+var map = {
+ _windows : { },
+
+ /**
+ * Check if a given window id is contained in the map of windows
+ *
+ * @param {Number} aWindowId
+ * Outer ID of the window to check.
+ * @returns {Boolean} True if the window is part of the map, otherwise false.
+ */
+ contains : function (aWindowId) {
+ return (aWindowId in this._windows);
+ },
+
+ /**
+ * Retrieve the value of the specified window's property.
+ *
+ * @param {Number} aWindowId
+ * Outer ID of the window to check.
+ * @param {String} aProperty
+ * Property to retrieve the value from
+ * @return {Object} Value of the window's property
+ */
+ getValue : function (aWindowId, aProperty) {
+ if (!this.contains(aWindowId)) {
+ return undefined;
+ } else {
+ var win = this._windows[aWindowId];
+
+ return (aProperty in win) ? win[aProperty]
+ : undefined;
+ }
+ },
+
+ /**
+ * Remove the entry for a given window
+ *
+ * @param {Number} aWindowId
+ * Outer ID of the window to check.
+ */
+ remove : function (aWindowId) {
+ if (this.contains(aWindowId)) {
+ delete this._windows[aWindowId];
+ }
+
+ // dump("* current map: " + JSON.stringify(this._windows) + "\n");
+ },
+
+ /**
+ * Update the property value of a given window
+ *
+ * @param {Number} aWindowId
+ * Outer ID of the window to check.
+ * @param {String} aProperty
+ * Property to update the value for
+ * @param {Object}
+ * Value to set
+ */
+ update : function (aWindowId, aProperty, aValue) {
+ if (!this.contains(aWindowId)) {
+ this._windows[aWindowId] = { };
+ }
+
+ this._windows[aWindowId][aProperty] = aValue;
+ // dump("* current map: " + JSON.stringify(this._windows) + "\n");
+ },
+
+ /**
+ * Update the internal loaded state of the given content window. To identify
+ * an active (re)load action we make use of an uuid.
+ *
+ * @param {Window} aId - The outer id of the window to update
+ * @param {Boolean} aIsLoaded - Has the window been loaded
+ */
+ updatePageLoadStatus : function (aId, aIsLoaded) {
+ this.update(aId, "loaded", aIsLoaded);
+
+ var uuid = this.getValue(aId, "id_load_in_transition");
+
+ // If no uuid has been set yet or when the page gets unloaded create a new id
+ if (!uuid || !aIsLoaded) {
+ uuid = uuidgen.generateUUID();
+ this.update(aId, "id_load_in_transition", uuid);
+ }
+
+ // dump("*** Page status updated: id=" + aId + ", loaded=" + aIsLoaded + ", uuid=" + uuid + "\n");
+ },
+
+ /**
+ * This method only applies to content windows, where we have to check if it has
+ * been successfully loaded or reloaded. An uuid allows us to wait for the next
+ * load action triggered by e.g. controller.open().
+ *
+ * @param {Window} aId - The outer id of the content window to check
+ *
+ * @returns {Boolean} True if the content window has been loaded
+ */
+ hasPageLoaded : function (aId) {
+ var load_current = this.getValue(aId, "id_load_in_transition");
+ var load_handled = this.getValue(aId, "id_load_handled");
+
+ var isLoaded = this.contains(aId) && this.getValue(aId, "loaded") &&
+ (load_current !== load_handled);
+
+ if (isLoaded) {
+ // Backup the current uuid so we can check later if another page load happened.
+ this.update(aId, "id_load_handled", load_current);
+ }
+
+ // dump("** Page has been finished loading: id=" + aId + ", status=" + isLoaded + ", uuid=" + load_current + "\n");
+
+ return isLoaded;
+ }
+};
+
+
+// Observer when a new top-level window is ready
+var windowReadyObserver = {
+ observe: function (aSubject, aTopic, aData) {
+ // Not in all cases we get a ChromeWindow. So ensure we really operate
+ // on such an instance. Otherwise load events will not be handled.
+ var win = utils.getChromeWindow(aSubject);
+
+ // var id = utils.getWindowId(win);
+ // dump("*** 'toplevel-window-ready' observer notification: id=" + id + "\n");
+ attachEventListeners(win);
+ }
+};
+
+
+// Observer when a top-level window is closed
+var windowCloseObserver = {
+ observe: function (aSubject, aTopic, aData) {
+ var id = utils.getWindowId(aSubject);
+ // dump("*** 'outer-window-destroyed' observer notification: id=" + id + "\n");
+
+ map.remove(id);
+ }
+};
+
+// Bug 915554
+// Support for the old Private Browsing Mode (eg. ESR17)
+// TODO: remove once ESR17 is no longer supported
+var enterLeavePrivateBrowsingObserver = {
+ observe: function (aSubject, aTopic, aData) {
+ handleAttachEventListeners();
+ }
+};
+
+/**
+ * Attach event listeners
+ *
+ * @param {ChromeWindow} aWindow
+ * Window to attach listeners on.
+ */
+function attachEventListeners(aWindow) {
+ // These are the event handlers
+ var pageShowHandler = function (aEvent) {
+ var doc = aEvent.originalTarget;
+
+ // Only update the flag if we have a document as target
+ // see https://bugzilla.mozilla.org/show_bug.cgi?id=690829
+ if ("defaultView" in doc) {
+ var id = utils.getWindowId(doc.defaultView);
+ // dump("*** 'pageshow' event: id=" + id + ", baseURI=" + doc.baseURI + "\n");
+ map.updatePageLoadStatus(id, true);
+ }
+
+ // We need to add/remove the unload/pagehide event listeners to preserve caching.
+ aWindow.addEventListener("beforeunload", beforeUnloadHandler, true);
+ aWindow.addEventListener("pagehide", pageHideHandler, true);
+ };
+
+ var DOMContentLoadedHandler = function (aEvent) {
+ var doc = aEvent.originalTarget;
+
+ // Only update the flag if we have a document as target
+ if ("defaultView" in doc) {
+ var id = utils.getWindowId(doc.defaultView);
+ // dump("*** 'DOMContentLoaded' event: id=" + id + ", baseURI=" + doc.baseURI + "\n");
+
+ // We only care about error pages for DOMContentLoaded
+ var errorRegex = /about:.+(error)|(blocked)\?/;
+ if (errorRegex.exec(doc.baseURI)) {
+ // Wait about 1s to be sure the DOM is ready
+ utils.sleep(1000);
+
+ map.updatePageLoadStatus(id, true);
+ }
+
+ // We need to add/remove the unload event listener to preserve caching.
+ aWindow.addEventListener("beforeunload", beforeUnloadHandler, true);
+ }
+ };
+
+ // beforeunload is still needed because pagehide doesn't fire before the page is unloaded.
+ // still use pagehide for cases when beforeunload doesn't get fired
+ var beforeUnloadHandler = function (aEvent) {
+ var doc = aEvent.originalTarget;
+
+ // Only update the flag if we have a document as target
+ if ("defaultView" in doc) {
+ var id = utils.getWindowId(doc.defaultView);
+ // dump("*** 'beforeunload' event: id=" + id + ", baseURI=" + doc.baseURI + "\n");
+ map.updatePageLoadStatus(id, false);
+ }
+
+ aWindow.removeEventListener("beforeunload", beforeUnloadHandler, true);
+ };
+
+ var pageHideHandler = function (aEvent) {
+ var doc = aEvent.originalTarget;
+
+ // Only update the flag if we have a document as target
+ if ("defaultView" in doc) {
+ var id = utils.getWindowId(doc.defaultView);
+ // dump("*** 'pagehide' event: id=" + id + ", baseURI=" + doc.baseURI + "\n");
+ map.updatePageLoadStatus(id, false);
+ }
+ // If event.persisted is true the beforeUnloadHandler would never fire
+ // and we have to remove the event handler here to avoid memory leaks.
+ if (aEvent.persisted)
+ aWindow.removeEventListener("beforeunload", beforeUnloadHandler, true);
+ };
+
+ var onWindowLoaded = function (aEvent) {
+ var id = utils.getWindowId(aWindow);
+ // dump("*** 'load' event: id=" + id + ", baseURI=" + aWindow.document.baseURI + "\n");
+
+ map.update(id, "loaded", true);
+
+ // Note: Error pages will never fire a "pageshow" event. For those we
+ // have to wait for the "DOMContentLoaded" event. That's the final state.
+ // Error pages will always have a baseURI starting with
+ // "about:" followed by "error" or "blocked".
+ aWindow.addEventListener("DOMContentLoaded", DOMContentLoadedHandler, true);
+
+ // Page is ready
+ aWindow.addEventListener("pageshow", pageShowHandler, true);
+
+ // Leave page (use caching)
+ aWindow.addEventListener("pagehide", pageHideHandler, true);
+ };
+
+ // If the window has already been finished loading, call the load handler
+ // directly. Otherwise attach it to the current window.
+ if (aWindow.document.readyState === 'complete') {
+ onWindowLoaded();
+ } else {
+ aWindow.addEventListener("load", onWindowLoaded, false);
+ }
+}
+
+// Attach event listeners to all already open top-level windows
+function handleAttachEventListeners() {
+ var enumerator = Cc["@mozilla.org/appshell/window-mediator;1"].
+ getService(Ci.nsIWindowMediator).getEnumerator("");
+ while (enumerator.hasMoreElements()) {
+ var win = enumerator.getNext();
+ attachEventListeners(win);
+ }
+}
+
+function init() {
+ // Activate observer for new top level windows
+ var observerService = Cc["@mozilla.org/observer-service;1"].
+ getService(Ci.nsIObserverService);
+ observerService.addObserver(windowReadyObserver, "toplevel-window-ready", false);
+ observerService.addObserver(windowCloseObserver, "outer-window-destroyed", false);
+ observerService.addObserver(enterLeavePrivateBrowsingObserver, "private-browsing", false);
+
+ handleAttachEventListeners();
+}
diff --git a/services/sync/tps/extensions/mozmill/resource/stdlib/EventUtils.js b/services/sync/tps/extensions/mozmill/resource/stdlib/EventUtils.js
index 6c0cc21d9..a821ab2e0 100644
--- a/services/sync/tps/extensions/mozmill/resource/stdlib/EventUtils.js
+++ b/services/sync/tps/extensions/mozmill/resource/stdlib/EventUtils.js
@@ -1,24 +1,28 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
// Export all available functions for Mozmill
-var EXPORTED_SYMBOLS = ["sendMouseEvent", "sendChar", "sendString", "sendKey",
- "synthesizeMouse", "synthesizeMouseScroll", "synthesizeKey",
+var EXPORTED_SYMBOLS = ["disableNonTestMouseEvents","sendMouseEvent", "sendChar",
+ "sendString", "sendKey", "synthesizeMouse", "synthesizeTouch",
+ "synthesizeMouseAtPoint", "synthesizeTouchAtPoint",
+ "synthesizeMouseAtCenter", "synthesizeTouchAtCenter",
+ "synthesizeWheel", "synthesizeKey",
"synthesizeMouseExpectEvent", "synthesizeKeyExpectEvent",
- "synthesizeDragStart", "synthesizeDrop", "synthesizeText",
- "disableNonTestMouseEvents", "synthesizeComposition",
- "synthesizeQuerySelectedText", "synthesizeQueryTextContent",
- "synthesizeQueryCaretRect", "synthesizeQueryTextRect",
- "synthesizeQueryEditorRect", "synthesizeCharAtPoint",
- "synthesizeSelectionSet"];
+ "synthesizeText",
+ "synthesizeComposition", "synthesizeQuerySelectedText"];
-/**
- * Get the array with available key events
- */
-function getKeyEvent(aWindow) {
- var win = aWindow.wrappedJSObject ? aWindow.wrappedJSObject : aWindow;
- return win.KeyEvent;
+const Ci = Components.interfaces;
+const Cc = Components.classes;
+
+var window = Cc["@mozilla.org/appshell/appShellService;1"]
+ .getService(Ci.nsIAppShellService).hiddenDOMWindow;
+
+var _EU_Ci = Ci;
+var navigator = window.navigator;
+var KeyEvent = window.KeyEvent;
+var parent = window.parent;
+
+function is(aExpression1, aExpression2, aMessage) {
+ if (aExpression1 !== aExpression2) {
+ throw new Error(aMessage);
+ }
}
/**
@@ -28,6 +32,14 @@ function getKeyEvent(aWindow) {
* sendChar
* sendString
* sendKey
+ * synthesizeMouse
+ * synthesizeMouseAtCenter
+ * synthesizeWheel
+ * synthesizeKey
+ * synthesizeMouseExpectEvent
+ * synthesizeKeyExpectEvent
+ *
+ * When adding methods to this file, please add a performance test for it.
*/
/**
@@ -39,16 +51,23 @@ function getKeyEvent(aWindow) {
*
* sendMouseEvent({type:'click'}, 'node');
*/
+function getElement(id) {
+ return ((typeof(id) == "string") ?
+ document.getElementById(id) : id);
+};
+
+this.$ = this.getElement;
+
function sendMouseEvent(aEvent, aTarget, aWindow) {
- if (['click', 'mousedown', 'mouseup', 'mouseover', 'mouseout'].indexOf(aEvent.type) == -1) {
- throw new Error("sendMouseEvent doesn't know about event type '"+aEvent.type+"'");
+ if (['click', 'dblclick', 'mousedown', 'mouseup', 'mouseover', 'mouseout'].indexOf(aEvent.type) == -1) {
+ throw new Error("sendMouseEvent doesn't know about event type '" + aEvent.type + "'");
}
if (!aWindow) {
aWindow = window;
}
- if (!(aTarget instanceof Element)) {
+ if (!(aTarget instanceof aWindow.Element)) {
aTarget = aWindow.document.getElementById(aTarget);
}
@@ -60,7 +79,8 @@ function sendMouseEvent(aEvent, aTarget, aWindow) {
var viewArg = aWindow;
var detailArg = aEvent.detail || (aEvent.type == 'click' ||
aEvent.type == 'mousedown' ||
- aEvent.type == 'mouseup' ? 1 : 0);
+ aEvent.type == 'mouseup' ? 1 :
+ aEvent.type == 'dblclick'? 2 : 0);
var screenXArg = aEvent.screenX || 0;
var screenYArg = aEvent.screenY || 0;
var clientXArg = aEvent.clientX || 0;
@@ -77,112 +97,71 @@ function sendMouseEvent(aEvent, aTarget, aWindow) {
ctrlKeyArg, altKeyArg, shiftKeyArg, metaKeyArg,
buttonArg, relatedTargetArg);
- aTarget.dispatchEvent(event);
+ SpecialPowers.dispatchEvent(aWindow, aTarget, event);
}
/**
- * Send the char aChar to the node with id aTarget. If aTarget is not
- * provided, use "target". This method handles casing of chars (sends the
- * right charcode, and sends a shift key for uppercase chars). No other
- * modifiers are handled at this point.
+ * Send the char aChar to the focused element. This method handles casing of
+ * chars (sends the right charcode, and sends a shift key for uppercase chars).
+ * No other modifiers are handled at this point.
*
- * For now this method only works for English letters (lower and upper case)
- * and the digits 0-9.
- *
- * Returns true if the keypress event was accepted (no calls to preventDefault
- * or anything like that), false otherwise.
+ * For now this method only works for ASCII characters and emulates the shift
+ * key state on US keyboard layout.
*/
-function sendChar(aChar, aTarget) {
- // DOM event charcodes match ASCII (JS charcodes) for a-zA-Z0-9.
- var hasShift = (aChar == aChar.toUpperCase());
- var charCode = aChar.charCodeAt(0);
- var keyCode = charCode;
- if (!hasShift) {
- // For lowercase letters, the keyCode is actually 32 less than the charCode
- keyCode -= 0x20;
+function sendChar(aChar, aWindow) {
+ var hasShift;
+ // Emulate US keyboard layout for the shiftKey state.
+ switch (aChar) {
+ case "!":
+ case "@":
+ case "#":
+ case "$":
+ case "%":
+ case "^":
+ case "&":
+ case "*":
+ case "(":
+ case ")":
+ case "_":
+ case "+":
+ case "{":
+ case "}":
+ case ":":
+ case "\"":
+ case "|":
+ case "<":
+ case ">":
+ case "?":
+ hasShift = true;
+ break;
+ default:
+ hasShift = (aChar == aChar.toUpperCase());
+ break;
}
-
- return __doEventDispatch(aTarget, charCode, keyCode, hasShift);
+ synthesizeKey(aChar, { shiftKey: hasShift }, aWindow);
}
/**
- * Send the string aStr to the node with id aTarget. If aTarget is not
- * provided, use "target".
+ * Send the string aStr to the focused element.
*
- * For now this method only works for English letters (lower and upper case)
- * and the digits 0-9.
+ * For now this method only works for ASCII characters and emulates the shift
+ * key state on US keyboard layout.
*/
-function sendString(aStr, aTarget) {
+function sendString(aStr, aWindow) {
for (var i = 0; i < aStr.length; ++i) {
- sendChar(aStr.charAt(i), aTarget);
+ sendChar(aStr.charAt(i), aWindow);
}
}
/**
- * Send the non-character key aKey to the node with id aTarget. If aTarget is
- * not provided, use "target". The name of the key should be a lowercase
- * version of the part that comes after "DOM_VK_" in the KeyEvent constant
- * name for this key. No modifiers are handled at this point.
- *
- * Returns true if the keypress event was accepted (no calls to preventDefault
- * or anything like that), false otherwise.
+ * Send the non-character key aKey to the focused node.
+ * The name of the key should be the part that comes after "DOM_VK_" in the
+ * KeyEvent constant name for this key.
+ * No modifiers are handled at this point.
*/
-function sendKey(aKey, aTarget, aWindow) {
- if (!aWindow)
- aWindow = window;
-
- keyName = "DOM_VK_" + aKey.toUpperCase();
-
- if (!getKeyEvent(aWindow)[keyName]) {
- throw "Unknown key: " + keyName;
- }
-
- return __doEventDispatch(aTarget, 0, getKeyEvent(aWindow)[keyName], false);
-}
-
-/**
- * Actually perform event dispatch given a charCode, keyCode, and boolean for
- * whether "shift" was pressed. Send the event to the node with id aTarget. If
- * aTarget is not provided, use "target".
- *
- * Returns true if the keypress event was accepted (no calls to preventDefault
- * or anything like that), false otherwise.
- */
-function __doEventDispatch(aTarget, aCharCode, aKeyCode, aHasShift) {
- if (aTarget === undefined) {
- aTarget = "target";
- }
-
- var event = document.createEvent("KeyEvents");
- event.initKeyEvent("keydown", true, true, document.defaultView,
- false, false, aHasShift, false,
- aKeyCode, 0);
- var accepted = $(aTarget).dispatchEvent(event);
-
- // Preventing the default keydown action also prevents the default
- // keypress action.
- event = document.createEvent("KeyEvents");
- if (aCharCode) {
- event.initKeyEvent("keypress", true, true, document.defaultView,
- false, false, aHasShift, false,
- 0, aCharCode);
- } else {
- event.initKeyEvent("keypress", true, true, document.defaultView,
- false, false, aHasShift, false,
- aKeyCode, 0);
- }
- if (!accepted) {
- event.preventDefault();
- }
- accepted = $(aTarget).dispatchEvent(event);
-
- // Always send keyup
- var event = document.createEvent("KeyEvents");
- event.initKeyEvent("keyup", true, true, document.defaultView,
- false, false, aHasShift, false,
- aKeyCode, 0);
- $(aTarget).dispatchEvent(event);
- return accepted;
+function sendKey(aKey, aWindow) {
+ var keyName = "VK_" + aKey.toUpperCase();
+ synthesizeKey(keyName, { shiftKey: false }, aWindow);
}
/**
@@ -191,23 +170,45 @@ function __doEventDispatch(aTarget, aCharCode, aKeyCode, aHasShift) {
*/
function _parseModifiers(aEvent)
{
- var hwindow = Components.classes["@mozilla.org/appshell/appShellService;1"]
- .getService(Components.interfaces.nsIAppShellService)
- .hiddenDOMWindow;
-
- const masks = Components.interfaces.nsIDOMNSEvent;
+ const nsIDOMWindowUtils = _EU_Ci.nsIDOMWindowUtils;
var mval = 0;
- if (aEvent.shiftKey)
- mval |= masks.SHIFT_MASK;
- if (aEvent.ctrlKey)
- mval |= masks.CONTROL_MASK;
- if (aEvent.altKey)
- mval |= masks.ALT_MASK;
- if (aEvent.metaKey)
- mval |= masks.META_MASK;
- if (aEvent.accelKey)
- mval |= (hwindow.navigator.platform.indexOf("Mac") >= 0) ? masks.META_MASK :
- masks.CONTROL_MASK;
+ if (aEvent.shiftKey) {
+ mval |= nsIDOMWindowUtils.MODIFIER_SHIFT;
+ }
+ if (aEvent.ctrlKey) {
+ mval |= nsIDOMWindowUtils.MODIFIER_CONTROL;
+ }
+ if (aEvent.altKey) {
+ mval |= nsIDOMWindowUtils.MODIFIER_ALT;
+ }
+ if (aEvent.metaKey) {
+ mval |= nsIDOMWindowUtils.MODIFIER_META;
+ }
+ if (aEvent.accelKey) {
+ mval |= (navigator.platform.indexOf("Mac") >= 0) ?
+ nsIDOMWindowUtils.MODIFIER_META : nsIDOMWindowUtils.MODIFIER_CONTROL;
+ }
+ if (aEvent.altGrKey) {
+ mval |= nsIDOMWindowUtils.MODIFIER_ALTGRAPH;
+ }
+ if (aEvent.capsLockKey) {
+ mval |= nsIDOMWindowUtils.MODIFIER_CAPSLOCK;
+ }
+ if (aEvent.fnKey) {
+ mval |= nsIDOMWindowUtils.MODIFIER_FN;
+ }
+ if (aEvent.numLockKey) {
+ mval |= nsIDOMWindowUtils.MODIFIER_NUMLOCK;
+ }
+ if (aEvent.scrollLockKey) {
+ mval |= nsIDOMWindowUtils.MODIFIER_SCROLLLOCK;
+ }
+ if (aEvent.symbolLockKey) {
+ mval |= nsIDOMWindowUtils.MODIFIER_SYMBOLLOCK;
+ }
+ if (aEvent.osKey) {
+ mval |= nsIDOMWindowUtils.MODIFIER_OS;
+ }
return mval;
}
@@ -224,84 +225,281 @@ function _parseModifiers(aEvent)
* a mousedown followed by a mouse up is performed.
*
* aWindow is optional, and defaults to the current window object.
+ *
+ * Returns whether the event had preventDefault() called on it.
*/
function synthesizeMouse(aTarget, aOffsetX, aOffsetY, aEvent, aWindow)
{
- if (!aWindow)
- aWindow = window;
+ var rect = aTarget.getBoundingClientRect();
+ return synthesizeMouseAtPoint(rect.left + aOffsetX, rect.top + aOffsetY,
+ aEvent, aWindow);
+}
+function synthesizeTouch(aTarget, aOffsetX, aOffsetY, aEvent, aWindow)
+{
+ var rect = aTarget.getBoundingClientRect();
+ synthesizeTouchAtPoint(rect.left + aOffsetX, rect.top + aOffsetY,
+ aEvent, aWindow);
+}
+
+/*
+ * Synthesize a mouse event at a particular point in aWindow.
+ *
+ * aEvent is an object which may contain the properties:
+ * shiftKey, ctrlKey, altKey, metaKey, accessKey, clickCount, button, type
+ *
+ * If the type is specified, an mouse event of that type is fired. Otherwise,
+ * a mousedown followed by a mouse up is performed.
+ *
+ * aWindow is optional, and defaults to the current window object.
+ */
+function synthesizeMouseAtPoint(left, top, aEvent, aWindow)
+{
+ var utils = _getDOMWindowUtils(aWindow);
+ var defaultPrevented = false;
- var utils = aWindow.QueryInterface(Components.interfaces.nsIInterfaceRequestor).
- getInterface(Components.interfaces.nsIDOMWindowUtils);
if (utils) {
var button = aEvent.button || 0;
var clickCount = aEvent.clickCount || 1;
var modifiers = _parseModifiers(aEvent);
+ var pressure = ("pressure" in aEvent) ? aEvent.pressure : 0;
+ var inputSource = ("inputSource" in aEvent) ? aEvent.inputSource : 0;
+
+ if (("type" in aEvent) && aEvent.type) {
+ defaultPrevented = utils.sendMouseEvent(aEvent.type, left, top, button, clickCount, modifiers, false, pressure, inputSource);
+ }
+ else {
+ utils.sendMouseEvent("mousedown", left, top, button, clickCount, modifiers, false, pressure, inputSource);
+ utils.sendMouseEvent("mouseup", left, top, button, clickCount, modifiers, false, pressure, inputSource);
+ }
+ }
- var rect = aTarget.getBoundingClientRect();
+ return defaultPrevented;
+}
+function synthesizeTouchAtPoint(left, top, aEvent, aWindow)
+{
+ var utils = _getDOMWindowUtils(aWindow);
- var left = rect.left + aOffsetX;
- var top = rect.top + aOffsetY;
+ if (utils) {
+ var id = aEvent.id || 0;
+ var rx = aEvent.rx || 1;
+ var ry = aEvent.rx || 1;
+ var angle = aEvent.angle || 0;
+ var force = aEvent.force || 1;
+ var modifiers = _parseModifiers(aEvent);
- if (aEvent.type) {
- utils.sendMouseEvent(aEvent.type, left, top, button, clickCount, modifiers);
+ if (("type" in aEvent) && aEvent.type) {
+ utils.sendTouchEvent(aEvent.type, [id], [left], [top], [rx], [ry], [angle], [force], 1, modifiers);
}
else {
- utils.sendMouseEvent("mousedown", left, top, button, clickCount, modifiers);
- utils.sendMouseEvent("mouseup", left, top, button, clickCount, modifiers);
+ utils.sendTouchEvent("touchstart", [id], [left], [top], [rx], [ry], [angle], [force], 1, modifiers);
+ utils.sendTouchEvent("touchend", [id], [left], [top], [rx], [ry], [angle], [force], 1, modifiers);
}
}
}
+// Call synthesizeMouse with coordinates at the center of aTarget.
+function synthesizeMouseAtCenter(aTarget, aEvent, aWindow)
+{
+ var rect = aTarget.getBoundingClientRect();
+ synthesizeMouse(aTarget, rect.width / 2, rect.height / 2, aEvent,
+ aWindow);
+}
+function synthesizeTouchAtCenter(aTarget, aEvent, aWindow)
+{
+ var rect = aTarget.getBoundingClientRect();
+ synthesizeTouch(aTarget, rect.width / 2, rect.height / 2, aEvent,
+ aWindow);
+}
/**
- * Synthesize a mouse scroll event on a target. The actual client point is determined
+ * Synthesize a wheel event on a target. The actual client point is determined
* by taking the aTarget's client box and offseting it by aOffsetX and
* aOffsetY.
*
* aEvent is an object which may contain the properties:
- * shiftKey, ctrlKey, altKey, metaKey, accessKey, button, type, axis, delta, hasPixels
- *
- * If the type is specified, a mouse scroll event of that type is fired. Otherwise,
- * "DOMMouseScroll" is used.
+ * shiftKey, ctrlKey, altKey, metaKey, accessKey, deltaX, deltaY, deltaZ,
+ * deltaMode, lineOrPageDeltaX, lineOrPageDeltaY, isMomentum, isPixelOnlyDevice,
+ * isCustomizedByPrefs, expectedOverflowDeltaX, expectedOverflowDeltaY
*
- * If the axis is specified, it must be one of "horizontal" or "vertical". If not specified,
- * "vertical" is used.
+ * deltaMode must be defined, others are ok even if undefined.
*
- * 'delta' is the amount to scroll by (can be positive or negative). It must
- * be specified.
- *
- * 'hasPixels' specifies whether kHasPixels should be set in the scrollFlags.
+ * expectedOverflowDeltaX and expectedOverflowDeltaY take integer value. The
+ * value is just checked as 0 or positive or negative.
*
* aWindow is optional, and defaults to the current window object.
*/
-function synthesizeMouseScroll(aTarget, aOffsetX, aOffsetY, aEvent, aWindow)
+function synthesizeWheel(aTarget, aOffsetX, aOffsetY, aEvent, aWindow)
{
- if (!aWindow)
- aWindow = window;
+ var utils = _getDOMWindowUtils(aWindow);
+ if (!utils) {
+ return;
+ }
- var utils = aWindow.QueryInterface(Components.interfaces.nsIInterfaceRequestor).
- getInterface(Components.interfaces.nsIDOMWindowUtils);
- if (utils) {
- // See nsMouseScrollFlags in nsGUIEvent.h
- const kIsVertical = 0x02;
- const kIsHorizontal = 0x04;
- const kHasPixels = 0x08;
+ var modifiers = _parseModifiers(aEvent);
+ var options = 0;
+ if (aEvent.isPixelOnlyDevice &&
+ (aEvent.deltaMode == WheelEvent.DOM_DELTA_PIXEL)) {
+ options |= utils.WHEEL_EVENT_CAUSED_BY_PIXEL_ONLY_DEVICE;
+ }
+ if (aEvent.isMomentum) {
+ options |= utils.WHEEL_EVENT_CAUSED_BY_MOMENTUM;
+ }
+ if (aEvent.isCustomizedByPrefs) {
+ options |= utils.WHEEL_EVENT_CUSTOMIZED_BY_USER_PREFS;
+ }
+ if (typeof aEvent.expectedOverflowDeltaX !== "undefined") {
+ if (aEvent.expectedOverflowDeltaX === 0) {
+ options |= utils.WHEEL_EVENT_EXPECTED_OVERFLOW_DELTA_X_ZERO;
+ } else if (aEvent.expectedOverflowDeltaX > 0) {
+ options |= utils.WHEEL_EVENT_EXPECTED_OVERFLOW_DELTA_X_POSITIVE;
+ } else {
+ options |= utils.WHEEL_EVENT_EXPECTED_OVERFLOW_DELTA_X_NEGATIVE;
+ }
+ }
+ if (typeof aEvent.expectedOverflowDeltaY !== "undefined") {
+ if (aEvent.expectedOverflowDeltaY === 0) {
+ options |= utils.WHEEL_EVENT_EXPECTED_OVERFLOW_DELTA_Y_ZERO;
+ } else if (aEvent.expectedOverflowDeltaY > 0) {
+ options |= utils.WHEEL_EVENT_EXPECTED_OVERFLOW_DELTA_Y_POSITIVE;
+ } else {
+ options |= utils.WHEEL_EVENT_EXPECTED_OVERFLOW_DELTA_Y_NEGATIVE;
+ }
+ }
+ var isPixelOnlyDevice =
+ aEvent.isPixelOnlyDevice && aEvent.deltaMode == WheelEvent.DOM_DELTA_PIXEL;
- var button = aEvent.button || 0;
- var modifiers = _parseModifiers(aEvent);
+ // Avoid the JS warnings "reference to undefined property"
+ if (!aEvent.deltaX) {
+ aEvent.deltaX = 0;
+ }
+ if (!aEvent.deltaY) {
+ aEvent.deltaY = 0;
+ }
+ if (!aEvent.deltaZ) {
+ aEvent.deltaZ = 0;
+ }
- var rect = aTarget.getBoundingClientRect();
+ var lineOrPageDeltaX =
+ aEvent.lineOrPageDeltaX != null ? aEvent.lineOrPageDeltaX :
+ aEvent.deltaX > 0 ? Math.floor(aEvent.deltaX) :
+ Math.ceil(aEvent.deltaX);
+ var lineOrPageDeltaY =
+ aEvent.lineOrPageDeltaY != null ? aEvent.lineOrPageDeltaY :
+ aEvent.deltaY > 0 ? Math.floor(aEvent.deltaY) :
+ Math.ceil(aEvent.deltaY);
+
+ var rect = aTarget.getBoundingClientRect();
+ utils.sendWheelEvent(rect.left + aOffsetX, rect.top + aOffsetY,
+ aEvent.deltaX, aEvent.deltaY, aEvent.deltaZ,
+ aEvent.deltaMode, modifiers,
+ lineOrPageDeltaX, lineOrPageDeltaY, options);
+}
- var left = rect.left;
- var top = rect.top;
+function _computeKeyCodeFromChar(aChar)
+{
+ if (aChar.length != 1) {
+ return 0;
+ }
+ const nsIDOMKeyEvent = _EU_Ci.nsIDOMKeyEvent;
+ if (aChar >= 'a' && aChar <= 'z') {
+ return nsIDOMKeyEvent.DOM_VK_A + aChar.charCodeAt(0) - 'a'.charCodeAt(0);
+ }
+ if (aChar >= 'A' && aChar <= 'Z') {
+ return nsIDOMKeyEvent.DOM_VK_A + aChar.charCodeAt(0) - 'A'.charCodeAt(0);
+ }
+ if (aChar >= '0' && aChar <= '9') {
+ return nsIDOMKeyEvent.DOM_VK_0 + aChar.charCodeAt(0) - '0'.charCodeAt(0);
+ }
+ // returns US keyboard layout's keycode
+ switch (aChar) {
+ case '~':
+ case '`':
+ return nsIDOMKeyEvent.DOM_VK_BACK_QUOTE;
+ case '!':
+ return nsIDOMKeyEvent.DOM_VK_1;
+ case '@':
+ return nsIDOMKeyEvent.DOM_VK_2;
+ case '#':
+ return nsIDOMKeyEvent.DOM_VK_3;
+ case '$':
+ return nsIDOMKeyEvent.DOM_VK_4;
+ case '%':
+ return nsIDOMKeyEvent.DOM_VK_5;
+ case '^':
+ return nsIDOMKeyEvent.DOM_VK_6;
+ case '&':
+ return nsIDOMKeyEvent.DOM_VK_7;
+ case '*':
+ return nsIDOMKeyEvent.DOM_VK_8;
+ case '(':
+ return nsIDOMKeyEvent.DOM_VK_9;
+ case ')':
+ return nsIDOMKeyEvent.DOM_VK_0;
+ case '-':
+ case '_':
+ return nsIDOMKeyEvent.DOM_VK_SUBTRACT;
+ case '+':
+ case '=':
+ return nsIDOMKeyEvent.DOM_VK_EQUALS;
+ case '{':
+ case '[':
+ return nsIDOMKeyEvent.DOM_VK_OPEN_BRACKET;
+ case '}':
+ case ']':
+ return nsIDOMKeyEvent.DOM_VK_CLOSE_BRACKET;
+ case '|':
+ case '\\':
+ return nsIDOMKeyEvent.DOM_VK_BACK_SLASH;
+ case ':':
+ case ';':
+ return nsIDOMKeyEvent.DOM_VK_SEMICOLON;
+ case '\'':
+ case '"':
+ return nsIDOMKeyEvent.DOM_VK_QUOTE;
+ case '<':
+ case ',':
+ return nsIDOMKeyEvent.DOM_VK_COMMA;
+ case '>':
+ case '.':
+ return nsIDOMKeyEvent.DOM_VK_PERIOD;
+ case '?':
+ case '/':
+ return nsIDOMKeyEvent.DOM_VK_SLASH;
+ default:
+ return 0;
+ }
+}
- var type = aEvent.type || "DOMMouseScroll";
- var axis = aEvent.axis || "vertical";
- var scrollFlags = (axis == "horizontal") ? kIsHorizontal : kIsVertical;
- if (aEvent.hasPixels) {
- scrollFlags |= kHasPixels;
+/**
+ * isKeypressFiredKey() returns TRUE if the given key should cause keypress
+ * event when widget handles the native key event. Otherwise, FALSE.
+ *
+ * aDOMKeyCode should be one of consts of nsIDOMKeyEvent::DOM_VK_*, or a key
+ * name begins with "VK_", or a character.
+ */
+function isKeypressFiredKey(aDOMKeyCode)
+{
+ if (typeof(aDOMKeyCode) == "string") {
+ if (aDOMKeyCode.indexOf("VK_") == 0) {
+ aDOMKeyCode = KeyEvent["DOM_" + aDOMKeyCode];
+ if (!aDOMKeyCode) {
+ throw "Unknown key: " + aDOMKeyCode;
+ }
+ } else {
+ // If the key generates a character, it must cause a keypress event.
+ return true;
}
- utils.sendMouseScrollEvent(type, left + aOffsetX, top + aOffsetY, button,
- scrollFlags, aEvent.delta, modifiers);
+ }
+ switch (aDOMKeyCode) {
+ case KeyEvent.DOM_VK_SHIFT:
+ case KeyEvent.DOM_VK_CONTROL:
+ case KeyEvent.DOM_VK_ALT:
+ case KeyEvent.DOM_VK_CAPS_LOCK:
+ case KeyEvent.DOM_VK_NUM_LOCK:
+ case KeyEvent.DOM_VK_SCROLL_LOCK:
+ case KeyEvent.DOM_VK_META:
+ return false;
+ default:
+ return true;
}
}
@@ -313,7 +511,10 @@ function synthesizeMouseScroll(aTarget, aOffsetX, aOffsetY, aEvent, aWindow)
* VK_ENTER.
*
* aEvent is an object which may contain the properties:
- * shiftKey, ctrlKey, altKey, metaKey, accessKey, type
+ * shiftKey, ctrlKey, altKey, metaKey, accessKey, type, location
+ *
+ * Sets one of KeyboardEvent.DOM_KEY_LOCATION_* to location. Otherwise,
+ * DOMWindowUtils will choose good location from the keycode.
*
* If the type is specified, a key event of that type is fired. Otherwise,
* a keydown, a keypress and then a keyup event are fired in sequence.
@@ -322,29 +523,55 @@ function synthesizeMouseScroll(aTarget, aOffsetX, aOffsetY, aEvent, aWindow)
*/
function synthesizeKey(aKey, aEvent, aWindow)
{
- if (!aWindow)
- aWindow = window;
-
- var utils = aWindow.QueryInterface(Components.interfaces.nsIInterfaceRequestor).
- getInterface(Components.interfaces.nsIDOMWindowUtils);
+ var utils = _getDOMWindowUtils(aWindow);
if (utils) {
var keyCode = 0, charCode = 0;
- if (aKey.indexOf("VK_") == 0)
- keyCode = getKeyEvent(aWindow)["DOM_" + aKey];
- else
+ if (aKey.indexOf("VK_") == 0) {
+ keyCode = KeyEvent["DOM_" + aKey];
+ if (!keyCode) {
+ throw "Unknown key: " + aKey;
+ }
+ } else {
charCode = aKey.charCodeAt(0);
+ keyCode = _computeKeyCodeFromChar(aKey.charAt(0));
+ }
var modifiers = _parseModifiers(aEvent);
-
- if (aEvent.type) {
- utils.sendKeyEvent(aEvent.type, keyCode, charCode, modifiers);
+ var flags = 0;
+ if (aEvent.location != undefined) {
+ switch (aEvent.location) {
+ case KeyboardEvent.DOM_KEY_LOCATION_STANDARD:
+ flags |= utils.KEY_FLAG_LOCATION_STANDARD;
+ break;
+ case KeyboardEvent.DOM_KEY_LOCATION_LEFT:
+ flags |= utils.KEY_FLAG_LOCATION_LEFT;
+ break;
+ case KeyboardEvent.DOM_KEY_LOCATION_RIGHT:
+ flags |= utils.KEY_FLAG_LOCATION_RIGHT;
+ break;
+ case KeyboardEvent.DOM_KEY_LOCATION_NUMPAD:
+ flags |= utils.KEY_FLAG_LOCATION_NUMPAD;
+ break;
+ }
}
- else {
+
+ if (!("type" in aEvent) || !aEvent.type) {
+ // Send keydown + (optional) keypress + keyup events.
var keyDownDefaultHappened =
- utils.sendKeyEvent("keydown", keyCode, charCode, modifiers);
- utils.sendKeyEvent("keypress", keyCode, charCode, modifiers,
- !keyDownDefaultHappened);
- utils.sendKeyEvent("keyup", keyCode, charCode, modifiers);
+ utils.sendKeyEvent("keydown", keyCode, 0, modifiers, flags);
+ if (isKeypressFiredKey(keyCode)) {
+ if (!keyDownDefaultHappened) {
+ flags |= utils.KEY_FLAG_PREVENT_DEFAULT;
+ }
+ utils.sendKeyEvent("keypress", keyCode, charCode, modifiers, flags);
+ }
+ utils.sendKeyEvent("keyup", keyCode, 0, modifiers, flags);
+ } else if (aEvent.type == "keypress") {
+ // Send standalone keypress event.
+ utils.sendKeyEvent(aEvent.type, keyCode, charCode, modifiers, flags);
+ } else {
+ // Send other standalone event than keypress.
+ utils.sendKeyEvent(aEvent.type, keyCode, 0, modifiers, flags);
}
}
}
@@ -368,9 +595,7 @@ function _expectEvent(aExpectedTarget, aExpectedEvent, aTestName)
var eventHandler = function(event) {
var epassed = (!_gSeenEvent && event.originalTarget == aExpectedTarget &&
event.type == type);
- if (!epassed)
- throw new Error(aTestName + " " + type + " event target " +
- (_gSeenEvent ? "twice" : ""));
+ is(epassed, true, aTestName + " " + type + " event target " + (_gSeenEvent ? "twice" : ""));
_gSeenEvent = true;
};
@@ -389,10 +614,9 @@ function _checkExpectedEvent(aExpectedTarget, aExpectedEvent, aEventHandler, aTe
var type = expectEvent ? aExpectedEvent : aExpectedEvent.substring(1);
aExpectedTarget.removeEventListener(type, aEventHandler, false);
var desc = type + " event";
- if (expectEvent)
+ if (!expectEvent)
desc += " not";
- if (_gSeenEvent != expectEvent)
- throw new Error(aTestName + ": " + desc + " fired.");
+ is(_gSeenEvent, expectEvent, aTestName + " " + desc + " fired");
}
_gSeenEvent = false;
@@ -442,174 +666,64 @@ function synthesizeKeyExpectEvent(key, aEvent, aExpectedTarget, aExpectedEvent,
_checkExpectedEvent(aExpectedTarget, aExpectedEvent, eventHandler, aTestName);
}
-/**
- * Emulate a dragstart event.
- * element - element to fire the dragstart event on
- * expectedDragData - the data you expect the data transfer to contain afterwards
- * This data is in the format:
- * [ [ {type: value, data: value, test: function}, ... ], ... ]
- * can be null
- * aWindow - optional; defaults to the current window object.
- * x - optional; initial x coordinate
- * y - optional; initial y coordinate
- * Returns null if data matches.
- * Returns the event.dataTransfer if data does not match
- *
- * eqTest is an optional function if comparison can't be done with x == y;
- * function (actualData, expectedData) {return boolean}
- * @param actualData from dataTransfer
- * @param expectedData from expectedDragData
- * see bug 462172 for example of use
- *
- */
-function synthesizeDragStart(element, expectedDragData, aWindow, x, y)
+function disableNonTestMouseEvents(aDisable)
{
- if (!aWindow)
- aWindow = window;
- x = x || 2;
- y = y || 2;
- const step = 9;
-
- var result = "trapDrag was not called";
- var trapDrag = function(event) {
- try {
- var dataTransfer = event.dataTransfer;
- result = null;
- if (!dataTransfer)
- throw "no dataTransfer";
- if (expectedDragData == null ||
- dataTransfer.mozItemCount != expectedDragData.length)
- throw dataTransfer;
- for (var i = 0; i < dataTransfer.mozItemCount; i++) {
- var dtTypes = dataTransfer.mozTypesAt(i);
- if (dtTypes.length != expectedDragData[i].length)
- throw dataTransfer;
- for (var j = 0; j < dtTypes.length; j++) {
- if (dtTypes[j] != expectedDragData[i][j].type)
- throw dataTransfer;
- var dtData = dataTransfer.mozGetDataAt(dtTypes[j],i);
- if (expectedDragData[i][j].eqTest) {
- if (!expectedDragData[i][j].eqTest(dtData, expectedDragData[i][j].data))
- throw dataTransfer;
- }
- else if (expectedDragData[i][j].data != dtData)
- throw dataTransfer;
- }
- }
- } catch(ex) {
- result = ex;
- }
- event.preventDefault();
- event.stopPropagation();
- }
- aWindow.addEventListener("dragstart", trapDrag, false);
- synthesizeMouse(element, x, y, { type: "mousedown" }, aWindow);
- x += step; y += step;
- synthesizeMouse(element, x, y, { type: "mousemove" }, aWindow);
- x += step; y += step;
- synthesizeMouse(element, x, y, { type: "mousemove" }, aWindow);
- aWindow.removeEventListener("dragstart", trapDrag, false);
- synthesizeMouse(element, x, y, { type: "mouseup" }, aWindow);
- return result;
+ var domutils = _getDOMWindowUtils();
+ domutils.disableNonTestMouseEvents(aDisable);
}
-/**
- * Emulate a drop by emulating a dragstart and firing events dragenter, dragover, and drop.
- * srcElement - the element to use to start the drag, usually the same as destElement
- * but if destElement isn't suitable to start a drag on pass a suitable
- * element for srcElement
- * destElement - the element to fire the dragover, dragleave and drop events
- * dragData - the data to supply for the data transfer
- * This data is in the format:
- * [ [ {type: value, data: value}, ...], ... ]
- * dropEffect - the drop effect to set during the dragstart event, or 'move' if null
- * aWindow - optional; defaults to the current window object.
- *
- * Returns the drop effect that was desired.
- */
-function synthesizeDrop(srcElement, destElement, dragData, dropEffect, aWindow)
+function _getDOMWindowUtils(aWindow)
{
- if (!aWindow)
+ if (!aWindow) {
aWindow = window;
-
- var dataTransfer;
- var trapDrag = function(event) {
- dataTransfer = event.dataTransfer;
- for (var i = 0; i < dragData.length; i++) {
- var item = dragData[i];
- for (var j = 0; j < item.length; j++) {
- dataTransfer.mozSetDataAt(item[j].type, item[j].data, i);
- }
- }
- dataTransfer.dropEffect = dropEffect || "move";
- event.preventDefault();
- event.stopPropagation();
}
- // need to use real mouse action
- aWindow.addEventListener("dragstart", trapDrag, true);
- synthesizeMouse(srcElement, 2, 2, { type: "mousedown" }, aWindow);
- synthesizeMouse(srcElement, 11, 11, { type: "mousemove" }, aWindow);
- synthesizeMouse(srcElement, 20, 20, { type: "mousemove" }, aWindow);
- aWindow.removeEventListener("dragstart", trapDrag, true);
-
- event = aWindow.document.createEvent("DragEvents");
- event.initDragEvent("dragenter", true, true, aWindow, 0, 0, 0, 0, 0, false, false, false, false, 0, null, dataTransfer);
- destElement.dispatchEvent(event);
-
- var event = aWindow.document.createEvent("DragEvents");
- event.initDragEvent("dragover", true, true, aWindow, 0, 0, 0, 0, 0, false, false, false, false, 0, null, dataTransfer);
- if (destElement.dispatchEvent(event)) {
- synthesizeMouse(destElement, 20, 20, { type: "mouseup" }, aWindow);
- return "none";
+ // we need parent.SpecialPowers for:
+ // layout/base/tests/test_reftests_with_caret.html
+ // chrome: toolkit/content/tests/chrome/test_findbar.xul
+ // chrome: toolkit/content/tests/chrome/test_popup_anchor.xul
+ if ("SpecialPowers" in window && window.SpecialPowers != undefined) {
+ return SpecialPowers.getDOMWindowUtils(aWindow);
}
-
- if (dataTransfer.dropEffect != "none") {
- event = aWindow.document.createEvent("DragEvents");
- event.initDragEvent("drop", true, true, aWindow, 0, 0, 0, 0, 0, false, false, false, false, 0, null, dataTransfer);
- destElement.dispatchEvent(event);
+ if ("SpecialPowers" in parent && parent.SpecialPowers != undefined) {
+ return parent.SpecialPowers.getDOMWindowUtils(aWindow);
}
- synthesizeMouse(destElement, 20, 20, { type: "mouseup" }, aWindow);
- return dataTransfer.dropEffect;
+ //TODO: this is assuming we are in chrome space
+ return aWindow.QueryInterface(_EU_Ci.nsIInterfaceRequestor).
+ getInterface(_EU_Ci.nsIDOMWindowUtils);
}
-function disableNonTestMouseEvents(aDisable)
-{
- var utils =
- window.QueryInterface(Components.interfaces.nsIInterfaceRequestor).
- getInterface(Components.interfaces.nsIDOMWindowUtils);
- if (utils)
- utils.disableNonTestMouseEvents(aDisable);
-}
-
-function _getDOMWindowUtils(aWindow)
-{
- if (!aWindow) {
- aWindow = window;
- }
- return aWindow.QueryInterface(Components.interfaces.nsIInterfaceRequestor).
- getInterface(Components.interfaces.nsIDOMWindowUtils);
-}
+// Must be synchronized with nsIDOMWindowUtils.
+const COMPOSITION_ATTR_RAWINPUT = 0x02;
+const COMPOSITION_ATTR_SELECTEDRAWTEXT = 0x03;
+const COMPOSITION_ATTR_CONVERTEDTEXT = 0x04;
+const COMPOSITION_ATTR_SELECTEDCONVERTEDTEXT = 0x05;
/**
* Synthesize a composition event.
*
- * @param aIsCompositionStart If true, this synthesize compositionstart event.
- * Otherwise, compositionend event.
+ * @param aEvent The composition event information. This must
+ * have |type| member. The value must be
+ * "compositionstart", "compositionend" or
+ * "compositionupdate".
+ * And also this may have |data| and |locale| which
+ * would be used for the value of each property of
+ * the composition event. Note that the data would
+ * be ignored if the event type were
+ * "compositionstart".
* @param aWindow Optional (If null, current |window| will be used)
*/
-function synthesizeComposition(aIsCompositionStart, aWindow)
+function synthesizeComposition(aEvent, aWindow)
{
var utils = _getDOMWindowUtils(aWindow);
if (!utils) {
return;
}
- utils.sendCompositionEvent(aIsCompositionStart ?
- "compositionstart" : "compositionend");
+ utils.sendCompositionEvent(aEvent.type, aEvent.data ? aEvent.data : "",
+ aEvent.locale ? aEvent.locale : "");
}
-
/**
* Synthesize a text event.
*
@@ -702,123 +816,8 @@ function synthesizeQuerySelectedText(aWindow)
{
var utils = _getDOMWindowUtils(aWindow);
if (!utils) {
- return nullptr;
- }
- return utils.sendQueryContentEvent(utils.QUERY_SELECTED_TEXT, 0, 0, 0, 0);
-}
-
-/**
- * Synthesize a query text content event.
- *
- * @param aOffset The character offset. 0 means the first character in the
- * selection root.
- * @param aLength The length of getting text. If the length is too long,
- * the extra length is ignored.
- * @param aWindow Optional (If null, current |window| will be used)
- * @return An nsIQueryContentEventResult object. If this failed,
- * the result might be null.
- */
-function synthesizeQueryTextContent(aOffset, aLength, aWindow)
-{
- var utils = _getDOMWindowUtils(aWindow);
- if (!utils) {
- return nullptr;
- }
- return utils.sendQueryContentEvent(utils.QUERY_TEXT_CONTENT,
- aOffset, aLength, 0, 0);
-}
-
-/**
- * Synthesize a query caret rect event.
- *
- * @param aOffset The caret offset. 0 means left side of the first character
- * in the selection root.
- * @param aWindow Optional (If null, current |window| will be used)
- * @return An nsIQueryContentEventResult object. If this failed,
- * the result might be null.
- */
-function synthesizeQueryCaretRect(aOffset, aWindow)
-{
- var utils = _getDOMWindowUtils(aWindow);
- if (!utils) {
- return nullptr;
- }
- return utils.sendQueryContentEvent(utils.QUERY_CARET_RECT,
- aOffset, 0, 0, 0);
-}
-
-/**
- * Synthesize a query text rect event.
- *
- * @param aOffset The character offset. 0 means the first character in the
- * selection root.
- * @param aLength The length of the text. If the length is too long,
- * the extra length is ignored.
- * @param aWindow Optional (If null, current |window| will be used)
- * @return An nsIQueryContentEventResult object. If this failed,
- * the result might be null.
- */
-function synthesizeQueryTextRect(aOffset, aLength, aWindow)
-{
- var utils = _getDOMWindowUtils(aWindow);
- if (!utils) {
- return nullptr;
- }
- return utils.sendQueryContentEvent(utils.QUERY_TEXT_RECT,
- aOffset, aLength, 0, 0);
-}
-
-/**
- * Synthesize a query editor rect event.
- *
- * @param aWindow Optional (If null, current |window| will be used)
- * @return An nsIQueryContentEventResult object. If this failed,
- * the result might be null.
- */
-function synthesizeQueryEditorRect(aWindow)
-{
- var utils = _getDOMWindowUtils(aWindow);
- if (!utils) {
- return nullptr;
- }
- return utils.sendQueryContentEvent(utils.QUERY_EDITOR_RECT, 0, 0, 0, 0);
-}
-
-/**
- * Synthesize a character at point event.
- *
- * @param aX, aY The offset in the client area of the DOM window.
- * @param aWindow Optional (If null, current |window| will be used)
- * @return An nsIQueryContentEventResult object. If this failed,
- * the result might be null.
- */
-function synthesizeCharAtPoint(aX, aY, aWindow)
-{
- var utils = _getDOMWindowUtils(aWindow);
- if (!utils) {
- return nullptr;
+ return null;
}
- return utils.sendQueryContentEvent(utils.QUERY_CHARACTER_AT_POINT,
- 0, 0, aX, aY);
-}
-/**
- * Synthesize a selection set event.
- *
- * @param aOffset The character offset. 0 means the first character in the
- * selection root.
- * @param aLength The length of the text. If the length is too long,
- * the extra length is ignored.
- * @param aReverse If true, the selection is from |aOffset + aLength| to
- * |aOffset|. Otherwise, from |aOffset| to |aOffset + aLength|.
- * @param aWindow Optional (If null, current |window| will be used)
- * @return True, if succeeded. Otherwise false.
- */
-function synthesizeSelectionSet(aOffset, aLength, aReverse, aWindow)
-{
- var utils = _getDOMWindowUtils(aWindow);
- if (!utils) {
- return false;
- }
- return utils.sendSelectionSetEvent(aOffset, aLength, aReverse);
+ return utils.sendQueryContentEvent(utils.QUERY_SELECTED_TEXT, 0, 0, 0, 0);
}
diff --git a/services/sync/tps/extensions/mozmill/resource/stdlib/arrays.js b/services/sync/tps/extensions/mozmill/resource/stdlib/arrays.js
index f33beda38..c70a262c9 100644
--- a/services/sync/tps/extensions/mozmill/resource/stdlib/arrays.js
+++ b/services/sync/tps/extensions/mozmill/resource/stdlib/arrays.js
@@ -1,49 +1,65 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+ * file, you can obtain one at http://mozilla.org/MPL/2.0/. */
-var EXPORTED_SYMBOLS = ['inArray', 'getSet', 'indexOf', 'rindexOf', 'compare'];
+var EXPORTED_SYMBOLS = ['inArray', 'getSet', 'indexOf',
+ 'remove', 'rindexOf', 'compare'];
-function inArray (array, value) {
- for (i in array) {
+
+function remove(array, from, to) {
+ var rest = array.slice((to || from) + 1 || array.length);
+ array.length = from < 0 ? array.length + from : from;
+
+ return array.push.apply(array, rest);
+}
+
+function inArray(array, value) {
+ for (var i in array) {
if (value == array[i]) {
return true;
}
}
+
return false;
}
-function getSet (array) {
+function getSet(array) {
var narray = [];
- for (i in array) {
- if ( !inArray(narray, array[i]) ) {
+
+ for (var i in array) {
+ if (!inArray(narray, array[i])) {
narray.push(array[i]);
}
}
+
return narray;
}
-function indexOf (array, v, offset) {
- for (i in array) {
+function indexOf(array, v, offset) {
+ for (var i in array) {
if (offset == undefined || i >= offset) {
- if ( !isNaN(i) && array[i] == v) {
+ if (!isNaN(i) && array[i] == v) {
return new Number(i);
}
}
}
+
return -1;
}
function rindexOf (array, v) {
var l = array.length;
- for (i in array) {
+
+ for (var i in array) {
if (!isNaN(i)) {
- var i = new Number(i)
+ var i = new Number(i);
}
+
if (!isNaN(i) && array[l - i] == v) {
return l - i;
}
}
+
return -1;
}
@@ -51,10 +67,12 @@ function compare (array, carray) {
if (array.length != carray.length) {
return false;
}
- for (i in array) {
+
+ for (var i in array) {
if (array[i] != carray[i]) {
return false;
}
}
+
return true;
}
diff --git a/services/sync/tps/extensions/mozmill/resource/stdlib/dom.js b/services/sync/tps/extensions/mozmill/resource/stdlib/dom.js
index 1592a7411..06bfcb529 100644
--- a/services/sync/tps/extensions/mozmill/resource/stdlib/dom.js
+++ b/services/sync/tps/extensions/mozmill/resource/stdlib/dom.js
@@ -1,21 +1,24 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+ * file, you can obtain one at http://mozilla.org/MPL/2.0/. */
var EXPORTED_SYMBOLS = ['getAttributes'];
var getAttributes = function (node) {
var attributes = {};
- for (i in node.attributes) {
- if ( !isNaN(i) ) {
+
+ for (var i in node.attributes) {
+ if (!isNaN(i)) {
try {
var attr = node.attributes[i];
attributes[attr.name] = attr.value;
- } catch (err) {
+ }
+ catch (e) {
}
}
}
+
return attributes;
}
diff --git a/services/sync/tps/extensions/mozmill/resource/stdlib/httpd.js b/services/sync/tps/extensions/mozmill/resource/stdlib/httpd.js
index 6b58b6607..c5eea6251 100644
--- a/services/sync/tps/extensions/mozmill/resource/stdlib/httpd.js
+++ b/services/sync/tps/extensions/mozmill/resource/stdlib/httpd.js
@@ -10,15 +10,35 @@
* httpd.js.
*/
-Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
-
-var EXPORTED_SYMBOLS = ['getServer'];
+this.EXPORTED_SYMBOLS = [
+ "HTTP_400",
+ "HTTP_401",
+ "HTTP_402",
+ "HTTP_403",
+ "HTTP_404",
+ "HTTP_405",
+ "HTTP_406",
+ "HTTP_407",
+ "HTTP_408",
+ "HTTP_409",
+ "HTTP_410",
+ "HTTP_411",
+ "HTTP_412",
+ "HTTP_413",
+ "HTTP_414",
+ "HTTP_415",
+ "HTTP_417",
+ "HTTP_500",
+ "HTTP_501",
+ "HTTP_502",
+ "HTTP_503",
+ "HTTP_504",
+ "HTTP_505",
+ "HttpError",
+ "HttpServer",
+];
-/**
- * Overwrite both dump functions because we do not wanna have this output for Mozmill
- */
-function dump() {}
-function dumpn() {}
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
const Cc = Components.classes;
const Ci = Components.interfaces;
@@ -58,7 +78,7 @@ function NS_ASSERT(cond, msg)
}
/** Constructs an HTTP error object. */
-function HttpError(code, description)
+this.HttpError = function HttpError(code, description)
{
this.code = code;
this.description = description;
@@ -74,30 +94,30 @@ HttpError.prototype =
/**
* Errors thrown to trigger specific HTTP server responses.
*/
-const HTTP_400 = new HttpError(400, "Bad Request");
-const HTTP_401 = new HttpError(401, "Unauthorized");
-const HTTP_402 = new HttpError(402, "Payment Required");
-const HTTP_403 = new HttpError(403, "Forbidden");
-const HTTP_404 = new HttpError(404, "Not Found");
-const HTTP_405 = new HttpError(405, "Method Not Allowed");
-const HTTP_406 = new HttpError(406, "Not Acceptable");
-const HTTP_407 = new HttpError(407, "Proxy Authentication Required");
-const HTTP_408 = new HttpError(408, "Request Timeout");
-const HTTP_409 = new HttpError(409, "Conflict");
-const HTTP_410 = new HttpError(410, "Gone");
-const HTTP_411 = new HttpError(411, "Length Required");
-const HTTP_412 = new HttpError(412, "Precondition Failed");
-const HTTP_413 = new HttpError(413, "Request Entity Too Large");
-const HTTP_414 = new HttpError(414, "Request-URI Too Long");
-const HTTP_415 = new HttpError(415, "Unsupported Media Type");
-const HTTP_417 = new HttpError(417, "Expectation Failed");
-
-const HTTP_500 = new HttpError(500, "Internal Server Error");
-const HTTP_501 = new HttpError(501, "Not Implemented");
-const HTTP_502 = new HttpError(502, "Bad Gateway");
-const HTTP_503 = new HttpError(503, "Service Unavailable");
-const HTTP_504 = new HttpError(504, "Gateway Timeout");
-const HTTP_505 = new HttpError(505, "HTTP Version Not Supported");
+this.HTTP_400 = new HttpError(400, "Bad Request");
+this.HTTP_401 = new HttpError(401, "Unauthorized");
+this.HTTP_402 = new HttpError(402, "Payment Required");
+this.HTTP_403 = new HttpError(403, "Forbidden");
+this.HTTP_404 = new HttpError(404, "Not Found");
+this.HTTP_405 = new HttpError(405, "Method Not Allowed");
+this.HTTP_406 = new HttpError(406, "Not Acceptable");
+this.HTTP_407 = new HttpError(407, "Proxy Authentication Required");
+this.HTTP_408 = new HttpError(408, "Request Timeout");
+this.HTTP_409 = new HttpError(409, "Conflict");
+this.HTTP_410 = new HttpError(410, "Gone");
+this.HTTP_411 = new HttpError(411, "Length Required");
+this.HTTP_412 = new HttpError(412, "Precondition Failed");
+this.HTTP_413 = new HttpError(413, "Request Entity Too Large");
+this.HTTP_414 = new HttpError(414, "Request-URI Too Long");
+this.HTTP_415 = new HttpError(415, "Unsupported Media Type");
+this.HTTP_417 = new HttpError(417, "Expectation Failed");
+
+this.HTTP_500 = new HttpError(500, "Internal Server Error");
+this.HTTP_501 = new HttpError(501, "Not Implemented");
+this.HTTP_502 = new HttpError(502, "Bad Gateway");
+this.HTTP_503 = new HttpError(503, "Service Unavailable");
+this.HTTP_504 = new HttpError(504, "Gateway Timeout");
+this.HTTP_505 = new HttpError(505, "HTTP Version Not Supported");
/** Creates a hash with fields corresponding to the values in arr. */
function array2obj(arr)
@@ -266,7 +286,7 @@ function toDateString(date)
{
var hrs = date.getUTCHours();
var rv = (hrs < 10) ? "0" + hrs : hrs;
-
+
var mins = date.getUTCMinutes();
rv += ":";
rv += (mins < 10) ? "0" + mins : mins;
@@ -451,7 +471,15 @@ nsHttpServer.prototype =
onStopListening: function(socket, status)
{
dumpn(">>> shutting down server on port " + socket.port);
+ for (var n in this._connections) {
+ if (!this._connections[n]._requestStarted) {
+ this._connections[n].close();
+ }
+ }
this._socketClosed = true;
+ if (this._hasOpenConnections()) {
+ dumpn("*** open connections!!!");
+ }
if (!this._hasOpenConnections())
{
dumpn("*** no open connections, notifying async from onStopListening");
@@ -493,12 +521,14 @@ nsHttpServer.prototype =
this._host = host;
// The listen queue needs to be long enough to handle
- // network.http.max-persistent-connections-per-server concurrent connections,
- // plus a safety margin in case some other process is talking to
- // the server as well.
+ // network.http.max-persistent-connections-per-server or
+ // network.http.max-persistent-connections-per-proxy concurrent
+ // connections, plus a safety margin in case some other process is
+ // talking to the server as well.
var prefs = getRootPrefBranch();
- var maxConnections =
- prefs.getIntPref("network.http.max-persistent-connections-per-server") + 5;
+ var maxConnections = 5 + Math.max(
+ prefs.getIntPref("network.http.max-persistent-connections-per-server"),
+ prefs.getIntPref("network.http.max-persistent-connections-per-proxy"));
try
{
@@ -507,18 +537,52 @@ nsHttpServer.prototype =
var loopback = false;
}
- var socket = new ServerSocket(this._port,
+ // When automatically selecting a port, sometimes the chosen port is
+ // "blocked" from clients. We don't want to use these ports because
+ // tests will intermittently fail. So, we simply keep trying to to
+ // get a server socket until a valid port is obtained. We limit
+ // ourselves to finite attempts just so we don't loop forever.
+ var ios = Cc["@mozilla.org/network/io-service;1"]
+ .getService(Ci.nsIIOService);
+ var socket;
+ for (var i = 100; i; i--)
+ {
+ var temp = new ServerSocket(this._port,
loopback, // true = localhost, false = everybody
maxConnections);
+
+ var allowed = ios.allowPort(temp.port, "http");
+ if (!allowed)
+ {
+ dumpn(">>>Warning: obtained ServerSocket listens on a blocked " +
+ "port: " + temp.port);
+ }
+
+ if (!allowed && this._port == -1)
+ {
+ dumpn(">>>Throwing away ServerSocket with bad port.");
+ temp.close();
+ continue;
+ }
+
+ socket = temp;
+ break;
+ }
+
+ if (!socket) {
+ throw new Error("No socket server available. Are there no available ports?");
+ }
+
dumpn(">>> listening on port " + socket.port + ", " + maxConnections +
" pending connections");
socket.asyncListen(this);
- this._identity._initialize(port, host, true);
+ this._port = socket.port;
+ this._identity._initialize(socket.port, host, true);
this._socket = socket;
}
catch (e)
{
- dumpn("!!! could not start server on port " + port + ": " + e);
+ dump("\n!!! could not start server on port " + port + ": " + e + "\n\n");
throw Cr.NS_ERROR_NOT_AVAILABLE;
}
},
@@ -588,6 +652,14 @@ nsHttpServer.prototype =
},
//
+ // see nsIHttpServer.registerPrefixHandler
+ //
+ registerPrefixHandler: function(prefix, handler)
+ {
+ this._handler.registerPrefixHandler(prefix, handler);
+ },
+
+ //
// see nsIHttpServer.registerErrorHandler
//
registerErrorHandler: function(code, handler)
@@ -759,6 +831,10 @@ nsHttpServer.prototype =
// Fire a pending server-stopped notification if it's our responsibility.
if (!this._hasOpenConnections() && this._socketClosed)
this._notifyStopped();
+ // Bug 508125: Add a GC here else we'll use gigabytes of memory running
+ // mochitests. We can't rely on xpcshell doing an automated GC, as that
+ // would interfere with testing GC stuff...
+ Components.utils.forceGC();
},
/**
@@ -772,6 +848,7 @@ nsHttpServer.prototype =
}
};
+this.HttpServer = nsHttpServer;
//
// RFC 2396 section 3.2.2:
@@ -790,7 +867,7 @@ const HOST_REGEX =
// toplabel
"[a-z](?:[a-z0-9-]*[a-z0-9])?" +
"|" +
- // IPv4 address
+ // IPv4 address
"\\d+\\.\\d+\\.\\d+\\.\\d+" +
")$",
"i");
@@ -1001,7 +1078,7 @@ ServerIdentity.prototype =
// Not the default primary location, nothing special to do here
this.remove("http", "127.0.0.1", this._defaultPort);
}
-
+
// This is a *very* tricky bit of reasoning here; make absolutely sure the
// tests for this code pass before you commit changes to it.
if (this._primaryScheme == "http" &&
@@ -1097,14 +1174,25 @@ function Connection(input, output, server, port, outgoingPort, number)
*/
this.request = null;
- /** State variables for debugging. */
- this._closed = this._processed = false;
+ /** This allows a connection to disambiguate between a peer initiating a
+ * close and the socket being forced closed on shutdown.
+ */
+ this._closed = false;
+
+ /** State variable for debugging. */
+ this._processed = false;
+
+ /** whether or not 1st line of request has been received */
+ this._requestStarted = false;
}
Connection.prototype =
{
/** Closes this connection's input/output streams. */
close: function()
{
+ if (this._closed)
+ return;
+
dumpn("*** closing connection " + this.number +
" on port " + this._outgoingPort);
@@ -1162,6 +1250,11 @@ Connection.prototype =
return "<Connection(" + this.number +
(this.request ? ", " + this.request.path : "") +"): " +
(this._closed ? "closed" : "open") + ">";
+ },
+
+ requestStarted: function()
+ {
+ this._requestStarted = true;
}
};
@@ -1348,6 +1441,7 @@ RequestReader.prototype =
{
this._parseRequestLine(line.value);
this._state = READER_IN_HEADERS;
+ this._connection.requestStarted();
return true;
}
catch (e)
@@ -1433,7 +1527,7 @@ RequestReader.prototype =
this._handleResponse();
return true;
}
-
+
return false;
}
catch (e)
@@ -1606,7 +1700,10 @@ RequestReader.prototype =
// between fields, even though only a single SP is required (section 19.3)
var request = line.split(/[ \t]+/);
if (!request || request.length != 3)
+ {
+ dumpn("*** No request in line");
throw HTTP_400;
+ }
metadata._method = request[0];
@@ -1614,7 +1711,10 @@ RequestReader.prototype =
var ver = request[2];
var match = ver.match(/^HTTP\/(\d+\.\d+)$/);
if (!match)
+ {
+ dumpn("*** No HTTP version in line");
throw HTTP_400;
+ }
// determine HTTP version
try
@@ -1639,7 +1739,10 @@ RequestReader.prototype =
{
// No absolute paths in the request line in HTTP prior to 1.1
if (!metadata._httpVersion.atLeast(nsHttpVersion.HTTP_1_1))
+ {
+ dumpn("*** Metadata version too low");
throw HTTP_400;
+ }
try
{
@@ -1653,11 +1756,18 @@ RequestReader.prototype =
if (port === -1)
{
if (scheme === "http")
+ {
port = 80;
+ }
else if (scheme === "https")
+ {
port = 443;
+ }
else
+ {
+ dumpn("*** Unknown scheme: " + scheme);
throw HTTP_400;
+ }
}
}
catch (e)
@@ -1665,11 +1775,15 @@ RequestReader.prototype =
// If the host is not a valid host on the server, the response MUST be a
// 400 (Bad Request) error message (section 5.2). Alternately, the URI
// is malformed.
+ dumpn("*** Threw when dealing with URI: " + e);
throw HTTP_400;
}
if (!serverIdentity.has(scheme, host, port) || fullPath.charAt(0) != "/")
+ {
+ dumpn("*** serverIdentity unknown or path does not start with '/'");
throw HTTP_400;
+ }
}
var splitter = fullPath.indexOf("?");
@@ -1713,6 +1827,8 @@ RequestReader.prototype =
var line = {};
while (true)
{
+ dumpn("*** Last name: '" + lastName + "'");
+ dumpn("*** Last val: '" + lastVal + "'");
NS_ASSERT(!((lastVal === undefined) ^ (lastName === undefined)),
lastName === undefined ?
"lastVal without lastName? lastVal: '" + lastVal + "'" :
@@ -1727,6 +1843,7 @@ RequestReader.prototype =
}
var lineText = line.value;
+ dumpn("*** Line text: '" + lineText + "'");
var firstChar = lineText.charAt(0);
// blank line means end of headers
@@ -1741,7 +1858,7 @@ RequestReader.prototype =
}
catch (e)
{
- dumpn("*** e == " + e);
+ dumpn("*** setHeader threw on last header, e == " + e);
throw HTTP_400;
}
}
@@ -1759,7 +1876,7 @@ RequestReader.prototype =
// multi-line header if we've already seen a header line
if (!lastName)
{
- // we don't have a header to continue!
+ dumpn("We don't have a header to continue!");
throw HTTP_400;
}
@@ -1778,7 +1895,7 @@ RequestReader.prototype =
}
catch (e)
{
- dumpn("*** e == " + e);
+ dumpn("*** setHeader threw on a header, e == " + e);
throw HTTP_400;
}
}
@@ -1786,7 +1903,7 @@ RequestReader.prototype =
var colon = lineText.indexOf(":"); // first colon must be splitter
if (colon < 1)
{
- // no colon or missing header field-name
+ dumpn("*** No colon or missing header field-name");
throw HTTP_400;
}
@@ -1811,12 +1928,14 @@ const CR = 0x0D, LF = 0x0A;
* character; the first CRLF is the lowest index i where
* |array[i] == "\r".charCodeAt(0)| and |array[i+1] == "\n".charCodeAt(0)|,
* if such an |i| exists, and -1 otherwise
+ * @param start : uint
+ * start index from which to begin searching in array
* @returns int
* the index of the first CRLF if any were present, -1 otherwise
*/
-function findCRLF(array)
+function findCRLF(array, start)
{
- for (var i = array.indexOf(CR); i >= 0; i = array.indexOf(CR, i + 1))
+ for (var i = array.indexOf(CR, start); i >= 0; i = array.indexOf(CR, i + 1))
{
if (array[i + 1] == LF)
return i;
@@ -1833,6 +1952,9 @@ function LineData()
{
/** An array of queued bytes from which to get line-based characters. */
this._data = [];
+
+ /** Start index from which to search for CRLF. */
+ this._start = 0;
}
LineData.prototype =
{
@@ -1842,7 +1964,22 @@ LineData.prototype =
*/
appendBytes: function(bytes)
{
- Array.prototype.push.apply(this._data, bytes);
+ var count = bytes.length;
+ var quantum = 262144; // just above half SpiderMonkey's argument-count limit
+ if (count < quantum)
+ {
+ Array.prototype.push.apply(this._data, bytes);
+ return;
+ }
+
+ // Large numbers of bytes may cause Array.prototype.push to be called with
+ // more arguments than the JavaScript engine supports. In that case append
+ // bytes in fixed-size amounts until all bytes are appended.
+ for (var start = 0; start < count; start += quantum)
+ {
+ var slice = bytes.slice(start, Math.min(start + quantum, count));
+ Array.prototype.push.apply(this._data, slice);
+ }
},
/**
@@ -1860,23 +1997,38 @@ LineData.prototype =
readLine: function(out)
{
var data = this._data;
- var length = findCRLF(data);
+ var length = findCRLF(data, this._start);
if (length < 0)
+ {
+ this._start = data.length;
+
+ // But if our data ends in a CR, we have to back up one, because
+ // the first byte in the next packet might be an LF and if we
+ // start looking at data.length we won't find it.
+ if (data.length > 0 && data[data.length - 1] === CR)
+ --this._start;
+
return false;
+ }
+
+ // Reset for future lines.
+ this._start = 0;
//
// We have the index of the CR, so remove all the characters, including
- // CRLF, from the array with splice, and convert the removed array into the
- // corresponding string, from which we then strip the trailing CRLF.
+ // CRLF, from the array with splice, and convert the removed array
+ // (excluding the trailing CRLF characters) into the corresponding string.
//
- // Getting the line in this matter acknowledges that substring is an O(1)
- // operation in SpiderMonkey because strings are immutable, whereas two
- // splices, both from the beginning of the data, are less likely to be as
- // cheap as a single splice plus two extra character conversions.
- //
- var line = String.fromCharCode.apply(null, data.splice(0, length + 2));
- out.value = line.substring(0, length);
+ var leading = data.splice(0, length + 2);
+ var quantum = 262144;
+ var line = "";
+ for (var start = 0; start < length; start += quantum)
+ {
+ var slice = leading.slice(start, Math.min(start + quantum, length));
+ line += String.fromCharCode.apply(null, slice);
+ }
+ out.value = line;
return true;
},
@@ -1911,7 +2063,7 @@ function createHandlerFunc(handler)
*/
function defaultIndexHandler(metadata, response)
{
- response.setHeader("Content-Type", "text/html", false);
+ response.setHeader("Content-Type", "text/html;charset=utf-8", false);
var path = htmlEscape(decodeURI(metadata.path));
@@ -2018,6 +2170,7 @@ function toInternalPath(path, encoded)
return comps.join("/");
}
+const PERMS_READONLY = (4 << 6) | (4 << 3) | 4;
/**
* Adds custom-specified headers for the given file to the given response, if
@@ -2045,7 +2198,7 @@ function maybeAddHeaders(file, metadata, response)
return;
const PR_RDONLY = 0x01;
- var fis = new FileInputStream(headerFile, PR_RDONLY, 0444,
+ var fis = new FileInputStream(headerFile, PR_RDONLY, PERMS_READONLY,
Ci.nsIFileInputStream.CLOSE_ON_EOF);
try
@@ -2078,7 +2231,7 @@ function maybeAddHeaders(file, metadata, response)
code = status.substring(0, space);
description = status.substring(space + 1, status.length);
}
-
+
response.setStatusLine(metadata.httpVersion, parseInt(code, 10), description);
line.value = "";
@@ -2150,6 +2303,15 @@ function ServerHandler(server)
this._overridePaths = {};
/**
+ * Custom request handlers for the path prefixes on the server in which this
+ * resides. Path-handler pairs are stored as property-value pairs in this
+ * property.
+ *
+ * @see ServerHandler.prototype._defaultPaths
+ */
+ this._overridePrefixes = {};
+
+ /**
* Custom request handlers for the error handlers in the server in which this
* resides. Path-handler pairs are stored as property-value pairs in this
* property.
@@ -2213,7 +2375,23 @@ ServerHandler.prototype =
}
else
{
- this._handleDefault(request, response);
+ var longestPrefix = "";
+ for (let prefix in this._overridePrefixes) {
+ if (prefix.length > longestPrefix.length &&
+ path.substr(0, prefix.length) == prefix)
+ {
+ longestPrefix = prefix;
+ }
+ }
+ if (longestPrefix.length > 0)
+ {
+ dumpn("calling prefix override for " + longestPrefix);
+ this._overridePrefixes[longestPrefix](request, response);
+ }
+ else
+ {
+ this._handleDefault(request, response);
+ }
}
}
catch (e)
@@ -2319,6 +2497,18 @@ ServerHandler.prototype =
},
//
+ // see nsIHttpServer.registerPrefixHandler
+ //
+ registerPrefixHandler: function(path, handler)
+ {
+ // XXX true path validation!
+ if (path.charAt(0) != "/" || path.charAt(path.length - 1) != "/")
+ throw Cr.NS_ERROR_INVALID_ARG;
+
+ this._handlerToField(handler, this._overridePrefixes, path);
+ },
+
+ //
// see nsIHttpServer.registerDirectory
//
registerDirectory: function(path, directory)
@@ -2458,7 +2648,10 @@ ServerHandler.prototype =
{
var rangeMatch = metadata.getHeader("Range").match(/^bytes=(\d+)?-(\d+)?$/);
if (!rangeMatch)
+ {
+ dumpn("*** Range header bogosity: '" + metadata.getHeader("Range") + "'");
throw HTTP_400;
+ }
if (rangeMatch[1] !== undefined)
start = parseInt(rangeMatch[1], 10);
@@ -2467,7 +2660,10 @@ ServerHandler.prototype =
end = parseInt(rangeMatch[2], 10);
if (start === undefined && end === undefined)
+ {
+ dumpn("*** More Range header bogosity: '" + metadata.getHeader("Range") + "'");
throw HTTP_400;
+ }
// No start given, so the end is really the count of bytes from the
// end of the file.
@@ -2537,7 +2733,7 @@ ServerHandler.prototype =
var type = this._getTypeFromFile(file);
if (type === SJS_TYPE)
{
- var fis = new FileInputStream(file, PR_RDONLY, 0444,
+ var fis = new FileInputStream(file, PR_RDONLY, PERMS_READONLY,
Ci.nsIFileInputStream.CLOSE_ON_EOF);
try
@@ -2574,6 +2770,10 @@ ServerHandler.prototype =
{
self._setObjectState(k, v);
});
+ s.importFunction(function registerPathHandler(p, h)
+ {
+ self.registerPathHandler(p, h);
+ });
// Make it possible for sjs files to access their location
this._setState(path, "__LOCATION__", file.path);
@@ -2586,7 +2786,7 @@ ServerHandler.prototype =
// getting the line number where we evaluate the SJS file. Don't
// separate these two lines!
var line = new Error().lineNumber;
- Cu.evalInSandbox(sis.read(file.fileSize), s);
+ Cu.evalInSandbox(sis.read(file.fileSize), s, "latest");
}
catch (e)
{
@@ -2627,7 +2827,7 @@ ServerHandler.prototype =
maybeAddHeaders(file, metadata, response);
response.setHeader("Content-Length", "" + count, false);
- var fis = new FileInputStream(file, PR_RDONLY, 0444,
+ var fis = new FileInputStream(file, PR_RDONLY, PERMS_READONLY,
Ci.nsIFileInputStream.CLOSE_ON_EOF);
offset = offset || 0;
@@ -2878,6 +3078,7 @@ ServerHandler.prototype =
}
catch (e)
{
+ dumpn("*** toInternalPath threw " + e);
throw HTTP_400; // malformed path
}
@@ -2962,7 +3163,7 @@ ServerHandler.prototype =
dumpn("*** error in request: " + errorCode);
this._handleError(errorCode, new Request(connection.port), response);
- },
+ },
/**
* Handles a request which generates the given error code, using the
@@ -3072,7 +3273,7 @@ ServerHandler.prototype =
{
// none of the data in metadata is reliable, so hard-code everything here
response.setStatusLine("1.1", 400, "Bad Request");
- response.setHeader("Content-Type", "text/plain", false);
+ response.setHeader("Content-Type", "text/plain;charset=utf-8", false);
var body = "Bad request\n";
response.bodyOutputStream.write(body, body.length);
@@ -3080,7 +3281,7 @@ ServerHandler.prototype =
403: function(metadata, response)
{
response.setStatusLine(metadata.httpVersion, 403, "Forbidden");
- response.setHeader("Content-Type", "text/html", false);
+ response.setHeader("Content-Type", "text/html;charset=utf-8", false);
var body = "<html>\
<head><title>403 Forbidden</title></head>\
@@ -3093,7 +3294,7 @@ ServerHandler.prototype =
404: function(metadata, response)
{
response.setStatusLine(metadata.httpVersion, 404, "Not Found");
- response.setHeader("Content-Type", "text/html", false);
+ response.setHeader("Content-Type", "text/html;charset=utf-8", false);
var body = "<html>\
<head><title>404 Not Found</title></head>\
@@ -3113,7 +3314,7 @@ ServerHandler.prototype =
response.setStatusLine(metadata.httpVersion,
416,
"Requested Range Not Satisfiable");
- response.setHeader("Content-Type", "text/html", false);
+ response.setHeader("Content-Type", "text/html;charset=utf-8", false);
var body = "<html>\
<head>\
@@ -3132,7 +3333,7 @@ ServerHandler.prototype =
response.setStatusLine(metadata.httpVersion,
500,
"Internal Server Error");
- response.setHeader("Content-Type", "text/html", false);
+ response.setHeader("Content-Type", "text/html;charset=utf-8", false);
var body = "<html>\
<head><title>500 Internal Server Error</title></head>\
@@ -3147,7 +3348,7 @@ ServerHandler.prototype =
501: function(metadata, response)
{
response.setStatusLine(metadata.httpVersion, 501, "Not Implemented");
- response.setHeader("Content-Type", "text/html", false);
+ response.setHeader("Content-Type", "text/html;charset=utf-8", false);
var body = "<html>\
<head><title>501 Not Implemented</title></head>\
@@ -3161,7 +3362,7 @@ ServerHandler.prototype =
505: function(metadata, response)
{
response.setStatusLine("1.1", 505, "HTTP Version Not Supported");
- response.setHeader("Content-Type", "text/html", false);
+ response.setHeader("Content-Type", "text/html;charset=utf-8", false);
var body = "<html>\
<head><title>505 HTTP Version Not Supported</title></head>\
@@ -3183,7 +3384,7 @@ ServerHandler.prototype =
"/": function(metadata, response)
{
response.setStatusLine(metadata.httpVersion, 200, "OK");
- response.setHeader("Content-Type", "text/html", false);
+ response.setHeader("Content-Type", "text/html;charset=utf-8", false);
var body = "<html>\
<head><title>httpd.js</title></head>\
@@ -3201,7 +3402,7 @@ ServerHandler.prototype =
"/trace": function(metadata, response)
{
response.setStatusLine(metadata.httpVersion, 200, "OK");
- response.setHeader("Content-Type", "text/plain", false);
+ response.setHeader("Content-Type", "text/plain;charset=utf-8", false);
var body = "Request-URI: " +
metadata.scheme + "://" + metadata.host + ":" + metadata.port +
@@ -3211,7 +3412,7 @@ ServerHandler.prototype =
if (metadata.queryString)
body += "?" + metadata.queryString;
-
+
body += " HTTP/" + metadata.httpVersion + "\r\n";
var headEnum = metadata.headers;
@@ -4569,7 +4770,10 @@ const headerUtils =
normalizeFieldName: function(fieldName)
{
if (fieldName == "")
+ {
+ dumpn("*** Empty fieldName");
throw Cr.NS_ERROR_INVALID_ARG;
+ }
for (var i = 0, sz = fieldName.length; i < sz; i++)
{
@@ -4620,9 +4824,13 @@ const headerUtils =
val = val.replace(/^ +/, "").replace(/ +$/, "");
// that should have taken care of all CTLs, so val should contain no CTLs
+ dumpn("*** Normalized value: '" + val + "'");
for (var i = 0, len = val.length; i < len; i++)
if (isCTL(val.charCodeAt(i)))
+ {
+ dump("*** Char " + i + " has charcode " + val.charCodeAt(i));
throw Cr.NS_ERROR_INVALID_ARG;
+ }
// XXX disallows quoted-pair where CHAR is a CTL -- will not invalidly
// normalize, however, so this can be construed as a tightening of the
@@ -4757,17 +4965,17 @@ nsHttpHeaders.prototype =
var value = headerUtils.normalizeFieldValue(fieldValue);
// The following three headers are stored as arrays because their real-world
- // syntax prevents joining individual headers into a single header using
+ // syntax prevents joining individual headers into a single header using
// ",". See also <http://hg.mozilla.org/mozilla-central/diff/9b2a99adc05e/netwerk/protocol/http/src/nsHttpHeaderArray.cpp#l77>
if (merge && name in this._headers)
{
if (name === "www-authenticate" ||
name === "proxy-authenticate" ||
- name === "set-cookie")
+ name === "set-cookie")
{
this._headers[name].push(value);
}
- else
+ else
{
this._headers[name][0] += "," + value;
NS_ASSERT(this._headers[name].length === 1,
@@ -4790,8 +4998,8 @@ nsHttpHeaders.prototype =
* @returns string
* the field value for the given header, possibly with non-semantic changes
* (i.e., leading/trailing whitespace stripped, whitespace runs replaced
- * with spaces, etc.) at the option of the implementation; multiple
- * instances of the header will be combined with a comma, except for
+ * with spaces, etc.) at the option of the implementation; multiple
+ * instances of the header will be combined with a comma, except for
* the three headers noted in the description of getHeaderValues
*/
getHeader: function(fieldName)
@@ -5053,7 +5261,7 @@ Request.prototype =
//
// see nsIPropertyBag.getProperty
//
- getProperty: function(name)
+ getProperty: function(name)
{
this._ensurePropertyBag();
return this._bag.getProperty(name);
@@ -5075,7 +5283,7 @@ Request.prototype =
// PRIVATE IMPLEMENTATION
-
+
/** Ensures a property bag has been created for ad-hoc behaviors. */
_ensurePropertyBag: function()
{
@@ -5086,10 +5294,8 @@ Request.prototype =
// XPCOM trappings
-if (XPCOMUtils.generateNSGetFactory)
- var NSGetFactory = XPCOMUtils.generateNSGetFactory([nsHttpServer]);
-else
- var NSGetModule = XPCOMUtils.generateNSGetModule([nsHttpServer]);
+
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory([nsHttpServer]);
/**
* Creates a new HTTP server listening for loopback traffic on the given port,
@@ -5147,20 +5353,3 @@ function server(port, basePath)
DEBUG = false;
}
-
-function getServer (port, basePath) {
- if (basePath) {
- var lp = Cc["@mozilla.org/file/local;1"]
- .createInstance(Ci.nsILocalFile);
- lp.initWithPath(basePath);
- }
-
- var srv = new nsHttpServer();
- if (lp)
- srv.registerDirectory("/", lp);
- srv.registerContentType("sjs", SJS_TYPE);
- srv.identity.setPrimary("http", "localhost", port);
- srv._port = port;
-
- return srv;
-}
diff --git a/services/sync/tps/extensions/mozmill/resource/stdlib/objects.js b/services/sync/tps/extensions/mozmill/resource/stdlib/objects.js
index 5c2b024a1..576117145 100644
--- a/services/sync/tps/extensions/mozmill/resource/stdlib/objects.js
+++ b/services/sync/tps/extensions/mozmill/resource/stdlib/objects.js
@@ -1,6 +1,6 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+ * file, you can obtain one at http://mozilla.org/MPL/2.0/. */
var EXPORTED_SYMBOLS = ['getLength', ];//'compare'];
@@ -9,6 +9,7 @@ var getLength = function (obj) {
for (i in obj) {
len++;
}
+
return len;
}
diff --git a/services/sync/tps/extensions/mozmill/resource/stdlib/os.js b/services/sync/tps/extensions/mozmill/resource/stdlib/os.js
index f515b9a01..fcda30572 100644
--- a/services/sync/tps/extensions/mozmill/resource/stdlib/os.js
+++ b/services/sync/tps/extensions/mozmill/resource/stdlib/os.js
@@ -1,38 +1,46 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+ * file, you can obtain one at http://mozilla.org/MPL/2.0/. */
var EXPORTED_SYMBOLS = ['listDirectory', 'getFileForPath', 'abspath', 'getPlatform'];
-function listDirectory (file) {
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cu = Components.utils;
+
+Cu.import("resource://gre/modules/Services.jsm");
+
+function listDirectory(file) {
// file is the given directory (nsIFile)
var entries = file.directoryEntries;
var array = [];
- while (entries.hasMoreElements())
- {
+
+ while (entries.hasMoreElements()) {
var entry = entries.getNext();
- entry.QueryInterface(Components.interfaces.nsIFile);
+ entry.QueryInterface(Ci.nsIFile);
array.push(entry);
}
+
return array;
}
-function getFileForPath (path) {
- var file = Components.classes["@mozilla.org/file/local;1"]
- .createInstance(Components.interfaces.nsILocalFile);
+function getFileForPath(path) {
+ var file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsILocalFile);
file.initWithPath(path);
return file;
}
-function abspath (rel, file) {
+function abspath(rel, file) {
var relSplit = rel.split('/');
+
if (relSplit[0] == '..' && !file.isDirectory()) {
file = file.parent;
}
- for each(p in relSplit) {
+
+ for each(var p in relSplit) {
if (p == '..') {
file = file.parent;
- } else if (p == '.'){
+ } else if (p == '.') {
if (!file.isDirectory()) {
file = file.parent;
}
@@ -40,14 +48,10 @@ function abspath (rel, file) {
file.append(p);
}
}
+
return file.path;
}
-function getPlatform () {
- var xulRuntime = Components.classes["@mozilla.org/xre/app-info;1"]
- .getService(Components.interfaces.nsIXULRuntime);
- mPlatform = xulRuntime.OS.toLowerCase();
- return mPlatform;
+function getPlatform() {
+ return Services.appinfo.OS.toLowerCase();
}
-
-
diff --git a/services/sync/tps/extensions/mozmill/resource/stdlib/securable-module.js b/services/sync/tps/extensions/mozmill/resource/stdlib/securable-module.js
index 7a7d8af14..794c3e2c2 100644
--- a/services/sync/tps/extensions/mozmill/resource/stdlib/securable-module.js
+++ b/services/sync/tps/extensions/mozmill/resource/stdlib/securable-module.js
@@ -1,6 +1,38 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Jetpack.
+ *
+ * The Initial Developer of the Original Code is Mozilla.
+ * Portions created by the Initial Developer are Copyright (C) 2007
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ * Atul Varma <atul@mozilla.com>
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
(function(global) {
const Cc = Components.classes;
diff --git a/services/sync/tps/extensions/mozmill/resource/stdlib/strings.js b/services/sync/tps/extensions/mozmill/resource/stdlib/strings.js
index d702dd0a0..24a93d958 100644
--- a/services/sync/tps/extensions/mozmill/resource/stdlib/strings.js
+++ b/services/sync/tps/extensions/mozmill/resource/stdlib/strings.js
@@ -1,6 +1,6 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+ * file, you can obtain one at http://mozilla.org/MPL/2.0/. */
var EXPORTED_SYMBOLS = ['trim', 'vslice'];
diff --git a/services/sync/tps/extensions/mozmill/resource/stdlib/utils.js b/services/sync/tps/extensions/mozmill/resource/stdlib/utils.js
new file mode 100644
index 000000000..4bb3124ee
--- /dev/null
+++ b/services/sync/tps/extensions/mozmill/resource/stdlib/utils.js
@@ -0,0 +1,462 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, you can obtain one at http://mozilla.org/MPL/2.0/. */
+
+var EXPORTED_SYMBOLS = ["applicationName", "assert", "Copy", "getBrowserObject",
+ "getChromeWindow", "getWindows", "getWindowByTitle",
+ "getWindowByType", "getWindowId", "getMethodInWindows",
+ "getPreference", "saveDataURL", "setPreference",
+ "sleep", "startTimer", "stopTimer", "takeScreenshot",
+ "unwrapNode", "waitFor"
+ ];
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cu = Components.utils;
+
+
+Cu.import("resource://gre/modules/NetUtil.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
+const applicationIdMap = {
+ '{8de7fcbb-c55c-4fbe-bfc5-fc555c87dbc4}': 'Firefox',
+ '{99bceaaa-e3c6-48c1-b981-ef9b46b67d60}': 'MetroFirefox'
+}
+const applicationName = applicationIdMap[Services.appinfo.ID] || Services.appinfo.name;
+
+var assertions = {}; Cu.import('resource://mozmill/modules/assertions.js', assertions);
+var broker = {}; Cu.import('resource://mozmill/driver/msgbroker.js', broker);
+var errors = {}; Cu.import('resource://mozmill/modules/errors.js', errors);
+
+var assert = new assertions.Assert();
+
+var hwindow = Services.appShell.hiddenDOMWindow;
+
+var uuidgen = Cc["@mozilla.org/uuid-generator;1"].getService(Ci.nsIUUIDGenerator);
+
+function Copy (obj) {
+ for (var n in obj) {
+ this[n] = obj[n];
+ }
+}
+
+/**
+ * Returns the browser object of the specified window
+ *
+ * @param {Window} aWindow
+ * Window to get the browser element from.
+ *
+ * @returns {Object} The browser element
+ */
+function getBrowserObject(aWindow) {
+ switch(applicationName) {
+ case "MetroFirefox":
+ return aWindow.Browser;
+ case "Firefox":
+ default:
+ return aWindow.gBrowser;
+ }
+}
+
+function getChromeWindow(aWindow) {
+ var chromeWin = aWindow.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebNavigation)
+ .QueryInterface(Ci.nsIDocShellTreeItem)
+ .rootTreeItem
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindow)
+ .QueryInterface(Ci.nsIDOMChromeWindow);
+
+ return chromeWin;
+}
+
+function getWindows(type) {
+ if (type == undefined) {
+ type = "";
+ }
+
+ var windows = [];
+ var enumerator = Services.wm.getEnumerator(type);
+
+ while (enumerator.hasMoreElements()) {
+ windows.push(enumerator.getNext());
+ }
+
+ if (type == "") {
+ windows.push(hwindow);
+ }
+
+ return windows;
+}
+
+function getMethodInWindows(methodName) {
+ for each (var w in getWindows()) {
+ if (w[methodName] != undefined) {
+ return w[methodName];
+ }
+ }
+
+ throw new Error("Method with name: '" + methodName + "' is not in any open window.");
+}
+
+function getWindowByTitle(title) {
+ for each (var w in getWindows()) {
+ if (w.document.title && w.document.title == title) {
+ return w;
+ }
+ }
+
+ throw new Error("Window with title: '" + title + "' not found.");
+}
+
+function getWindowByType(type) {
+ return Services.wm.getMostRecentWindow(type);
+}
+
+/**
+ * Retrieve the outer window id for the given window.
+ *
+ * @param {Number} aWindow
+ * Window to retrieve the id from.
+ * @returns {Boolean} The outer window id
+ **/
+function getWindowId(aWindow) {
+ try {
+ // Normally we can retrieve the id via window utils
+ return aWindow.QueryInterface(Ci.nsIInterfaceRequestor).
+ getInterface(Ci.nsIDOMWindowUtils).
+ outerWindowID;
+ } catch (e) {
+ // ... but for observer notifications we need another interface
+ return aWindow.QueryInterface(Ci.nsISupportsPRUint64).data;
+ }
+}
+
+var checkChrome = function () {
+ var loc = window.document.location.href;
+ try {
+ loc = window.top.document.location.href;
+ } catch (e) {
+ }
+
+ return /^chrome:\/\//.test(loc);
+}
+
+/**
+ * Called to get the state of an individual preference.
+ *
+ * @param aPrefName string The preference to get the state of.
+ * @param aDefaultValue any The default value if preference was not found.
+ *
+ * @returns any The value of the requested preference
+ *
+ * @see setPref
+ * Code by Henrik Skupin: <hskupin@gmail.com>
+ */
+function getPreference(aPrefName, aDefaultValue) {
+ try {
+ var branch = Services.prefs;
+
+ switch (typeof aDefaultValue) {
+ case ('boolean'):
+ return branch.getBoolPref(aPrefName);
+ case ('string'):
+ return branch.getCharPref(aPrefName);
+ case ('number'):
+ return branch.getIntPref(aPrefName);
+ default:
+ return branch.getComplexValue(aPrefName);
+ }
+ } catch (e) {
+ return aDefaultValue;
+ }
+}
+
+/**
+ * Called to set the state of an individual preference.
+ *
+ * @param aPrefName string The preference to set the state of.
+ * @param aValue any The value to set the preference to.
+ *
+ * @returns boolean Returns true if value was successfully set.
+ *
+ * @see getPref
+ * Code by Henrik Skupin: <hskupin@gmail.com>
+ */
+function setPreference(aName, aValue) {
+ try {
+ var branch = Services.prefs;
+
+ switch (typeof aValue) {
+ case ('boolean'):
+ branch.setBoolPref(aName, aValue);
+ break;
+ case ('string'):
+ branch.setCharPref(aName, aValue);
+ break;
+ case ('number'):
+ branch.setIntPref(aName, aValue);
+ break;
+ default:
+ branch.setComplexValue(aName, aValue);
+ }
+ } catch (e) {
+ return false;
+ }
+
+ return true;
+}
+
+/**
+ * Sleep for the given amount of milliseconds
+ *
+ * @param {number} milliseconds
+ * Sleeps the given number of milliseconds
+ */
+function sleep(milliseconds) {
+ var timeup = false;
+
+ hwindow.setTimeout(function () { timeup = true; }, milliseconds);
+ var thread = Services.tm.currentThread;
+
+ while (!timeup) {
+ thread.processNextEvent(true);
+ }
+
+ broker.pass({'function':'utils.sleep()'});
+}
+
+/**
+ * Check if the callback function evaluates to true
+ */
+function assert(callback, message, thisObject) {
+ var result = callback.call(thisObject);
+
+ if (!result) {
+ throw new Error(message || arguments.callee.name + ": Failed for '" + callback + "'");
+ }
+
+ return true;
+}
+
+/**
+ * Unwraps a node which is wrapped into a XPCNativeWrapper or XrayWrapper
+ *
+ * @param {DOMnode} Wrapped DOM node
+ * @returns {DOMNode} Unwrapped DOM node
+ */
+function unwrapNode(aNode) {
+ var node = aNode;
+ if (node) {
+ // unwrap is not available on older branches (3.5 and 3.6) - Bug 533596
+ if ("unwrap" in XPCNativeWrapper) {
+ node = XPCNativeWrapper.unwrap(node);
+ }
+ else if (node.wrappedJSObject != null) {
+ node = node.wrappedJSObject;
+ }
+ }
+
+ return node;
+}
+
+/**
+ * Waits for the callback evaluates to true
+ */
+function waitFor(callback, message, timeout, interval, thisObject) {
+ broker.log({'function': 'utils.waitFor() - DEPRECATED',
+ 'message': 'utils.waitFor() is deprecated. Use assert.waitFor() instead'});
+ assert.waitFor(callback, message, timeout, interval, thisObject);
+}
+
+/**
+ * Calculates the x and y chrome offset for an element
+ * See https://developer.mozilla.org/en/DOM/window.innerHeight
+ *
+ * Note this function will not work if the user has custom toolbars (via extension) at the bottom or left/right of the screen
+ */
+function getChromeOffset(elem) {
+ var win = elem.ownerDocument.defaultView;
+ // Calculate x offset
+ var chromeWidth = 0;
+
+ if (win["name"] != "sidebar") {
+ chromeWidth = win.outerWidth - win.innerWidth;
+ }
+
+ // Calculate y offset
+ var chromeHeight = win.outerHeight - win.innerHeight;
+ // chromeHeight == 0 means elem is already in the chrome and doesn't need the addonbar offset
+ if (chromeHeight > 0) {
+ // window.innerHeight doesn't include the addon or find bar, so account for these if present
+ var addonbar = win.document.getElementById("addon-bar");
+ if (addonbar) {
+ chromeHeight -= addonbar.scrollHeight;
+ }
+
+ var findbar = win.document.getElementById("FindToolbar");
+ if (findbar) {
+ chromeHeight -= findbar.scrollHeight;
+ }
+ }
+
+ return {'x':chromeWidth, 'y':chromeHeight};
+}
+
+/**
+ * Takes a screenshot of the specified DOM node
+ */
+function takeScreenshot(node, highlights) {
+ var rect, win, width, height, left, top, needsOffset;
+ // node can be either a window or an arbitrary DOM node
+ try {
+ // node is an arbitrary DOM node
+ win = node.ownerDocument.defaultView;
+ rect = node.getBoundingClientRect();
+ width = rect.width;
+ height = rect.height;
+ top = rect.top;
+ left = rect.left;
+ // offset for highlights not needed as they will be relative to this node
+ needsOffset = false;
+ } catch (e) {
+ // node is a window
+ win = node;
+ width = win.innerWidth;
+ height = win.innerHeight;
+ top = 0;
+ left = 0;
+ // offset needed for highlights to take 'outerHeight' of window into account
+ needsOffset = true;
+ }
+
+ var canvas = win.document.createElementNS("http://www.w3.org/1999/xhtml", "canvas");
+ canvas.width = width;
+ canvas.height = height;
+
+ var ctx = canvas.getContext("2d");
+ // Draws the DOM contents of the window to the canvas
+ ctx.drawWindow(win, left, top, width, height, "rgb(255,255,255)");
+
+ // This section is for drawing a red rectangle around each element passed in via the highlights array
+ if (highlights) {
+ ctx.lineWidth = "2";
+ ctx.strokeStyle = "red";
+ ctx.save();
+
+ for (var i = 0; i < highlights.length; ++i) {
+ var elem = highlights[i];
+ rect = elem.getBoundingClientRect();
+
+ var offsetY = 0, offsetX = 0;
+ if (needsOffset) {
+ var offset = getChromeOffset(elem);
+ offsetX = offset.x;
+ offsetY = offset.y;
+ } else {
+ // Don't need to offset the window chrome, just make relative to containing node
+ offsetY = -top;
+ offsetX = -left;
+ }
+
+ // Draw the rectangle
+ ctx.strokeRect(rect.left + offsetX, rect.top + offsetY, rect.width, rect.height);
+ }
+ }
+
+ return canvas.toDataURL("image/jpeg", 0.5);
+}
+
+/**
+ * Save the dataURL content to the specified file. It will be stored in either the persisted screenshot or temporary folder.
+ *
+ * @param {String} aDataURL
+ * The dataURL to save
+ * @param {String} aFilename
+ * Target file name without extension
+ *
+ * @returns {Object} The hash containing the path of saved file, and the failure bit
+ */
+function saveDataURL(aDataURL, aFilename) {
+ var frame = {}; Cu.import('resource://mozmill/modules/frame.js', frame);
+ const FILE_PERMISSIONS = parseInt("0644", 8);
+
+ var file;
+ file = Cc['@mozilla.org/file/local;1']
+ .createInstance(Ci.nsILocalFile);
+ file.initWithPath(frame.persisted['screenshots']['path']);
+ file.append(aFilename + ".jpg");
+ file.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, FILE_PERMISSIONS);
+
+ // Create an output stream to write to file
+ let foStream = Cc["@mozilla.org/network/file-output-stream;1"]
+ .createInstance(Ci.nsIFileOutputStream);
+ foStream.init(file, 0x02 | 0x08 | 0x10, FILE_PERMISSIONS, foStream.DEFER_OPEN);
+
+ let dataURI = NetUtil.newURI(aDataURL, "UTF8", null);
+ if (!dataURI.schemeIs("data")) {
+ throw TypeError("aDataURL parameter has to have 'data'" +
+ " scheme instead of '" + dataURI.scheme + "'");
+ }
+
+ // Write asynchronously to buffer;
+ // Input and output streams are closed after write
+
+ let ready = false;
+ let failure = false;
+
+ function sync(aStatus) {
+ if (!Components.isSuccessCode(aStatus)) {
+ failure = true;
+ }
+ ready = true;
+ }
+
+ NetUtil.asyncFetch(dataURI, function (aInputStream, aAsyncFetchResult) {
+ if (!Components.isSuccessCode(aAsyncFetchResult)) {
+ // An error occurred!
+ sync(aAsyncFetchResult);
+ } else {
+ // Consume the input stream.
+ NetUtil.asyncCopy(aInputStream, foStream, function (aAsyncCopyResult) {
+ sync(aAsyncCopyResult);
+ });
+ }
+ });
+
+ assert.waitFor(function () {
+ return ready;
+ }, "DataURL has been saved to '" + file.path + "'");
+
+ return {filename: file.path, failure: failure};
+}
+
+/**
+ * Some very brain-dead timer functions useful for performance optimizations
+ * This is only enabled in debug mode
+ *
+ **/
+var gutility_mzmltimer = 0;
+/**
+ * Starts timer initializing with current EPOC time in milliseconds
+ *
+ * @returns none
+ **/
+function startTimer(){
+ dump("TIMERCHECK:: starting now: " + Date.now() + "\n");
+ gutility_mzmltimer = Date.now();
+}
+
+/**
+ * Checks the timer and outputs current elapsed time since start of timer. It
+ * will print out a message you provide with its "time check" so you can
+ * correlate in the log file and figure out elapsed time of specific functions.
+ *
+ * @param aMsg string The debug message to print with the timer check
+ *
+ * @returns none
+ **/
+function checkTimer(aMsg){
+ var end = Date.now();
+ dump("TIMERCHECK:: at " + aMsg + " is: " + (end - gutility_mzmltimer) + "\n");
+}
diff --git a/services/sync/tps/extensions/tps/chrome.manifest b/services/sync/tps/extensions/tps/chrome.manifest
index 0731ba34a..4baf55677 100644
--- a/services/sync/tps/extensions/tps/chrome.manifest
+++ b/services/sync/tps/extensions/tps/chrome.manifest
@@ -1,4 +1,5 @@
-resource tps modules/
+resource tps resource/
+
component {4e5bd3f0-41d3-11df-9879-0800200c9a66} components/tps-cmdline.js
contract @mozilla.org/commandlinehandler/general-startup;1?type=tps {4e5bd3f0-41d3-11df-9879-0800200c9a66}
category command-line-handler m-tps @mozilla.org/commandlinehandler/general-startup;1?type=tps
diff --git a/services/sync/tps/extensions/tps/components/tps-cmdline.js b/services/sync/tps/extensions/tps/components/tps-cmdline.js
index 66622d6e1..aaa9870ba 100644
--- a/services/sync/tps/extensions/tps/components/tps-cmdline.js
+++ b/services/sync/tps/extensions/tps/components/tps-cmdline.js
@@ -22,8 +22,8 @@ const nsIWindowWatcher = Components.interfaces.nsIWindowWatcher;
Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
function TPSCmdLineHandler() {}
-TPSCmdLineHandler.prototype =
-{
+
+TPSCmdLineHandler.prototype = {
classDescription: "TPSCmdLineHandler",
classID : TPS_CMDLINE_CLSID,
contractID : TPS_CMDLINE_CONTRACTID,
@@ -49,7 +49,7 @@ TPSCmdLineHandler.prototype =
return;
let phase = cmdLine.handleFlagWithParam("tpsphase", false);
if (phase == null)
- throw("must specify --tpsphase with --tps");
+ throw Error("must specify --tpsphase with --tps");
let logfile = cmdLine.handleFlagWithParam("tpslogfile", false);
if (logfile == null)
logfile = "";
@@ -72,19 +72,17 @@ TPSCmdLineHandler.prototype =
//cmdLine.preventDefault = true;
},
- helpInfo : " -tps <file> Run TPS tests with the given test file.\n" +
- " -tpsphase <phase> Run the specified phase in the TPS test.\n" +
- " -tpslogfile <file> Logfile for TPS output.\n" +
+ helpInfo : " --tps <file> Run TPS tests with the given test file.\n" +
+ " --tpsphase <phase> Run the specified phase in the TPS test.\n" +
+ " --tpslogfile <file> Logfile for TPS output.\n" +
" --ignore-unused-engines Don't load engines not used in tests.\n",
};
-var TPSCmdLineFactory =
-{
- createInstance : function(outer, iid)
- {
+var TPSCmdLineFactory = {
+ createInstance : function(outer, iid) {
if (outer != null) {
- throw Components.results.NS_ERROR_NO_AGGREGATION;
+ throw new Error(Components.results.NS_ERROR_NO_AGGREGATION);
}
return new TPSCmdLineHandler().QueryInterface(iid);
@@ -92,10 +90,8 @@ var TPSCmdLineFactory =
};
-var TPSCmdLineModule =
-{
- registerSelf : function(compMgr, fileSpec, location, type)
- {
+var TPSCmdLineModule = {
+ registerSelf : function(compMgr, fileSpec, location, type) {
compMgr = compMgr.QueryInterface(nsIComponentRegistrar);
compMgr.registerFactoryLocation(TPS_CMDLINE_CLSID,
@@ -114,8 +110,7 @@ var TPSCmdLineModule =
TPS_CMDLINE_CONTRACTID, true, true);
},
- unregisterSelf : function(compMgr, fileSpec, location)
- {
+ unregisterSelf : function(compMgr, fileSpec, location) {
compMgr = compMgr.QueryInterface(nsIComponentRegistrar);
compMgr.unregisterFactoryLocation(TPS_CMDLINE_CLSID, fileSpec);
@@ -126,21 +121,19 @@ var TPSCmdLineModule =
"m-tps", true);
},
- getClassObject : function(compMgr, cid, iid)
- {
+ getClassObject : function(compMgr, cid, iid) {
if (cid.equals(TPS_CMDLINE_CLSID)) {
return TPSCmdLineFactory;
}
if (!iid.equals(Components.interfaces.nsIFactory)) {
- throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
+ throw new Error(Components.results.NS_ERROR_NOT_IMPLEMENTED);
}
- throw Components.results.NS_ERROR_NO_INTERFACE;
+ throw new Error(Components.results.NS_ERROR_NO_INTERFACE);
},
- canUnload : function(compMgr)
- {
+ canUnload : function(compMgr) {
return true;
}
};
diff --git a/services/sync/tps/extensions/tps/install.rdf b/services/sync/tps/extensions/tps/install.rdf
index bfa6091c5..cc9491b07 100644
--- a/services/sync/tps/extensions/tps/install.rdf
+++ b/services/sync/tps/extensions/tps/install.rdf
@@ -7,23 +7,14 @@
xmlns:em="http://www.mozilla.org/2004/em-rdf#">
<Description about="urn:mozilla:install-manifest">
<em:id>tps@mozilla.org</em:id>
- <em:version>0.2</em:version>
-
- <em:targetApplication>
- <!-- Pale Moon -->
- <Description>
- <em:id>{8de7fcbb-c55c-4fbe-bfc5-fc555c87dbc4}</em:id>
- <em:minVersion>3.5.0</em:minVersion>
- <em:maxVersion>24.0.*</em:maxVersion>
- </Description>
- </em:targetApplication>
+ <em:version>0.5</em:version>
<em:targetApplication>
<!-- Firefox -->
<Description>
- <em:id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</em:id>
- <em:minVersion>3.5.0</em:minVersion>
- <em:maxVersion>12.0.*</em:maxVersion>
+ <em:id>{8de7fcbb-c55c-4fbe-bfc5-fc555c87dbc4}</em:id>
+ <em:minVersion>24.0.*</em:minVersion>
+ <em:maxVersion>31.0.*</em:maxVersion>
</Description>
</em:targetApplication>
@@ -31,6 +22,7 @@
<em:name>TPS</em:name>
<em:description>Sync test extension</em:description>
<em:creator>Jonathan Griffin</em:creator>
- <em:homepageURL>http://www.mozilla.org/</em:homepageURL>
+ <em:contributor>Henrik Skupin</em:contributor>
+ <em:homepageURL>https://developer.mozilla.org/en-US/docs/TPS</em:homepageURL>
</Description>
</RDF>
diff --git a/services/sync/tps/extensions/tps/modules/sync.jsm b/services/sync/tps/extensions/tps/modules/sync.jsm
deleted file mode 100644
index 56c752a8b..000000000
--- a/services/sync/tps/extensions/tps/modules/sync.jsm
+++ /dev/null
@@ -1,115 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-
-var EXPORTED_SYMBOLS = ["TPS", "SYNC_WIPE_SERVER", "SYNC_RESET_CLIENT",
- "SYNC_WIPE_CLIENT"];
-
-const CC = Components.classes;
-const CI = Components.interfaces;
-const CU = Components.utils;
-
-CU.import("resource://gre/modules/XPCOMUtils.jsm");
-CU.import("resource://gre/modules/Services.jsm");
-CU.import("resource://services-sync/util.js");
-CU.import("resource://tps/logger.jsm");
-var utils = {}; CU.import('resource://mozmill/modules/utils.js', utils);
-
-const SYNC_RESET_CLIENT = "reset-client";
-const SYNC_WIPE_CLIENT = "wipe-client";
-const SYNC_WIPE_REMOTE = "wipe-remote";
-const SYNC_WIPE_SERVER = "wipe-server";
-
-var prefs = CC["@mozilla.org/preferences-service;1"]
- .getService(CI.nsIPrefBranch);
-
-var syncFinishedCallback = function() {
- Logger.logInfo('syncFinishedCallback returned ' + !TPS._waitingForSync);
- return !TPS._waitingForSync;
-};
-
-var TPS = {
- _waitingForSync: false,
- _syncErrors: 0,
-
- QueryInterface: XPCOMUtils.generateQI([CI.nsIObserver,
- CI.nsISupportsWeakReference]),
-
- observe: function TPS__observe(subject, topic, data) {
- Logger.logInfo('Mozmill observed: ' + topic);
- switch(topic) {
- case "weave:service:sync:error":
- if (this._waitingForSync && this._syncErrors == 0) {
- Logger.logInfo("sync error; retrying...");
- this._syncErrors++;
- Utils.namedTimer(function() {
- Weave.service.sync();
- }, 1000, this, "resync");
- }
- else if (this._waitingForSync) {
- this._syncErrors = "sync error, see log";
- this._waitingForSync = false;
- }
- break;
- case "weave:service:sync:finish":
- if (this._waitingForSync) {
- this._syncErrors = 0;
- this._waitingForSync = false;
- }
- break;
- }
- },
-
- SetupSyncAccount: function TPS__SetupSyncAccount() {
- try {
- let serverURL = prefs.getCharPref('tps.account.serverURL');
- if (serverURL) {
- Weave.Service.serverURL = serverURL;
- }
- }
- catch(e) {}
- Weave.Service.identity.account = prefs.getCharPref('tps.account.username');
- Weave.Service.Identity.basicPassword = prefs.getCharPref('tps.account.password');
- Weave.Service.identity.syncKey = prefs.getCharPref('tps.account.passphrase');
- Weave.Svc.Obs.notify("weave:service:setup-complete");
- },
-
- Sync: function TPS__Sync(options) {
- Logger.logInfo('Mozmill starting sync operation: ' + options);
- switch(options) {
- case SYNC_WIPE_REMOTE:
- Weave.Svc.Prefs.set("firstSync", "wipeRemote");
- break;
- case SYNC_WIPE_CLIENT:
- Weave.Svc.Prefs.set("firstSync", "wipeClient");
- break;
- case SYNC_RESET_CLIENT:
- Weave.Svc.Prefs.set("firstSync", "resetClient");
- break;
- default:
- Weave.Svc.Prefs.reset("firstSync");
- }
-
- if (Weave.Status.service != Weave.STATUS_OK) {
- return "Sync status not ok: " + Weave.Status.service;
- }
-
- this._syncErrors = 0;
-
- if (options == SYNC_WIPE_SERVER) {
- Weave.Service.wipeServer();
- } else {
- this._waitingForSync = true;
- Weave.Service.sync();
- utils.waitFor(syncFinishedCallback, null, 20000, 500, TPS);
- }
- return this._syncErrors;
- },
-};
-
-Services.obs.addObserver(TPS, "weave:service:sync:finish", true);
-Services.obs.addObserver(TPS, "weave:service:sync:error", true);
-Logger.init();
-
-
diff --git a/services/sync/tps/extensions/tps/resource/auth/fxaccounts.jsm b/services/sync/tps/extensions/tps/resource/auth/fxaccounts.jsm
new file mode 100644
index 000000000..f5daa14be
--- /dev/null
+++ b/services/sync/tps/extensions/tps/resource/auth/fxaccounts.jsm
@@ -0,0 +1,96 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+this.EXPORTED_SYMBOLS = [
+ "Authentication",
+];
+
+const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
+
+Cu.import("resource://gre/modules/FxAccounts.jsm");
+Cu.import("resource://gre/modules/FxAccountsClient.jsm");
+Cu.import("resource://services-common/async.js");
+Cu.import("resource://services-sync/main.js");
+Cu.import("resource://tps/logger.jsm");
+
+
+/**
+ * Helper object for Firefox Accounts authentication
+ */
+var Authentication = {
+
+ /**
+ * Check if an user has been logged in
+ */
+ get isLoggedIn() {
+ return !!this.getSignedInUser();
+ },
+
+ /**
+ * Wrapper to retrieve the currently signed in user
+ *
+ * @returns Information about the currently signed in user
+ */
+ getSignedInUser: function getSignedInUser() {
+ let cb = Async.makeSpinningCallback();
+
+ fxAccounts.getSignedInUser().then(user => {
+ cb(null, user);
+ }, error => {
+ cb(error);
+ })
+
+ try {
+ return cb.wait();
+ } catch (error) {
+ Logger.logError("getSignedInUser() failed with: " + JSON.stringify(error));
+ throw error;
+ }
+ },
+
+ /**
+ * Wrapper to synchronize the login of a user
+ *
+ * @param account
+ * Account information of the user to login
+ * @param account.username
+ * The username for the account (utf8)
+ * @param account.password
+ * The user's password
+ */
+ signIn: function signIn(account) {
+ let cb = Async.makeSpinningCallback();
+
+ Logger.AssertTrue(account["username"], "Username has been found");
+ Logger.AssertTrue(account["password"], "Password has been found");
+
+ Logger.logInfo("Login user: " + account["username"] + '\n');
+
+ let client = new FxAccountsClient();
+ client.signIn(account["username"], account["password"], true).then(credentials => {
+ return fxAccounts.setSignedInUser(credentials);
+ }).then(() => {
+ cb(null, true);
+ }, error => {
+ cb(error, false);
+ });
+
+ try {
+ cb.wait();
+
+ if (Weave.Status.login !== Weave.LOGIN_SUCCEEDED) {
+ Logger.logInfo("Logging into Weave.");
+ Weave.Service.login();
+ Logger.AssertEqual(Weave.Status.login, Weave.LOGIN_SUCCEEDED,
+ "Weave logged in");
+ }
+
+ return true;
+ } catch (error) {
+ throw new Error("signIn() failed with: " + error.message);
+ }
+ }
+};
diff --git a/services/sync/tps/extensions/tps/resource/auth/sync.jsm b/services/sync/tps/extensions/tps/resource/auth/sync.jsm
new file mode 100644
index 000000000..676b17a91
--- /dev/null
+++ b/services/sync/tps/extensions/tps/resource/auth/sync.jsm
@@ -0,0 +1,84 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+this.EXPORTED_SYMBOLS = [
+ "Authentication",
+];
+
+const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
+
+Cu.import("resource://services-sync/main.js");
+Cu.import("resource://tps/logger.jsm");
+
+
+/**
+ * Helper object for deprecated Firefox Sync authentication
+ */
+var Authentication = {
+
+ /**
+ * Check if an user has been logged in
+ */
+ get isLoggedIn() {
+ return !!this.getSignedInUser();
+ },
+
+ /**
+ * Wrapper to retrieve the currently signed in user
+ *
+ * @returns Information about the currently signed in user
+ */
+ getSignedInUser: function getSignedInUser() {
+ let user = null;
+
+ if (Weave.Service.isLoggedIn) {
+ user = {
+ email: Weave.Service.identity.account,
+ password: Weave.Service.identity.basicPassword,
+ passphrase: Weave.Service.identity.syncKey
+ };
+ }
+
+ return user;
+ },
+
+ /**
+ * Wrapper to synchronize the login of a user
+ *
+ * @param account
+ * Account information of the user to login
+ * @param account.username
+ * The username for the account (utf8)
+ * @param account.password
+ * The user's password
+ * @param account.passphrase
+ * The users's passphrase
+ */
+ signIn: function signIn(account) {
+ Logger.AssertTrue(account["username"], "Username has been found");
+ Logger.AssertTrue(account["password"], "Password has been found");
+ Logger.AssertTrue(account["passphrase"], "Passphrase has been found");
+
+ Logger.logInfo("Logging in user: " + account["username"]);
+
+ Weave.Service.identity.account = account["username"];
+ Weave.Service.identity.basicPassword = account["password"];
+ Weave.Service.identity.syncKey = account["passphrase"];
+
+ if (Weave.Status.login !== Weave.LOGIN_SUCCEEDED) {
+ Logger.logInfo("Logging into Weave.");
+ Weave.Service.login();
+ Logger.AssertEqual(Weave.Status.login, Weave.LOGIN_SUCCEEDED,
+ "Weave logged in");
+
+ // Bug 997279: Temporary workaround until we can ensure that Sync itself
+ // sends this notification for the first login attempt by TPS
+ Weave.Svc.Obs.notify("weave:service:setup-complete");
+ }
+
+ return true;
+ }
+};
diff --git a/services/sync/tps/extensions/tps/modules/logger.jsm b/services/sync/tps/extensions/tps/resource/logger.jsm
index 8b46f2033..f4dd4bfb0 100644
--- a/services/sync/tps/extensions/tps/modules/logger.jsm
+++ b/services/sync/tps/extensions/tps/resource/logger.jsm
@@ -9,12 +9,9 @@
var EXPORTED_SYMBOLS = ["Logger"];
-const CC = Components.classes;
-const CI = Components.interfaces;
-const CU = Components.utils;
+const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
-var Logger =
-{
+var Logger = {
_foStream: null,
_converter: null,
_potentialError: null,
@@ -25,8 +22,8 @@ var Logger =
return;
}
- let prefs = CC["@mozilla.org/preferences-service;1"]
- .getService(CI.nsIPrefBranch);
+ let prefs = Cc["@mozilla.org/preferences-service;1"]
+ .getService(Ci.nsIPrefBranch);
if (path) {
prefs.setCharPref("tps.logfile", path);
}
@@ -34,26 +31,26 @@ var Logger =
path = prefs.getCharPref("tps.logfile");
}
- this._file = CC["@mozilla.org/file/local;1"]
- .createInstance(CI.nsILocalFile);
+ this._file = Cc["@mozilla.org/file/local;1"]
+ .createInstance(Ci.nsILocalFile);
this._file.initWithPath(path);
var exists = this._file.exists();
// Make a file output stream and converter to handle it.
- this._foStream = CC["@mozilla.org/network/file-output-stream;1"]
- .createInstance(CI.nsIFileOutputStream);
+ this._foStream = Cc["@mozilla.org/network/file-output-stream;1"]
+ .createInstance(Ci.nsIFileOutputStream);
// If the file already exists, append it, otherwise create it.
var fileflags = exists ? 0x02 | 0x08 | 0x10 : 0x02 | 0x08 | 0x20;
this._foStream.init(this._file, fileflags, 0666, 0);
- this._converter = CC["@mozilla.org/intl/converter-output-stream;1"]
- .createInstance(CI.nsIConverterOutputStream);
+ this._converter = Cc["@mozilla.org/intl/converter-output-stream;1"]
+ .createInstance(Ci.nsIConverterOutputStream);
this._converter.init(this._foStream, "UTF-8", 0, 0);
},
write: function (data) {
if (this._converter == null) {
- CU.reportError(
+ Cu.reportError(
"TPS Logger.write called with _converter == null!");
return;
}
@@ -77,7 +74,7 @@ var Logger =
msg += "; " + this._potentialError;
this._potentialError = null;
}
- throw("ASSERTION FAILED! " + msg);
+ throw new Error("ASSERTION FAILED! " + msg);
},
AssertFalse: function(bool, msg, showPotentialError) {
@@ -86,7 +83,7 @@ var Logger =
AssertEqual: function(val1, val2, msg) {
if (val1 != val2)
- throw("ASSERTION FAILED! " + msg + "; expected " +
+ throw new Error("ASSERTION FAILED! " + msg + "; expected " +
JSON.stringify(val2) + ", got " + JSON.stringify(val1));
},
diff --git a/services/sync/tps/extensions/tps/modules/addons.jsm b/services/sync/tps/extensions/tps/resource/modules/addons.jsm
index 69cc43c17..5c308b5c2 100644
--- a/services/sync/tps/extensions/tps/modules/addons.jsm
+++ b/services/sync/tps/extensions/tps/resource/modules/addons.jsm
@@ -8,20 +8,26 @@ let EXPORTED_SYMBOLS = ["Addon", "STATE_ENABLED", "STATE_DISABLED"];
const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
Cu.import("resource://gre/modules/AddonManager.jsm");
-Cu.import("resource://gre/modules/AddonRepository.jsm");
+Cu.import("resource://gre/modules/addons/AddonRepository.jsm");
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://services-common/async.js");
Cu.import("resource://services-sync/addonutils.js");
Cu.import("resource://services-sync/util.js");
Cu.import("resource://tps/logger.jsm");
-const ADDONSGETURL = 'http://127.0.0.1:4567/';
+const ADDONSGETURL = "http://127.0.0.1:4567/";
const STATE_ENABLED = 1;
const STATE_DISABLED = 2;
-function GetFileAsText(file)
-{
- let channel = Services.io.newChannel(file, null, null);
+function GetFileAsText(file) {
+ let channel = Services.io.newChannel2(file,
+ null,
+ null,
+ null, // aLoadingNode
+ Services.scriptSecurityManager.getSystemPrincipal(),
+ null, // aTriggeringPrincipal
+ Ci.nsILoadInfo.SEC_NORMAL,
+ Ci.nsIContentPolicy.TYPE_OTHER);
let inputStream = channel.open();
if (channel instanceof Ci.nsIHttpChannel &&
channel.responseStatus != 200) {
@@ -84,7 +90,7 @@ Addon.prototype = {
Logger.AssertTrue(addon.userDisabled, "add-on is enabled: " + addon.id);
return true;
} else if (state) {
- throw Error("Don't know how to handle state: " + state);
+ throw new Error("Don't know how to handle state: " + state);
} else {
// No state, so just checking that it exists.
return true;
diff --git a/services/sync/tps/extensions/tps/modules/bookmarks.jsm b/services/sync/tps/extensions/tps/resource/modules/bookmarks.jsm
index 3dc832846..6a288bbec 100644
--- a/services/sync/tps/extensions/tps/modules/bookmarks.jsm
+++ b/services/sync/tps/extensions/tps/resource/modules/bookmarks.jsm
@@ -10,37 +10,22 @@
var EXPORTED_SYMBOLS = ["PlacesItem", "Bookmark", "Separator", "Livemark",
"BookmarkFolder", "DumpBookmarks"];
-const CC = Components.classes;
-const CI = Components.interfaces;
-const CU = Components.utils;
+const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
-CU.import("resource://tps/logger.jsm");
-CU.import("resource://gre/modules/Services.jsm");
-CU.import("resource://gre/modules/PlacesUtils.jsm");
-CU.import("resource://gre/modules/BookmarkJSONUtils.jsm");
-CU.import("resource://gre/modules/Task.jsm");
-CU.import("resource://services-common/async.js");
+Cu.import("resource://gre/modules/PlacesBackups.jsm");
+Cu.import("resource://gre/modules/PlacesUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://services-common/async.js");
+Cu.import("resource://tps/logger.jsm");
var DumpBookmarks = function TPS_Bookmarks__DumpBookmarks() {
- let writer = {
- value: "",
- write: function PlacesItem__dump__write(aStr, aLen) {
- this.value += aStr;
- }
- };
-
- let options = PlacesUtils.history.getNewQueryOptions();
- options.queryType = options.QUERY_TYPE_BOOKMARKS;
- let query = PlacesUtils.history.getNewQuery();
- query.setFolders([PlacesUtils.placesRootId], 1);
- let root = PlacesUtils.history.executeQuery(query, options).root;
- root.containerOpen = true;
let cb = Async.makeSpinningCallback();
- Task.spawn(function() {
- yield BookmarkJSONUtils.serializeNodeAsJSONToOutputStream(root, writer, true, false);
- let value = JSON.parse(writer.value);
- Logger.logInfo("dumping bookmarks\n\n" + JSON.stringify(value, null, ' ') + "\n\n");
- cb();
+ PlacesBackups.getBookmarksTree().then(result => {
+ let [bookmarks, count] = result;
+ Logger.logInfo("Dumping Bookmarks...\n" + JSON.stringify(bookmarks) + "\n\n");
+ cb(null);
+ }).then(null, error => {
+ cb(error);
});
cb.wait();
};
@@ -223,7 +208,7 @@ PlacesItem.prototype = {
for (let i = 1; i < folder_parts.length; i++) {
let subfolder_id = this.GetPlacesNodeId(
folder_id,
- CI.nsINavHistoryResultNode.RESULT_TYPE_FOLDER,
+ Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER,
folder_parts[i]);
if (subfolder_id == -1) {
return -1;
@@ -253,7 +238,7 @@ PlacesItem.prototype = {
for (let i = 1; i < folder_parts.length; i++) {
let subfolder_id = this.GetPlacesNodeId(
folder_id,
- CI.nsINavHistoryResultNode.RESULT_TYPE_FOLDER,
+ Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER,
folder_parts[i]);
if (subfolder_id == -1) {
folder_id = PlacesUtils.bookmarks.createFolder(folder_id,
@@ -703,7 +688,7 @@ BookmarkFolder.prototype = {
}
this.props.item_id = this.GetPlacesNodeId(
this.props.folder_id,
- CI.nsINavHistoryResultNode.RESULT_TYPE_FOLDER,
+ Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER,
this.props.folder);
if (!this.CheckDescription(this.props.description))
return -1;
@@ -784,14 +769,14 @@ Livemark.prototype = {
// Until this can handle asynchronous creation, we need to spin.
let spinningCb = Async.makeSpinningCallback();
- PlacesUtils.livemarks.addLivemark(livemarkObj,
- function (aStatus, aLivemark) {
- spinningCb(null, [aStatus, aLivemark]);
- });
+ PlacesUtils.livemarks.addLivemark(livemarkObj).then(
+ aLivemark => { spinningCb(null, [Components.results.NS_OK, aLivemark]) },
+ () => { spinningCb(null, [Components.results.NS_ERROR_UNEXPECTED, aLivemark]) }
+ );
let [status, livemark] = spinningCb.wait();
if (!Components.isSuccessCode(status)) {
- throw status;
+ throw new Error(status);
}
this.props.item_id = livemark.id;
@@ -814,7 +799,7 @@ Livemark.prototype = {
}
this.props.item_id = this.GetPlacesNodeId(
this.props.folder_id,
- CI.nsINavHistoryResultNode.RESULT_TYPE_FOLDER,
+ Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER,
this.props.livemark);
if (!PlacesUtils.annotations
.itemHasAnnotation(this.props.item_id, PlacesUtils.LMANNO_FEEDURI)) {
diff --git a/services/sync/tps/extensions/tps/modules/forms.jsm b/services/sync/tps/extensions/tps/resource/modules/forms.jsm
index 99dbcb085..ece2e14f7 100644
--- a/services/sync/tps/extensions/tps/modules/forms.jsm
+++ b/services/sync/tps/extensions/tps/resource/modules/forms.jsm
@@ -9,14 +9,12 @@
var EXPORTED_SYMBOLS = ["FormData"];
-const CC = Components.classes;
-const CI = Components.interfaces;
-const CU = Components.utils;
+const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
-CU.import("resource://tps/logger.jsm");
+Cu.import("resource://tps/logger.jsm");
-let formService = CC["@mozilla.org/satchel/form-history;1"]
- .getService(CI.nsIFormHistory2);
+let formService = Cc["@mozilla.org/satchel/form-history;1"]
+ .getService(Ci.nsIFormHistory2);
/**
* FormDB
diff --git a/services/sync/tps/extensions/tps/modules/history.jsm b/services/sync/tps/extensions/tps/resource/modules/history.jsm
index f3a274cc7..ab0514bcc 100644
--- a/services/sync/tps/extensions/tps/modules/history.jsm
+++ b/services/sync/tps/extensions/tps/resource/modules/history.jsm
@@ -9,14 +9,12 @@
var EXPORTED_SYMBOLS = ["HistoryEntry", "DumpHistory"];
-const CC = Components.classes;
-const CI = Components.interfaces;
-const CU = Components.utils;
+const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
-CU.import("resource://gre/modules/Services.jsm");
-CU.import("resource://gre/modules/PlacesUtils.jsm");
-CU.import("resource://tps/logger.jsm");
-CU.import("resource://services-common/async.js");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/PlacesUtils.jsm");
+Cu.import("resource://tps/logger.jsm");
+Cu.import("resource://services-common/async.js");
var DumpHistory = function TPS_History__DumpHistory() {
let writer = {
@@ -55,7 +53,7 @@ var HistoryEntry = {
* Returns the DBConnection object for the history service.
*/
get _db() {
- return PlacesUtils.history.QueryInterface(CI.nsPIPlacesDatabase).DBConnection;
+ return PlacesUtils.history.QueryInterface(Ci.nsPIPlacesDatabase).DBConnection;
},
/**
@@ -74,7 +72,7 @@ var HistoryEntry = {
"FROM moz_places " +
"WHERE url = :url) " +
"ORDER BY date DESC LIMIT 10");
- this.__defineGetter__("_visitStm", function() stm);
+ this.__defineGetter__("_visitStm", () => stm);
return stm;
},
@@ -200,4 +198,3 @@ var HistoryEntry = {
}
},
};
-
diff --git a/services/sync/tps/extensions/tps/modules/passwords.jsm b/services/sync/tps/extensions/tps/resource/modules/passwords.jsm
index 3f8b24b39..f7221224a 100644
--- a/services/sync/tps/extensions/tps/modules/passwords.jsm
+++ b/services/sync/tps/extensions/tps/resource/modules/passwords.jsm
@@ -9,16 +9,14 @@
var EXPORTED_SYMBOLS = ["Password", "DumpPasswords"];
-const CC = Components.classes;
-const CI = Components.interfaces;
-const CU = Components.utils;
+const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
-CU.import("resource://gre/modules/Services.jsm");
-CU.import("resource://tps/logger.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://tps/logger.jsm");
let nsLoginInfo = new Components.Constructor(
"@mozilla.org/login-manager/loginInfo;1",
- CI.nsILoginInfo,
+ Ci.nsILoginInfo,
"init");
var DumpPasswords = function TPS__Passwords__DumpPasswords() {
@@ -87,7 +85,7 @@ Password.prototype = {
this.props.usernameField,
this.props.passwordField);
Services.logins.addLogin(login);
- login.QueryInterface(CI.nsILoginMetaInfo);
+ login.QueryInterface(Ci.nsILoginMetaInfo);
return login.guid;
},
@@ -109,7 +107,7 @@ Password.prototype = {
logins[i].password == this.props.password &&
logins[i].usernameField == this.props.usernameField &&
logins[i].passwordField == this.props.passwordField) {
- logins[i].QueryInterface(CI.nsILoginMetaInfo);
+ logins[i].QueryInterface(Ci.nsILoginMetaInfo);
return logins[i].guid;
}
}
diff --git a/services/sync/tps/extensions/tps/modules/prefs.jsm b/services/sync/tps/extensions/tps/resource/modules/prefs.jsm
index 8707f723f..18a6e32ee 100644
--- a/services/sync/tps/extensions/tps/modules/prefs.jsm
+++ b/services/sync/tps/extensions/tps/resource/modules/prefs.jsm
@@ -9,15 +9,14 @@
var EXPORTED_SYMBOLS = ["Preference"];
-const CC = Components.classes;
-const CI = Components.interfaces;
-const CU = Components.utils;
+const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
+
const WEAVE_PREF_PREFIX = "services.sync.prefs.sync.";
-let prefs = CC["@mozilla.org/preferences-service;1"]
- .getService(CI.nsIPrefBranch);
+let prefs = Cc["@mozilla.org/preferences-service;1"]
+ .getService(Ci.nsIPrefBranch);
-CU.import("resource://tps/logger.jsm");
+Cu.import("resource://tps/logger.jsm");
/**
* Preference class constructor
@@ -60,17 +59,17 @@ Preference.prototype = {
// than the value type specified in the test.
let prefType = prefs.getPrefType(this.name);
switch (prefType) {
- case CI.nsIPrefBranch.PREF_INT:
+ case Ci.nsIPrefBranch.PREF_INT:
Logger.AssertEqual(typeof(this.value), "number",
"Wrong type used for preference value");
prefs.setIntPref(this.name, this.value);
break;
- case CI.nsIPrefBranch.PREF_STRING:
+ case Ci.nsIPrefBranch.PREF_STRING:
Logger.AssertEqual(typeof(this.value), "string",
"Wrong type used for preference value");
prefs.setCharPref(this.name, this.value);
break;
- case CI.nsIPrefBranch.PREF_BOOL:
+ case Ci.nsIPrefBranch.PREF_BOOL:
Logger.AssertEqual(typeof(this.value), "boolean",
"Wrong type used for preference value");
prefs.setBoolPref(this.name, this.value);
@@ -93,13 +92,13 @@ Preference.prototype = {
try {
let prefType = prefs.getPrefType(this.name);
switch(prefType) {
- case CI.nsIPrefBranch.PREF_INT:
+ case Ci.nsIPrefBranch.PREF_INT:
value = prefs.getIntPref(this.name);
break;
- case CI.nsIPrefBranch.PREF_STRING:
+ case Ci.nsIPrefBranch.PREF_STRING:
value = prefs.getCharPref(this.name);
break;
- case CI.nsIPrefBranch.PREF_BOOL:
+ case Ci.nsIPrefBranch.PREF_BOOL:
value = prefs.getBoolPref(this.name);
break;
}
diff --git a/services/sync/tps/extensions/tps/modules/tabs.jsm b/services/sync/tps/extensions/tps/resource/modules/tabs.jsm
index a2ce1afc1..a2ce1afc1 100644
--- a/services/sync/tps/extensions/tps/modules/tabs.jsm
+++ b/services/sync/tps/extensions/tps/resource/modules/tabs.jsm
diff --git a/services/sync/tps/extensions/tps/modules/windows.jsm b/services/sync/tps/extensions/tps/resource/modules/windows.jsm
index 62cc80d2c..62cc80d2c 100644
--- a/services/sync/tps/extensions/tps/modules/windows.jsm
+++ b/services/sync/tps/extensions/tps/resource/modules/windows.jsm
diff --git a/services/sync/tps/extensions/tps/modules/quit.js b/services/sync/tps/extensions/tps/resource/quit.js
index ccaa05441..0ec5498b0 100644
--- a/services/sync/tps/extensions/tps/modules/quit.js
+++ b/services/sync/tps/extensions/tps/resource/quit.js
@@ -1,4 +1,4 @@
-/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; -*- */
+/* -*- indent-tabs-mode: nil -*- */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
@@ -11,30 +11,24 @@ var EXPORTED_SYMBOLS = ["goQuitApplication"];
Components.utils.import("resource://gre/modules/Services.jsm");
-function canQuitApplication()
-{
- try
- {
+function canQuitApplication() {
+ try {
var cancelQuit = Components.classes["@mozilla.org/supports-PRBool;1"]
- .createInstance(Components.interfaces.nsISupportsPRBool);
+ .createInstance(Components.interfaces.nsISupportsPRBool);
Services.obs.notifyObservers(cancelQuit, "quit-application-requested", null);
// Something aborted the quit process.
- if (cancelQuit.data)
- {
+ if (cancelQuit.data) {
return false;
}
}
- catch (ex)
- {
- }
+ catch (ex) {}
+
return true;
}
-function goQuitApplication()
-{
- if (!canQuitApplication())
- {
+function goQuitApplication() {
+ if (!canQuitApplication()) {
return false;
}
@@ -43,30 +37,25 @@ function goQuitApplication()
var appService;
var forceQuit;
- if (kAppStartup in Components.classes)
- {
- appService = Components.classes[kAppStartup].
- getService(Components.interfaces.nsIAppStartup);
+ if (kAppStartup in Components.classes) {
+ appService = Components.classes[kAppStartup]
+ .getService(Components.interfaces.nsIAppStartup);
forceQuit = Components.interfaces.nsIAppStartup.eForceQuit;
}
- else if (kAppShell in Components.classes)
- {
+ else if (kAppShell in Components.classes) {
appService = Components.classes[kAppShell].
getService(Components.interfaces.nsIAppShellService);
forceQuit = Components.interfaces.nsIAppShellService.eForceQuit;
}
- else
- {
- throw 'goQuitApplication: no AppStartup/appShell';
+ else {
+ throw new Error('goQuitApplication: no AppStartup/appShell');
}
- try
- {
+ try {
appService.quit(forceQuit);
}
- catch(ex)
- {
- throw('goQuitApplication: ' + ex);
+ catch(ex) {
+ throw new Error('goQuitApplication: ' + ex);
}
return true;
diff --git a/services/sync/tps/extensions/tps/modules/tps.jsm b/services/sync/tps/extensions/tps/resource/tps.jsm
index 8d753e44f..d3a8b0b7d 100644
--- a/services/sync/tps/extensions/tps/modules/tps.jsm
+++ b/services/sync/tps/extensions/tps/resource/tps.jsm
@@ -7,120 +7,219 @@
* listed symbols will exposed on import, and only when and where imported.
*/
-let EXPORTED_SYMBOLS = ["TPS"];
-
-const {classes: CC, interfaces: CI, utils: CU} = Components;
-
-CU.import("resource://gre/modules/XPCOMUtils.jsm");
-CU.import("resource://gre/modules/Services.jsm");
-CU.import("resource://services-common/async.js");
-CU.import("resource://services-sync/constants.js");
-CU.import("resource://services-sync/main.js");
-CU.import("resource://services-sync/util.js");
-CU.import("resource://tps/addons.jsm");
-CU.import("resource://tps/bookmarks.jsm");
-CU.import("resource://tps/logger.jsm");
-CU.import("resource://tps/passwords.jsm");
-CU.import("resource://tps/history.jsm");
-CU.import("resource://tps/forms.jsm");
-CU.import("resource://tps/prefs.jsm");
-CU.import("resource://tps/tabs.jsm");
-CU.import("resource://tps/windows.jsm");
-
-var hh = CC["@mozilla.org/network/protocol;1?name=http"]
- .getService(CI.nsIHttpProtocolHandler);
-var prefs = CC["@mozilla.org/preferences-service;1"]
- .getService(CI.nsIPrefBranch);
+let EXPORTED_SYMBOLS = ["ACTIONS", "TPS"];
+
+const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
+
+let module = this;
+
+// Global modules
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://services-common/async.js");
+Cu.import("resource://services-sync/constants.js");
+Cu.import("resource://services-sync/main.js");
+Cu.import("resource://services-sync/util.js");
+
+// TPS modules
+Cu.import("resource://tps/logger.jsm");
+
+// Module wrappers for tests
+Cu.import("resource://tps/modules/addons.jsm");
+Cu.import("resource://tps/modules/bookmarks.jsm");
+Cu.import("resource://tps/modules/forms.jsm");
+Cu.import("resource://tps/modules/history.jsm");
+Cu.import("resource://tps/modules/passwords.jsm");
+Cu.import("resource://tps/modules/prefs.jsm");
+Cu.import("resource://tps/modules/tabs.jsm");
+Cu.import("resource://tps/modules/windows.jsm");
+
+var hh = Cc["@mozilla.org/network/protocol;1?name=http"]
+ .getService(Ci.nsIHttpProtocolHandler);
+var prefs = Cc["@mozilla.org/preferences-service;1"]
+ .getService(Ci.nsIPrefBranch);
var mozmillInit = {};
-CU.import('resource://mozmill/modules/init.js', mozmillInit);
-
-const ACTION_ADD = "add";
-const ACTION_VERIFY = "verify";
-const ACTION_VERIFY_NOT = "verify-not";
-const ACTION_MODIFY = "modify";
-const ACTION_SYNC = "sync";
-const ACTION_DELETE = "delete";
-const ACTION_PRIVATE_BROWSING = "private-browsing";
-const ACTION_WIPE_REMOTE = "wipe-remote";
-const ACTION_WIPE_SERVER = "wipe-server";
-const ACTION_SET_ENABLED = "set-enabled";
-
-const ACTIONS = [ACTION_ADD, ACTION_VERIFY, ACTION_VERIFY_NOT,
- ACTION_MODIFY, ACTION_SYNC, ACTION_DELETE,
- ACTION_PRIVATE_BROWSING, ACTION_WIPE_REMOTE,
- ACTION_WIPE_SERVER, ACTION_SET_ENABLED];
-
-const SYNC_WIPE_CLIENT = "wipe-client";
-const SYNC_WIPE_REMOTE = "wipe-remote";
-const SYNC_WIPE_SERVER = "wipe-server";
-const SYNC_RESET_CLIENT = "reset-client";
-const SYNC_START_OVER = "start-over";
-
-const OBSERVER_TOPICS = ["weave:engine:start-tracking",
+Cu.import('resource://mozmill/driver/mozmill.js', mozmillInit);
+
+// Options for wiping data during a sync
+const SYNC_RESET_CLIENT = "resetClient";
+const SYNC_WIPE_CLIENT = "wipeClient";
+const SYNC_WIPE_REMOTE = "wipeRemote";
+
+// Actions a test can perform
+const ACTION_ADD = "add";
+const ACTION_DELETE = "delete";
+const ACTION_MODIFY = "modify";
+const ACTION_PRIVATE_BROWSING = "private-browsing";
+const ACTION_SET_ENABLED = "set-enabled";
+const ACTION_SYNC = "sync";
+const ACTION_SYNC_RESET_CLIENT = SYNC_RESET_CLIENT;
+const ACTION_SYNC_WIPE_CLIENT = SYNC_WIPE_CLIENT;
+const ACTION_SYNC_WIPE_REMOTE = SYNC_WIPE_REMOTE;
+const ACTION_VERIFY = "verify";
+const ACTION_VERIFY_NOT = "verify-not";
+
+const ACTIONS = [
+ ACTION_ADD,
+ ACTION_DELETE,
+ ACTION_MODIFY,
+ ACTION_PRIVATE_BROWSING,
+ ACTION_SET_ENABLED,
+ ACTION_SYNC,
+ ACTION_SYNC_RESET_CLIENT,
+ ACTION_SYNC_WIPE_CLIENT,
+ ACTION_SYNC_WIPE_REMOTE,
+ ACTION_VERIFY,
+ ACTION_VERIFY_NOT,
+];
+
+const OBSERVER_TOPICS = ["fxaccounts:onlogin",
+ "fxaccounts:onlogout",
+ "private-browsing",
+ "quit-application-requested",
+ "sessionstore-windows-restored",
+ "weave:engine:start-tracking",
"weave:engine:stop-tracking",
+ "weave:service:login:error",
+ "weave:service:setup-complete",
"weave:service:sync:finish",
+ "weave:service:sync:delayed",
"weave:service:sync:error",
- "sessionstore-windows-restored",
- "private-browsing"];
+ "weave:service:sync:start"
+ ];
let TPS = {
- _waitingForSync: false,
- _isTracking: false,
- _test: null,
_currentAction: -1,
_currentPhase: -1,
+ _enabledEngines: null,
_errors: 0,
+ _finalPhase: false,
+ _isTracking: false,
+ _operations_pending: 0,
+ _phaseFinished: false,
+ _phaselist: {},
+ _setupComplete: false,
+ _syncActive: false,
_syncErrors: 0,
- _usSinceEpoch: 0,
+ _syncWipeAction: null,
_tabsAdded: 0,
_tabsFinished: 0,
- _phaselist: {},
- _operations_pending: 0,
- _loggedIn: false,
- _enabledEngines: null,
+ _test: null,
+ _triggeredSync: false,
+ _usSinceEpoch: 0,
+
+ _init: function TPS__init() {
+ // Check if Firefox Accounts is enabled
+ let service = Cc["@mozilla.org/weave/service;1"]
+ .getService(Components.interfaces.nsISupports)
+ .wrappedJSObject;
+ this.fxaccounts_enabled = service.fxAccountsEnabled;
- DumpError: function (msg) {
+ this.delayAutoSync();
+
+ OBSERVER_TOPICS.forEach(function (aTopic) {
+ Services.obs.addObserver(this, aTopic, true);
+ }, this);
+
+ // Import the appropriate authentication module
+ if (this.fxaccounts_enabled) {
+ Cu.import("resource://tps/auth/fxaccounts.jsm", module);
+ }
+ else {
+ Cu.import("resource://tps/auth/sync.jsm", module);
+ }
+ },
+
+ DumpError: function TPS__DumpError(msg) {
this._errors++;
Logger.logError("[phase" + this._currentPhase + "] " + msg);
this.quit();
},
- QueryInterface: XPCOMUtils.generateQI([CI.nsIObserver,
- CI.nsISupportsWeakReference]),
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver,
+ Ci.nsISupportsWeakReference]),
observe: function TPS__observe(subject, topic, data) {
try {
Logger.logInfo("----------event observed: " + topic);
+
switch(topic) {
case "private-browsing":
Logger.logInfo("private browsing " + data);
break;
+
+ case "quit-application-requested":
+ // Ensure that we eventually wipe the data on the server
+ if (this._errors || !this._phaseFinished || this._finalPhase) {
+ try {
+ this.WipeServer();
+ } catch (ex) {}
+ }
+
+ OBSERVER_TOPICS.forEach(function(topic) {
+ Services.obs.removeObserver(this, topic);
+ }, this);
+
+ Logger.close();
+
+ break;
+
+ case "sessionstore-windows-restored":
+ Utils.nextTick(this.RunNextTestAction, this);
+ break;
+
+ case "weave:service:setup-complete":
+ this._setupComplete = true;
+
+ if (this._syncWipeAction) {
+ Weave.Svc.Prefs.set("firstSync", this._syncWipeAction);
+ this._syncWipeAction = null;
+ }
+
+ break;
+
case "weave:service:sync:error":
- if (this._waitingForSync && this._syncErrors == 0) {
- // if this is the first sync error, retry...
- Logger.logInfo("sync error; retrying...");
+ this._syncActive = false;
+
+ this.delayAutoSync();
+
+ // If this is the first sync error, retry...
+ if (this._syncErrors === 0) {
+ Logger.logInfo("Sync error; retrying...");
this._syncErrors++;
- this._waitingForSync = false;
Utils.nextTick(this.RunNextTestAction, this);
}
- else if (this._waitingForSync) {
- // ...otherwise abort the test
- this.DumpError("sync error; aborting test");
+ else {
+ this._triggeredSync = false;
+ this.DumpError("Sync error; aborting test");
return;
}
+
break;
case "weave:service:sync:finish":
- if (this._waitingForSync) {
- this._syncErrors = 0;
- this._waitingForSync = false;
- // Wait a second before continuing, otherwise we can get
- // 'sync not complete' errors.
- Utils.namedTimer(function() {
- this.FinishAsyncOperation();
- }, 1000, this, "postsync");
+ this._syncActive = false;
+ this._syncErrors = 0;
+ this._triggeredSync = false;
+
+ this.delayAutoSync();
+
+ // Wait a second before continuing, otherwise we can get
+ // 'sync not complete' errors.
+ Utils.namedTimer(function () {
+ this.FinishAsyncOperation();
+ }, 1000, this, "postsync");
+
+ break;
+
+ case "weave:service:sync:start":
+ // Ensure that the sync operation has been started by TPS
+ if (!this._triggeredSync) {
+ this.DumpError("Automatic sync got triggered, which is not allowed.")
}
+
+ this._syncActive = true;
break;
case "weave:engine:start-tracking":
@@ -130,18 +229,27 @@ let TPS = {
case "weave:engine:stop-tracking":
this._isTracking = false;
break;
-
- case "sessionstore-windows-restored":
- Utils.nextTick(this.RunNextTestAction, this);
- break;
}
}
- catch(e) {
+ catch (e) {
this.DumpError("Exception caught: " + Utils.exceptionStr(e));
return;
}
},
+ /**
+ * Given that we cannot complely disable the automatic sync operations, we
+ * massively delay the next sync. Sync operations have to only happen when
+ * directly called via TPS.Sync()!
+ */
+ delayAutoSync: function TPS_delayAutoSync() {
+ Weave.Svc.Prefs.set("scheduler.eolInterval", 7200);
+ Weave.Svc.Prefs.set("scheduler.immediateInterval", 7200);
+ Weave.Svc.Prefs.set("scheduler.idleInterval", 7200);
+ Weave.Svc.Prefs.set("scheduler.activeInterval", 7200);
+ Weave.Svc.Prefs.set("syncThreshold", 10000000);
+ },
+
StartAsyncOperation: function TPS__StartAsyncOperation() {
this._operations_pending++;
},
@@ -156,11 +264,7 @@ let TPS = {
}
},
- quit: function () {
- OBSERVER_TOPICS.forEach(function(topic) {
- Services.obs.removeObserver(this, topic);
- }, this);
- Logger.close();
+ quit: function TPS__quit() {
this.goQuitApplication();
},
@@ -196,7 +300,12 @@ let TPS = {
Logger.logInfo("tab for " + taburi + " finished loading");
if (that._tabsFinished == that._tabsAdded) {
Logger.logInfo("all tabs loaded, continuing...");
- that.FinishAsyncOperation();
+
+ // Wait a second before continuing to be sure tabs can be synced,
+ // otherwise we can get 'error locating tab'
+ Utils.namedTimer(function () {
+ that.FinishAsyncOperation();
+ }, 1000, this, "postTabsOpening");
}
});
break;
@@ -378,12 +487,15 @@ let TPS = {
Logger.clearPotentialError();
let placesItem;
bookmark['location'] = folder;
+
if (last_item_pos != -1)
bookmark['last_item_pos'] = last_item_pos;
let item_id = -1;
+
if (action != ACTION_MODIFY && action != ACTION_DELETE)
Logger.logInfo("executing action " + action.toUpperCase() +
" on bookmark " + JSON.stringify(bookmark));
+
if ("uri" in bookmark)
placesItem = new Bookmark(bookmark);
else if ("folder" in bookmark)
@@ -392,6 +504,7 @@ let TPS = {
placesItem = new Livemark(bookmark);
else if ("separator" in bookmark)
placesItem = new Separator(bookmark);
+
if (action == ACTION_ADD) {
item_id = placesItem.Create();
}
@@ -430,7 +543,7 @@ let TPS = {
Logger.logPass("executing action " + action.toUpperCase() +
" on bookmarks");
}
- catch(e) {
+ catch (e) {
DumpBookmarks();
throw(e);
}
@@ -463,7 +576,8 @@ let TPS = {
this._phaselist["phase" + this._currentPhase].length) {
// we're all done
Logger.logInfo("test phase " + this._currentPhase + ": " +
- (this._errors ? "FAIL" : "PASS"));
+ (this._errors ? "FAIL" : "PASS"));
+ this._phaseFinished = true;
this.quit();
return;
}
@@ -477,7 +591,7 @@ let TPS = {
let phase = this._phaselist["phase" + this._currentPhase];
let action = phase[this._currentAction];
- Logger.logInfo("starting action: " + JSON.stringify(action));
+ Logger.logInfo("starting action: " + action[0].name);
action[0].apply(this, action.slice(1));
// if we're in an async operation, don't continue on to the next action
@@ -524,8 +638,9 @@ let TPS = {
Logger.init(logpath);
Logger.logInfo("Sync version: " + WEAVE_VERSION);
- Logger.logInfo("Firefox builddate: " + Services.appinfo.appBuildID);
+ Logger.logInfo("Firefox buildid: " + Services.appinfo.appBuildID);
Logger.logInfo("Firefox version: " + Services.appinfo.version);
+ Logger.logInfo('Firefox Accounts enabled: ' + this.fxaccounts_enabled);
// do some sync housekeeping
if (Weave.Service.isLoggedIn) {
@@ -555,10 +670,6 @@ let TPS = {
*/
_executeTestPhase: function _executeTestPhase(file, phase, settings) {
try {
- OBSERVER_TOPICS.forEach(function(topic) {
- Services.obs.addObserver(this, topic, true);
- }, this);
-
// parse the test file
Services.scriptloader.loadSubScript(file, this);
this._currentPhase = phase;
@@ -599,23 +710,33 @@ let TPS = {
// TODO Phases should be defined in a data type that has strong
// ordering, not by lexical sorting.
let currentPhase = parseInt(this._currentPhase, 10);
- // Reset everything at the beginning of the test.
+
+ // Login at the beginning of the test.
if (currentPhase <= 1) {
- this_phase.unshift([this.ResetData]);
+ this_phase.unshift([this.Login]);
}
// Wipe the server at the end of the final test phase.
if (currentPhase >= Object.keys(this.phases).length) {
- this_phase.push([this.WipeServer]);
+ this._finalPhase = true;
+ }
+
+ // If a custom server was specified, set it now
+ if (this.config["serverURL"]) {
+ Weave.Service.serverURL = this.config.serverURL;
+ prefs.setCharPref('tps.serverURL', this.config.serverURL);
}
- // Store account details as prefs so they're accessible to the mozmill
+ // Store account details as prefs so they're accessible to the Mozmill
// framework.
- prefs.setCharPref('tps.account.username', this.config.account.username);
- prefs.setCharPref('tps.account.password', this.config.account.password);
- prefs.setCharPref('tps.account.passphrase', this.config.account.passphrase);
- if (this.config.account['serverURL']) {
- prefs.setCharPref('tps.account.serverURL', this.config.account.serverURL);
+ if (this.fxaccounts_enabled) {
+ prefs.setCharPref('tps.account.username', this.config.fx_account.username);
+ prefs.setCharPref('tps.account.password', this.config.fx_account.password);
+ }
+ else {
+ prefs.setCharPref('tps.account.username', this.config.sync_account.username);
+ prefs.setCharPref('tps.account.password', this.config.sync_account.password);
+ prefs.setCharPref('tps.account.passphrase', this.config.sync_account.passphrase);
}
// start processing the test actions
@@ -664,8 +785,8 @@ let TPS = {
},
RunMozmillTest: function TPS__RunMozmillTest(testfile) {
- var mozmillfile = CC["@mozilla.org/file/local;1"]
- .createInstance(CI.nsILocalFile);
+ var mozmillfile = Cc["@mozilla.org/file/local;1"]
+ .createInstance(Ci.nsILocalFile);
if (hh.oscpu.toLowerCase().indexOf('windows') > -1) {
let re = /\/(\w)\/(.*)/;
this.config.testdir = this.config.testdir.replace(re, "$1://$2").replace(/\//g, "\\");
@@ -675,11 +796,11 @@ let TPS = {
Logger.logInfo("Running mozmill test " + mozmillfile.path);
var frame = {};
- CU.import('resource://mozmill/modules/frame.js', frame);
+ Cu.import('resource://mozmill/modules/frame.js', frame);
frame.events.addListener('setTest', this.MozmillSetTestListener.bind(this));
frame.events.addListener('endTest', this.MozmillEndTestListener.bind(this));
this.StartAsyncOperation();
- frame.runTestFile(mozmillfile.path, false);
+ frame.runTestFile(mozmillfile.path, null);
},
/**
@@ -688,22 +809,41 @@ let TPS = {
* When the event is observed, the function will wait an extra tick before
* returning.
*
- * @param name
+ * @param aEventName
* String event to wait for.
*/
- waitForEvent:function waitForEvent(name) {
- Logger.logInfo("Waiting for " + name + "...");
+ waitForEvent: function waitForEvent(aEventName) {
+ Logger.logInfo("Waiting for " + aEventName + "...");
let cb = Async.makeSpinningCallback();
- Svc.Obs.add(name, cb);
+ Svc.Obs.add(aEventName, cb);
cb.wait();
- Svc.Obs.remove(name, cb);
- Logger.logInfo(name + " observed!");
+ Svc.Obs.remove(aEventName, cb);
+ Logger.logInfo(aEventName + " observed!");
- let cb = Async.makeSpinningCallback();
+ cb = Async.makeSpinningCallback();
Utils.nextTick(cb);
cb.wait();
},
+
+ /**
+ * Waits for Sync to logged in before returning
+ */
+ waitForSetupComplete: function waitForSetup() {
+ if (!this._setupComplete) {
+ this.waitForEvent("weave:service:setup-complete");
+ }
+ },
+
+ /**
+ * Waits for Sync to be finished before returning
+ */
+ waitForSyncFinished: function TPS__waitForSyncFinished() {
+ if (this._syncActive) {
+ this.waitForEvent("weave:service:sync:finished");
+ }
+ },
+
/**
* Waits for Sync to start tracking before returning.
*/
@@ -711,112 +851,58 @@ let TPS = {
if (!this._isTracking) {
this.waitForEvent("weave:engine:start-tracking");
}
-
- let cb = Async.makeSyncCallback();
- Utils.nextTick(cb);
- Async.waitForSyncCallback(cb);
},
/**
- * Reset the client and server to an empty/pure state.
- *
- * All data on the server is wiped and replaced with new keys and local
- * client data. The local client is configured such that it is in sync
- * with the server and ready to handle changes.
- *
- * This is typically called at the beginning of every test to set up a clean
- * slate.
- *
- * This executes synchronously and doesn't return until things are in a good
- * state.
+ * Login on the server
*/
- ResetData: function ResetData() {
- this.Login(true);
-
- Weave.Service.login();
- Weave.Service.wipeServer();
- Weave.Service.resetClient();
- Weave.Service.login();
-
- this.waitForTracking();
- },
-
Login: function Login(force) {
- if (this._loggedIn && !force) {
+ if (Authentication.isLoggedIn && !force) {
return;
}
- let account = this.config.account;
- if (!account) {
- this.DumperError("No account information found! Did you use a valid " +
- "config file?");
- return;
- }
-
- if (account["serverURL"]) {
- Weave.Service.serverURL = account["serverURL"];
- }
-
- Logger.logInfo("Setting client credentials.");
- if (account["admin-secret"]) {
- // if admin-secret is specified, we'll dynamically create
- // a new sync account
- Weave.Svc.Prefs.set("admin-secret", account["admin-secret"]);
- let suffix = account["account-suffix"];
- Weave.Service.identity.account = "tps" + suffix + "@mozilla.com";
- Weave.Service.identity.basicPassword = "tps" + suffix + "tps" + suffix;
- Weave.Service.identity.syncKey = Weave.Utils.generatePassphrase();
- Weave.Service.createAccount(Weave.Service.identity.account,
- Weave.Service.identity.basicPassword,
- "dummy1", "dummy2");
- } else if (account["username"] && account["password"] &&
- account["passphrase"]) {
- Weave.Service.identity.account = account["username"];
- Weave.Service.identity.basicPassword = account["password"];
- Weave.Service.identity.syncKey = account["passphrase"];
- } else {
- this.DumpError("Must specify admin-secret, or " +
- "username/password/passphrase in the config file");
- return;
- }
-
- Weave.Service.login();
- Logger.AssertEqual(Weave.Status.service, Weave.STATUS_OK, "Weave status not OK");
- Weave.Svc.Obs.notify("weave:service:setup-complete");
- this._loggedIn = true;
-
+ Logger.logInfo("Setting client credentials and login.");
+ let account = this.fxaccounts_enabled ? this.config.fx_account
+ : this.config.sync_account;
+ Authentication.signIn(account);
+ this.waitForSetupComplete();
+ Logger.AssertEqual(Weave.Status.service, Weave.STATUS_OK, "Weave status OK");
this.waitForTracking();
},
- Sync: function TPS__Sync(options) {
- Logger.logInfo("executing Sync " + (options ? options : ""));
-
- if (options == SYNC_WIPE_REMOTE) {
- Weave.Svc.Prefs.set("firstSync", "wipeRemote");
- }
- else if (options == SYNC_WIPE_CLIENT) {
- Weave.Svc.Prefs.set("firstSync", "wipeClient");
- }
- else if (options == SYNC_RESET_CLIENT) {
- Weave.Svc.Prefs.set("firstSync", "resetClient");
+ /**
+ * Triggers a sync operation
+ *
+ * @param {String} [wipeAction]
+ * Type of wipe to perform (resetClient, wipeClient, wipeRemote)
+ *
+ */
+ Sync: function TPS__Sync(wipeAction) {
+ Logger.logInfo("Executing Sync" + (wipeAction ? ": " + wipeAction : ""));
+
+ // Force a wipe action if requested. In case of an initial sync the pref
+ // will be overwritten by Sync itself (see bug 992198), so ensure that we
+ // also handle it via the "weave:service:setup-complete" notification.
+ if (wipeAction) {
+ this._syncWipeAction = wipeAction;
+ Weave.Svc.Prefs.set("firstSync", wipeAction);
}
- else if (options) {
- throw new Error("Unhandled options to Sync(): " + options);
- } else {
+ else {
Weave.Svc.Prefs.reset("firstSync");
}
this.Login(false);
- this._waitingForSync = true;
+ this._triggeredSync = true;
this.StartAsyncOperation();
-
Weave.Service.sync();
},
WipeServer: function TPS__WipeServer() {
- Logger.logInfo("WipeServer()");
- this.Login();
+ Logger.logInfo("Wiping data from server.");
+
+ this.Login(false);
+ Weave.Service.login();
Weave.Service.wipeServer();
},
@@ -941,3 +1027,6 @@ var Windows = {
TPS.HandleWindows(aWindow, ACTION_ADD);
},
};
+
+// Initialize TPS
+TPS._init(); \ No newline at end of file