diff options
author | Pale Moon <git-repo@palemoon.org> | 2016-09-01 13:39:08 +0200 |
---|---|---|
committer | Pale Moon <git-repo@palemoon.org> | 2016-09-01 13:39:08 +0200 |
commit | 3d8ce1a11a7347cc94a937719c4bc8df46fb8d14 (patch) | |
tree | 8c26ca375a6312751c00a27e1653fb6f189f0463 /services/sync | |
parent | e449bdb1ec3a82f204bffdd9c3c54069d086eee3 (diff) | |
download | palemoon-gre-3d8ce1a11a7347cc94a937719c4bc8df46fb8d14.tar.gz |
Base import of Tycho code (warning: huge commit)
Diffstat (limited to 'services/sync')
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 Binary files differindex 973bc00cb..973bc00cb 100644 --- a/services/sync/tests/tps/restartless.xpi +++ b/services/sync/tests/tps/addons/restartless.xpi diff --git a/services/sync/tests/tps/unsigned.xpi b/services/sync/tests/tps/addons/unsigned.xpi Binary files differindex 51b00475a..51b00475a 100644 --- a/services/sync/tests/tps/unsigned.xpi +++ b/services/sync/tests/tps/addons/unsigned.xpi 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(/ /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 |