summaryrefslogtreecommitdiff
path: root/services/sync/tests/unit/head_helpers.js
diff options
context:
space:
mode:
Diffstat (limited to 'services/sync/tests/unit/head_helpers.js')
-rw-r--r--services/sync/tests/unit/head_helpers.js446
1 files changed, 446 insertions, 0 deletions
diff --git a/services/sync/tests/unit/head_helpers.js b/services/sync/tests/unit/head_helpers.js
new file mode 100644
index 0000000000..3c59e1de55
--- /dev/null
+++ b/services/sync/tests/unit/head_helpers.js
@@ -0,0 +1,446 @@
+/* Any copyright is dedicated to the Public Domain.
+ 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");
+Cu.import("resource://services-sync/util.js");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyGetter(this, 'SyncPingSchema', function() {
+ let ns = {};
+ Cu.import("resource://gre/modules/FileUtils.jsm", ns);
+ let stream = Cc["@mozilla.org/network/file-input-stream;1"]
+ .createInstance(Ci.nsIFileInputStream);
+ let jsonReader = Cc["@mozilla.org/dom/json;1"]
+ .createInstance(Components.interfaces.nsIJSON);
+ let schema;
+ try {
+ let schemaFile = do_get_file("sync_ping_schema.json");
+ stream.init(schemaFile, ns.FileUtils.MODE_RDONLY, ns.FileUtils.PERMS_FILE, 0);
+ schema = jsonReader.decodeFromStream(stream, stream.available());
+ } finally {
+ stream.close();
+ }
+
+ // Allow tests to make whatever engines they want, this shouldn't cause
+ // validation failure.
+ schema.definitions.engine.properties.name = { type: "string" };
+ return schema;
+});
+
+XPCOMUtils.defineLazyGetter(this, 'SyncPingValidator', function() {
+ let ns = {};
+ Cu.import("resource://testing-common/ajv-4.1.1.js", ns);
+ let ajv = new ns.Ajv({ async: "co*" });
+ return ajv.compile(SyncPingSchema);
+});
+
+var provider = {
+ getFile: function(prop, persistent) {
+ persistent.value = true;
+ switch (prop) {
+ case "ExtPrefDL":
+ return [Services.dirsvc.get("CurProcD", Ci.nsIFile)];
+ default:
+ throw Cr.NS_ERROR_FAILURE;
+ }
+ },
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIDirectoryServiceProvider])
+};
+Services.dirsvc.QueryInterface(Ci.nsIDirectoryService).registerProvider(provider);
+
+// This is needed for loadAddonTestFunctions().
+var gGlobalScope = this;
+
+function ExtensionsTestPath(path) {
+ if (path[0] != "/") {
+ throw Error("Path must begin with '/': " + path);
+ }
+
+ return "../../../../toolkit/mozapps/extensions/test/xpcshell" + path;
+}
+
+/**
+ * Loads the AddonManager test functions by importing its test file.
+ *
+ * This should be called in the global scope of any test file needing to
+ * interface with the AddonManager. It should only be called once, or the
+ * universe will end.
+ */
+function loadAddonTestFunctions() {
+ const path = ExtensionsTestPath("/head_addons.js");
+ let file = do_get_file(path);
+ let uri = Services.io.newFileURI(file);
+ Services.scriptloader.loadSubScript(uri.spec, gGlobalScope);
+ createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2");
+}
+
+function webExtensionsTestPath(path) {
+ if (path[0] != "/") {
+ throw Error("Path must begin with '/': " + path);
+ }
+
+ return "../../../../toolkit/components/extensions/test/xpcshell" + path;
+}
+
+/**
+ * Loads the WebExtension test functions by importing its test file.
+ */
+function loadWebExtensionTestFunctions() {
+ const path = webExtensionsTestPath("/head_sync.js");
+ let file = do_get_file(path);
+ let uri = Services.io.newFileURI(file);
+ Services.scriptloader.loadSubScript(uri.spec, gGlobalScope);
+}
+
+function getAddonInstall(name) {
+ let f = do_get_file(ExtensionsTestPath("/addons/" + name + ".xpi"));
+ let cb = Async.makeSyncCallback();
+ AddonManager.getInstallForFile(f, cb);
+
+ return Async.waitForSyncCallback(cb);
+}
+
+/**
+ * Obtains an addon from the add-on manager by id.
+ *
+ * This is merely a synchronous wrapper.
+ *
+ * @param id
+ * ID of add-on to fetch
+ * @return addon object on success or undefined or null on failure
+ */
+function getAddonFromAddonManagerByID(id) {
+ let cb = Async.makeSyncCallback();
+ AddonManager.getAddonByID(id, cb);
+ return Async.waitForSyncCallback(cb);
+}
+
+/**
+ * Installs an add-on synchronously from an addonInstall
+ *
+ * @param install addonInstall instance to install
+ */
+function installAddonFromInstall(install) {
+ let cb = Async.makeSyncCallback();
+ let listener = {onInstallEnded: cb};
+ AddonManager.addInstallListener(listener);
+ install.install();
+ Async.waitForSyncCallback(cb);
+ AddonManager.removeAddonListener(listener);
+
+ do_check_neq(null, install.addon);
+ do_check_neq(null, install.addon.syncGUID);
+
+ return install.addon;
+}
+
+/**
+ * Convenience function to install an add-on from the extensions unit tests.
+ *
+ * @param name
+ * String name of add-on to install. e.g. test_install1
+ * @return addon object that was installed
+ */
+function installAddon(name) {
+ let install = getAddonInstall(name);
+ do_check_neq(null, install);
+ return installAddonFromInstall(install);
+}
+
+/**
+ * Convenience function to uninstall an add-on synchronously.
+ *
+ * @param addon
+ * Addon instance to uninstall
+ */
+function uninstallAddon(addon) {
+ let cb = Async.makeSyncCallback();
+ let listener = {onUninstalled: function(uninstalled) {
+ if (uninstalled.id == addon.id) {
+ AddonManager.removeAddonListener(listener);
+ cb(uninstalled);
+ }
+ }};
+
+ AddonManager.addAddonListener(listener);
+ addon.uninstall();
+ Async.waitForSyncCallback(cb);
+}
+
+function generateNewKeys(collectionKeys, collections=null) {
+ let wbo = collectionKeys.generateNewKeysWBO(collections);
+ let modified = new_timestamp();
+ 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]);
+ }
+}
+
+// Helper function to get the sync telemetry and add the typically used test
+// engine names to its list of allowed engines.
+function get_sync_test_telemetry() {
+ let ns = {};
+ Cu.import("resource://services-sync/telemetry.js", ns);
+ let testEngines = ["rotary", "steam", "sterling", "catapult"];
+ for (let engineName of testEngines) {
+ ns.SyncTelemetry.allowedEngines.add(engineName);
+ }
+ ns.SyncTelemetry.submissionInterval = -1;
+ return ns.SyncTelemetry;
+}
+
+function assert_valid_ping(record) {
+ // This is called as the test harness tears down due to shutdown. This
+ // will typically have no recorded syncs, and the validator complains about
+ // it. So ignore such records (but only ignore when *both* shutdown and
+ // no Syncs - either of them not being true might be an actual problem)
+ if (record && (record.why != "shutdown" || record.syncs.length != 0)) {
+ if (!SyncPingValidator(record)) {
+ deepEqual([], SyncPingValidator.errors, "Sync telemetry ping validation failed");
+ }
+ equal(record.version, 1);
+ record.syncs.forEach(p => {
+ lessOrEqual(p.when, Date.now());
+ if (p.devices) {
+ ok(!p.devices.some(device => device.id == p.deviceID));
+ equal(new Set(p.devices.map(device => device.id)).size,
+ p.devices.length, "Duplicate device ids in ping devices list");
+ }
+ });
+ }
+}
+
+// Asserts that `ping` is a ping that doesn't contain any failure information
+function assert_success_ping(ping) {
+ ok(!!ping);
+ assert_valid_ping(ping);
+ ping.syncs.forEach(record => {
+ ok(!record.failureReason);
+ equal(undefined, record.status);
+ greater(record.engines.length, 0);
+ for (let e of record.engines) {
+ ok(!e.failureReason);
+ equal(undefined, e.status);
+ if (e.validation) {
+ equal(undefined, e.validation.problems);
+ equal(undefined, e.validation.failureReason);
+ }
+ if (e.outgoing) {
+ for (let o of e.outgoing) {
+ equal(undefined, o.failed);
+ notEqual(undefined, o.sent);
+ }
+ }
+ if (e.incoming) {
+ equal(undefined, e.incoming.failed);
+ equal(undefined, e.incoming.newFailed);
+ notEqual(undefined, e.incoming.applied || e.incoming.reconciled);
+ }
+ }
+ });
+}
+
+// Hooks into telemetry to validate all pings after calling.
+function validate_all_future_pings() {
+ let telem = get_sync_test_telemetry();
+ telem.submit = assert_valid_ping;
+}
+
+function wait_for_ping(callback, allowErrorPings, getFullPing = false) {
+ return new Promise(resolve => {
+ let telem = get_sync_test_telemetry();
+ let oldSubmit = telem.submit;
+ telem.submit = function(record) {
+ telem.submit = oldSubmit;
+ if (allowErrorPings) {
+ assert_valid_ping(record);
+ } else {
+ assert_success_ping(record);
+ }
+ if (getFullPing) {
+ resolve(record);
+ } else {
+ equal(record.syncs.length, 1);
+ resolve(record.syncs[0]);
+ }
+ };
+ callback();
+ });
+}
+
+// Short helper for wait_for_ping
+function sync_and_validate_telem(allowErrorPings, getFullPing = false) {
+ return wait_for_ping(() => Service.sync(), allowErrorPings, getFullPing);
+}
+
+// Used for the (many) cases where we do a 'partial' sync, where only a single
+// engine is actually synced, but we still want to ensure we're generating a
+// valid ping. Returns a promise that resolves to the ping, or rejects with the
+// thrown error after calling an optional callback.
+function sync_engine_and_validate_telem(engine, allowErrorPings, onError) {
+ return new Promise((resolve, reject) => {
+ let telem = get_sync_test_telemetry();
+ let caughtError = null;
+ // Clear out status, so failures from previous syncs won't show up in the
+ // telemetry ping.
+ let ns = {};
+ Cu.import("resource://services-sync/status.js", ns);
+ ns.Status._engines = {};
+ ns.Status.partial = false;
+ // Ideally we'd clear these out like we do with engines, (probably via
+ // Status.resetSync()), but this causes *numerous* tests to fail, so we just
+ // assume that if no failureReason or engine failures are set, and the
+ // status properties are the same as they were initially, that it's just
+ // a leftover.
+ // This is only an issue since we're triggering the sync of just one engine,
+ // without doing any other parts of the sync.
+ let initialServiceStatus = ns.Status._service;
+ let initialSyncStatus = ns.Status._sync;
+
+ let oldSubmit = telem.submit;
+ telem.submit = function(ping) {
+ telem.submit = oldSubmit;
+ ping.syncs.forEach(record => {
+ if (record && record.status) {
+ // did we see anything to lead us to believe that something bad actually happened
+ let realProblem = record.failureReason || record.engines.some(e => {
+ if (e.failureReason || e.status) {
+ return true;
+ }
+ if (e.outgoing && e.outgoing.some(o => o.failed > 0)) {
+ return true;
+ }
+ return e.incoming && e.incoming.failed;
+ });
+ if (!realProblem) {
+ // no, so if the status is the same as it was initially, just assume
+ // that its leftover and that we can ignore it.
+ if (record.status.sync && record.status.sync == initialSyncStatus) {
+ delete record.status.sync;
+ }
+ if (record.status.service && record.status.service == initialServiceStatus) {
+ delete record.status.service;
+ }
+ if (!record.status.sync && !record.status.service) {
+ delete record.status;
+ }
+ }
+ }
+ });
+ if (allowErrorPings) {
+ assert_valid_ping(ping);
+ } else {
+ assert_success_ping(ping);
+ }
+ equal(ping.syncs.length, 1);
+ if (caughtError) {
+ if (onError) {
+ onError(ping.syncs[0]);
+ }
+ reject(caughtError);
+ } else {
+ resolve(ping.syncs[0]);
+ }
+ }
+ Svc.Obs.notify("weave:service:sync:start");
+ try {
+ engine.sync();
+ } catch (e) {
+ caughtError = e;
+ }
+ if (caughtError) {
+ Svc.Obs.notify("weave:service:sync:error", caughtError);
+ } else {
+ Svc.Obs.notify("weave:service:sync:finish");
+ }
+ });
+}
+
+// Avoid an issue where `client.name2` containing unicode characters causes
+// a number of tests to fail, due to them assuming that we do not need to utf-8
+// encode or decode data sent through the mocked server (see bug 1268912).
+Utils.getDefaultDeviceName = function() {
+ return "Test device name";
+};
+
+