summaryrefslogtreecommitdiffstats
path: root/services/sync/modules-testing
diff options
context:
space:
mode:
Diffstat (limited to 'services/sync/modules-testing')
-rw-r--r--services/sync/modules-testing/fakeservices.js131
-rw-r--r--services/sync/modules-testing/fxa_utils.js58
-rw-r--r--services/sync/modules-testing/rotaryengine.js124
-rw-r--r--services/sync/modules-testing/utils.js350
4 files changed, 663 insertions, 0 deletions
diff --git a/services/sync/modules-testing/fakeservices.js b/services/sync/modules-testing/fakeservices.js
new file mode 100644
index 000000000..2895736df
--- /dev/null
+++ b/services/sync/modules-testing/fakeservices.js
@@ -0,0 +1,131 @@
+/* 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 = [
+ "FakeCryptoService",
+ "FakeFilesystemService",
+ "FakeGUIDService",
+ "fakeSHA256HMAC",
+];
+
+var {utils: Cu} = Components;
+
+Cu.import("resource://services-sync/record.js");
+Cu.import("resource://services-sync/util.js");
+
+var btoa = Cu.import("resource://gre/modules/Log.jsm").btoa;
+
+this.FakeFilesystemService = function FakeFilesystemService(contents) {
+ this.fakeContents = contents;
+ let self = this;
+
+ // Save away the unmocked versions of the functions we replace here for tests
+ // that really want the originals. As this may be called many times per test,
+ // we must be careful to not replace them with ones we previously replaced.
+ // (And WTF are we bothering with these mocks in the first place? Is the
+ // performance of the filesystem *really* such that it outweighs the downside
+ // of not running our real JSON functions in the tests? Eg, these mocks don't
+ // always throw exceptions when the real ones do. Anyway...)
+ for (let name of ["jsonSave", "jsonLoad", "jsonMove", "jsonRemove"]) {
+ let origName = "_real_" + name;
+ if (!Utils[origName]) {
+ Utils[origName] = Utils[name];
+ }
+ }
+
+ Utils.jsonSave = function jsonSave(filePath, that, obj, callback) {
+ let json = typeof obj == "function" ? obj.call(that) : obj;
+ self.fakeContents["weave/" + filePath + ".json"] = JSON.stringify(json);
+ callback.call(that);
+ };
+
+ Utils.jsonLoad = function jsonLoad(filePath, that, cb) {
+ let obj;
+ let json = self.fakeContents["weave/" + filePath + ".json"];
+ if (json) {
+ obj = JSON.parse(json);
+ }
+ cb.call(that, obj);
+ };
+
+ Utils.jsonMove = function jsonMove(aFrom, aTo, that) {
+ const fromPath = "weave/" + aFrom + ".json";
+ self.fakeContents["weave/" + aTo + ".json"] = self.fakeContents[fromPath];
+ delete self.fakeContents[fromPath];
+ return Promise.resolve();
+ };
+
+ Utils.jsonRemove = function jsonRemove(filePath, that) {
+ delete self.fakeContents["weave/" + filePath + ".json"];
+ return Promise.resolve();
+ };
+};
+
+this.fakeSHA256HMAC = function fakeSHA256HMAC(message) {
+ message = message.substr(0, 64);
+ while (message.length < 64) {
+ message += " ";
+ }
+ return message;
+}
+
+this.FakeGUIDService = function FakeGUIDService() {
+ let latestGUID = 0;
+
+ Utils.makeGUID = function makeGUID() {
+ // ensure that this always returns a unique 12 character string
+ let nextGUID = "fake-guid-" + String(latestGUID++).padStart(2, "0");
+ return nextGUID.slice(nextGUID.length-12, nextGUID.length);
+ };
+}
+
+/*
+ * Mock implementation of WeaveCrypto. It does not encrypt or
+ * decrypt, merely returning the input verbatim.
+ */
+this.FakeCryptoService = function FakeCryptoService() {
+ this.counter = 0;
+
+ delete Svc.Crypto; // get rid of the getter first
+ Svc.Crypto = this;
+
+ CryptoWrapper.prototype.ciphertextHMAC = function ciphertextHMAC(keyBundle) {
+ return fakeSHA256HMAC(this.ciphertext);
+ };
+}
+FakeCryptoService.prototype = {
+
+ encrypt: function encrypt(clearText, symmetricKey, iv) {
+ return clearText;
+ },
+
+ decrypt: function decrypt(cipherText, symmetricKey, iv) {
+ return cipherText;
+ },
+
+ generateRandomKey: function generateRandomKey() {
+ return btoa("fake-symmetric-key-" + this.counter++);
+ },
+
+ generateRandomIV: function generateRandomIV() {
+ // A base64-encoded IV is 24 characters long
+ return btoa("fake-fake-fake-random-iv");
+ },
+
+ expandData: function expandData(data, len) {
+ return data;
+ },
+
+ deriveKeyFromPassphrase: function deriveKeyFromPassphrase(passphrase,
+ salt, keyLength) {
+ return "some derived key string composed of bytes";
+ },
+
+ generateRandomBytes: function generateRandomBytes(byteCount) {
+ return "not-so-random-now-are-we-HA-HA-HA! >:)".slice(byteCount);
+ }
+};
+
diff --git a/services/sync/modules-testing/fxa_utils.js b/services/sync/modules-testing/fxa_utils.js
new file mode 100644
index 000000000..70aa17b03
--- /dev/null
+++ b/services/sync/modules-testing/fxa_utils.js
@@ -0,0 +1,58 @@
+"use strict";
+
+this.EXPORTED_SYMBOLS = [
+ "initializeIdentityWithTokenServerResponse",
+];
+
+var {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/rotaryengine.js b/services/sync/modules-testing/rotaryengine.js
new file mode 100644
index 000000000..9d3bf723d
--- /dev/null
+++ b/services/sync/modules-testing/rotaryengine.js
@@ -0,0 +1,124 @@
+/* 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 = [
+ "RotaryEngine",
+ "RotaryRecord",
+ "RotaryStore",
+ "RotaryTracker",
+];
+
+var {utils: Cu} = Components;
+
+Cu.import("resource://services-sync/engines.js");
+Cu.import("resource://services-sync/record.js");
+Cu.import("resource://services-sync/util.js");
+
+/*
+ * A fake engine implementation.
+ * This is used all over the place.
+ *
+ * Complete with record, store, and tracker implementations.
+ */
+
+this.RotaryRecord = function RotaryRecord(collection, id) {
+ CryptoWrapper.call(this, collection, id);
+}
+RotaryRecord.prototype = {
+ __proto__: CryptoWrapper.prototype
+};
+Utils.deferGetSet(RotaryRecord, "cleartext", ["denomination"]);
+
+this.RotaryStore = function RotaryStore(name, engine) {
+ Store.call(this, name, engine);
+ this.items = {};
+}
+RotaryStore.prototype = {
+ __proto__: Store.prototype,
+
+ create: function create(record) {
+ this.items[record.id] = record.denomination;
+ },
+
+ remove: function remove(record) {
+ delete this.items[record.id];
+ },
+
+ update: function update(record) {
+ this.items[record.id] = record.denomination;
+ },
+
+ itemExists: function itemExists(id) {
+ return (id in this.items);
+ },
+
+ createRecord: function createRecord(id, collection) {
+ let record = new RotaryRecord(collection, id);
+
+ if (!(id in this.items)) {
+ record.deleted = true;
+ return record;
+ }
+
+ record.denomination = this.items[id] || "Data for new record: " + id;
+ return record;
+ },
+
+ changeItemID: function changeItemID(oldID, newID) {
+ if (oldID in this.items) {
+ this.items[newID] = this.items[oldID];
+ }
+
+ delete this.items[oldID];
+ },
+
+ getAllIDs: function getAllIDs() {
+ let ids = {};
+ for (let id in this.items) {
+ ids[id] = true;
+ }
+ return ids;
+ },
+
+ wipe: function wipe() {
+ this.items = {};
+ }
+};
+
+this.RotaryTracker = function RotaryTracker(name, engine) {
+ Tracker.call(this, name, engine);
+}
+RotaryTracker.prototype = {
+ __proto__: Tracker.prototype
+};
+
+
+this.RotaryEngine = function RotaryEngine(service) {
+ SyncEngine.call(this, "Rotary", service);
+ // Ensure that the engine starts with a clean slate.
+ this.toFetch = [];
+ this.previousFailed = [];
+}
+RotaryEngine.prototype = {
+ __proto__: SyncEngine.prototype,
+ _storeObj: RotaryStore,
+ _trackerObj: RotaryTracker,
+ _recordObj: RotaryRecord,
+
+ _findDupe: function _findDupe(item) {
+ // This is a semaphore used for testing proper reconciling on dupe
+ // detection.
+ if (item.id == "DUPE_INCOMING") {
+ return "DUPE_LOCAL";
+ }
+
+ for (let [id, value] of Object.entries(this._store.items)) {
+ if (item.denomination == value) {
+ return id;
+ }
+ }
+ }
+};
diff --git a/services/sync/modules-testing/utils.js b/services/sync/modules-testing/utils.js
new file mode 100644
index 000000000..261c2bb21
--- /dev/null
+++ b/services/sync/modules-testing/utils.js
@@ -0,0 +1,350 @@
+/* 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 = [
+ "btoa", // It comes from a module import.
+ "encryptPayload",
+ "isConfiguredWithLegacyIdentity",
+ "ensureLegacyIdentityManager",
+ "setBasicCredentials",
+ "makeIdentityConfig",
+ "makeFxAccountsInternalMock",
+ "configureFxAccountIdentity",
+ "configureIdentity",
+ "SyncTestingInfrastructure",
+ "waitForZeroTimer",
+ "Promise", // from a module import
+ "add_identity_test",
+ "MockFxaStorageManager",
+ "AccountState", // from a module import
+ "sumHistogram",
+];
+
+var {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://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");
+Cu.import("resource://gre/modules/FxAccounts.jsm");
+Cu.import("resource://gre/modules/FxAccountsClient.jsm");
+Cu.import("resource://gre/modules/FxAccountsCommon.js");
+Cu.import("resource://gre/modules/Promise.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
+// and grab non-exported stuff via a backstage pass.
+const {AccountState} = Cu.import("resource://gre/modules/FxAccounts.jsm", {});
+
+// A mock "storage manager" for FxAccounts that doesn't actually write anywhere.
+function MockFxaStorageManager() {
+}
+
+MockFxaStorageManager.prototype = {
+ promiseInitialized: Promise.resolve(),
+
+ initialize(accountData) {
+ this.accountData = accountData;
+ },
+
+ finalize() {
+ return Promise.resolve();
+ },
+
+ getAccountData() {
+ return Promise.resolve(this.accountData);
+ },
+
+ updateAccountData(updatedFields) {
+ for (let [name, value] of Object.entries(updatedFields)) {
+ if (value == null) {
+ delete this.accountData[name];
+ } else {
+ this.accountData[name] = value;
+ }
+ }
+ return Promise.resolve();
+ },
+
+ deleteAccountData() {
+ this.accountData = null;
+ return Promise.resolve();
+ }
+}
+
+/**
+ * First wait >100ms (nsITimers can take up to that much time to fire, so
+ * we can account for the timer in delayedAutoconnect) and then two event
+ * loop ticks (to account for the Utils.nextTick() in autoConnect).
+ */
+this.waitForZeroTimer = function waitForZeroTimer(callback) {
+ let ticks = 2;
+ function wait() {
+ if (ticks) {
+ ticks -= 1;
+ CommonUtils.nextTick(wait);
+ return;
+ }
+ callback();
+ }
+ CommonUtils.namedTimer(wait, 150, {}, "timer");
+}
+
+/**
+ * Return true if Sync is configured with the "legacy" identity provider.
+ */
+this.isConfiguredWithLegacyIdentity = function() {
+ let ns = {};
+ Cu.import("resource://services-sync/service.js", ns);
+
+ // We can't use instanceof as BrowserIDManager (the "other" identity) inherits
+ // from IdentityManager so that would return true - so check the prototype.
+ return Object.getPrototypeOf(ns.Service.identity) === IdentityManager.prototype;
+}
+
+/**
+ * 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 = {};
+ Cu.import("resource://services-sync/service.js", ns);
+
+ let auth = ns.Service.identity;
+ auth.username = username;
+ auth.basicPassword = password;
+ auth.syncKey = 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: "a".repeat(32),
+ verified: true,
+ },
+ token: {
+ endpoint: null,
+ duration: 300,
+ id: "id",
+ key: "key",
+ hashed_fxa_uid: "f".repeat(32), // used during telemetry validation
+ // 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;
+}
+
+this.makeFxAccountsInternalMock = function(config) {
+ return {
+ newAccountState(credentials) {
+ // We only expect this to be called with null indicating the (mock)
+ // storage should be read.
+ if (credentials) {
+ throw new Error("Not expecting to have credentials passed");
+ }
+ let storageManager = new MockFxaStorageManager();
+ storageManager.initialize(config.fxaccount.user);
+ let accountState = new AccountState(storageManager);
+ return accountState;
+ },
+ _getAssertion(audience) {
+ return Promise.resolve("assertion");
+ },
+ };
+};
+
+// 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(),
+ fxaInternal = makeFxAccountsInternalMock(config)) {
+ // 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;
+
+ let fxa = new FxAccounts(fxaInternal);
+
+ let MockFxAccountsClient = function() {
+ FxAccountsClient.apply(this);
+ };
+ MockFxAccountsClient.prototype = {
+ __proto__: FxAccountsClient.prototype,
+ accountStatus() {
+ return Promise.resolve(true);
+ }
+ };
+ let mockFxAClient = new MockFxAccountsClient();
+ fxa.internal._fxAccountsClient = mockFxAClient;
+
+ 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 = config.fxaccount.user;
+ authService._account = config.fxaccount.user.email;
+}
+
+this.configureIdentity = function(identityOverrides) {
+ let config = makeIdentityConfig(identityOverrides);
+ let ns = {};
+ Cu.import("resource://services-sync/service.js", ns);
+
+ 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;
+}
+
+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({});
+ this.fakeGUIDService = new FakeGUIDService();
+ this.fakeCryptoService = new FakeCryptoService();
+}
+
+/**
+ * Turn WBO cleartext into fake "encrypted" payload as it goes over the wire.
+ */
+this.encryptPayload = function encryptPayload(cleartext) {
+ if (typeof cleartext == "object") {
+ cleartext = JSON.stringify(cleartext);
+ }
+
+ return {
+ ciphertext: cleartext, // ciphertext == cleartext with fake crypto
+ IV: "irrelevant",
+ hmac: fakeSHA256HMAC(cleartext, CryptoUtils.makeHMACKey("")),
+ };
+}
+
+// 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;
+ });
+}
+
+this.sumHistogram = function(name, options = {}) {
+ let histogram = options.key ? Services.telemetry.getKeyedHistogramById(name) :
+ Services.telemetry.getHistogramById(name);
+ let snapshot = histogram.snapshot(options.key);
+ let sum = -Infinity;
+ if (snapshot) {
+ sum = snapshot.sum;
+ }
+ histogram.clear();
+ return sum;
+}