diff options
Diffstat (limited to 'services/sync/modules-testing')
-rw-r--r-- | services/sync/modules-testing/fakeservices.js | 131 | ||||
-rw-r--r-- | services/sync/modules-testing/fxa_utils.js | 58 | ||||
-rw-r--r-- | services/sync/modules-testing/rotaryengine.js | 124 | ||||
-rw-r--r-- | services/sync/modules-testing/utils.js | 350 |
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; +} |