diff options
Diffstat (limited to '')
-rw-r--r-- | services/sync/modules-testing/fakeservices.js | 124 | ||||
-rw-r--r-- | services/sync/modules-testing/fxa_utils.js | 65 | ||||
-rw-r--r-- | services/sync/modules-testing/rotaryengine.js | 126 | ||||
-rw-r--r-- | services/sync/modules-testing/utils.js | 361 |
4 files changed, 676 insertions, 0 deletions
diff --git a/services/sync/modules-testing/fakeservices.js b/services/sync/modules-testing/fakeservices.js new file mode 100644 index 0000000000..f13ef27c89 --- /dev/null +++ b/services/sync/modules-testing/fakeservices.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"; + +var EXPORTED_SYMBOLS = [ + "FakeCryptoService", + "FakeFilesystemService", + "FakeGUIDService", + "fakeSHA256HMAC", +]; + +const { Weave } = ChromeUtils.import("resource://services-sync/main.js"); +const { RawCryptoWrapper } = ChromeUtils.import( + "resource://services-sync/record.js" +); +const { Utils } = ChromeUtils.import("resource://services-sync/util.js"); + +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 why 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 = async function jsonSave(filePath, that, obj) { + let json = typeof obj == "function" ? obj.call(that) : obj; + self.fakeContents["weave/" + filePath + ".json"] = JSON.stringify(json); + }; + + Utils.jsonLoad = async function jsonLoad(filePath, that) { + let obj; + let json = self.fakeContents["weave/" + filePath + ".json"]; + if (json) { + obj = JSON.parse(json); + } + return 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(); + }; +} + +function fakeSHA256HMAC(message) { + message = message.substr(0, 64); + while (message.length < 64) { + message += " "; + } + return message; +} + +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. + */ +function FakeCryptoService() { + this.counter = 0; + + delete Weave.Crypto; // get rid of the getter first + Weave.Crypto = this; + + RawCryptoWrapper.prototype.ciphertextHMAC = function ciphertextHMAC( + keyBundle + ) { + return fakeSHA256HMAC(this.ciphertext); + }; +} +FakeCryptoService.prototype = { + async encrypt(clearText, symmetricKey, iv) { + return clearText; + }, + + async decrypt(cipherText, symmetricKey, iv) { + return cipherText; + }, + + async 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; + }, + + 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 0000000000..f55e07c4b8 --- /dev/null +++ b/services/sync/modules-testing/fxa_utils.js @@ -0,0 +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/. */ + +"use strict"; + +var EXPORTED_SYMBOLS = ["initializeIdentityWithTokenServerResponse"]; + +const { Log } = ChromeUtils.importESModule( + "resource://gre/modules/Log.sys.mjs" +); +const { Weave } = ChromeUtils.import("resource://services-sync/main.js"); +const { SyncAuthManager } = ChromeUtils.import( + "resource://services-sync/sync_auth.js" +); +const { TokenServerClient } = ChromeUtils.import( + "resource://services-common/tokenserverclient.js" +); +const { configureFxAccountIdentity } = ChromeUtils.import( + "resource://testing-common/services/sync/utils.js" +); + +// Create a new sync_auth object and initialize it with a +// mocked TokenServerClient which always receives the specified response. +var 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() {}, + async get() { + this.response = response; + return response; + }, + }; + // 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 sync_auth uses. + MockTSC.prototype.observerPrefix = "weave:service"; + + // tie it all together. + Weave.Status.__authManager = Weave.Service.identity = new SyncAuthManager(); + let syncAuthManager = Weave.Service.identity; + // a sanity check + if (!(syncAuthManager instanceof SyncAuthManager)) { + throw new Error("sync isn't configured to use sync_auth"); + } + let mockTSC = new MockTSC(); + configureFxAccountIdentity(syncAuthManager); + syncAuthManager._tokenServerClient = mockTSC; +}; diff --git a/services/sync/modules-testing/rotaryengine.js b/services/sync/modules-testing/rotaryengine.js new file mode 100644 index 0000000000..e8d4293c5a --- /dev/null +++ b/services/sync/modules-testing/rotaryengine.js @@ -0,0 +1,126 @@ +/* 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"; + +var EXPORTED_SYMBOLS = [ + "RotaryEngine", + "RotaryRecord", + "RotaryStore", + "RotaryTracker", +]; + +const { Store, SyncEngine, LegacyTracker } = ChromeUtils.import( + "resource://services-sync/engines.js" +); +const { CryptoWrapper } = ChromeUtils.import( + "resource://services-sync/record.js" +); +const { SerializableSet, Utils } = ChromeUtils.import( + "resource://services-sync/util.js" +); + +/* + * A fake engine implementation. + * This is used all over the place. + * + * Complete with record, store, and tracker implementations. + */ + +function RotaryRecord(collection, id) { + CryptoWrapper.call(this, collection, id); +} +RotaryRecord.prototype = {}; +Object.setPrototypeOf(RotaryRecord.prototype, CryptoWrapper.prototype); +Utils.deferGetSet(RotaryRecord, "cleartext", ["denomination"]); + +function RotaryStore(name, engine) { + Store.call(this, name, engine); + this.items = {}; +} +RotaryStore.prototype = { + async create(record) { + this.items[record.id] = record.denomination; + }, + + async remove(record) { + delete this.items[record.id]; + }, + + async update(record) { + this.items[record.id] = record.denomination; + }, + + async itemExists(id) { + return id in this.items; + }, + + async 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; + }, + + async changeItemID(oldID, newID) { + if (oldID in this.items) { + this.items[newID] = this.items[oldID]; + } + + delete this.items[oldID]; + }, + + async getAllIDs() { + let ids = {}; + for (let id in this.items) { + ids[id] = true; + } + return ids; + }, + + async wipe() { + this.items = {}; + }, +}; + +Object.setPrototypeOf(RotaryStore.prototype, Store.prototype); + +function RotaryTracker(name, engine) { + LegacyTracker.call(this, name, engine); +} +RotaryTracker.prototype = {}; +Object.setPrototypeOf(RotaryTracker.prototype, LegacyTracker.prototype); + +function RotaryEngine(service) { + SyncEngine.call(this, "Rotary", service); + // Ensure that the engine starts with a clean slate. + this.toFetch = new SerializableSet(); + this.previousFailed = new SerializableSet(); +} +RotaryEngine.prototype = { + _storeObj: RotaryStore, + _trackerObj: RotaryTracker, + _recordObj: RotaryRecord, + + async _findDupe(item) { + // This is a Special Value® 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; + } + } + return null; + }, +}; +Object.setPrototypeOf(RotaryEngine.prototype, SyncEngine.prototype); diff --git a/services/sync/modules-testing/utils.js b/services/sync/modules-testing/utils.js new file mode 100644 index 0000000000..38bd5186b7 --- /dev/null +++ b/services/sync/modules-testing/utils.js @@ -0,0 +1,361 @@ +/* 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"; + +var EXPORTED_SYMBOLS = [ + "encryptPayload", + "makeIdentityConfig", + "makeFxAccountsInternalMock", + "configureFxAccountIdentity", + "configureIdentity", + "SyncTestingInfrastructure", + "waitForZeroTimer", + "promiseZeroTimer", + "promiseNamedTimer", + "MockFxaStorageManager", + "AccountState", // from a module import + "sumHistogram", + "syncTestLogging", +]; + +const { CommonUtils } = ChromeUtils.import( + "resource://services-common/utils.js" +); +const { CryptoUtils } = ChromeUtils.import( + "resource://services-crypto/utils.js" +); +const { Assert } = ChromeUtils.importESModule( + "resource://testing-common/Assert.sys.mjs" +); +const { initTestLogging } = ChromeUtils.import( + "resource://testing-common/services/common/logging.js" +); +const { + FakeCryptoService, + FakeFilesystemService, + FakeGUIDService, + fakeSHA256HMAC, +} = ChromeUtils.import( + "resource://testing-common/services/sync/fakeservices.js" +); +const { FxAccounts } = ChromeUtils.import( + "resource://gre/modules/FxAccounts.jsm" +); +const { FxAccountsClient } = ChromeUtils.import( + "resource://gre/modules/FxAccountsClient.jsm" +); +const { SCOPE_OLD_SYNC, LEGACY_SCOPE_WEBEXT_SYNC } = ChromeUtils.import( + "resource://gre/modules/FxAccountsCommon.js" +); + +// and grab non-exported stuff via a backstage pass. +const { AccountState } = ChromeUtils.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(fields = null) { + let result; + if (!this.accountData) { + result = null; + } else if (fields == null) { + // can't use cloneInto as the keys get upset... + result = {}; + for (let field of Object.keys(this.accountData)) { + result[field] = this.accountData[field]; + } + } else { + if (!Array.isArray(fields)) { + fields = [fields]; + } + result = {}; + for (let field of fields) { + result[field] = this.accountData[field]; + } + } + return Promise.resolve(result); + }, + + 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 CommonUtils.nextTick() in autoConnect). + */ +function waitForZeroTimer(callback) { + let ticks = 2; + function wait() { + if (ticks) { + ticks -= 1; + CommonUtils.nextTick(wait); + return; + } + callback(); + } + CommonUtils.namedTimer(wait, 150, {}, "timer"); +} + +var promiseZeroTimer = function() { + return new Promise(resolve => { + waitForZeroTimer(resolve); + }); +}; + +var promiseNamedTimer = function(wait, thisObj, name) { + return new Promise(resolve => { + CommonUtils.namedTimer(resolve, wait, thisObj, name); + }); +}; + +// Return an identity configuration suitable for testing with our identity +// providers. |overrides| can specify overrides for any default values. +// |server| is optional, but if specified, will be used to form the cluster +// URL for the FxA identity. +var 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: { + email: "foo", + kSync: "a".repeat(128), + kXCS: "b".repeat(32), + kExtSync: "c".repeat(128), + kExtKbHash: "d".repeat(64), + scopedKeys: { + [SCOPE_OLD_SYNC]: { + kid: "1234567890123-u7u7u7u7u7u7u7u7u7u7uw", + k: + "qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqg", + kty: "oct", + }, + [LEGACY_SCOPE_WEBEXT_SYNC]: { + kid: "1234567890123-3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d0", + k: + "zMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzA", + kty: "oct", + }, + }, + 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. + }, + }, + }; + + // Now handle any specified overrides. + if (overrides) { + if (overrides.username) { + result.username = overrides.username; + } + if (overrides.fxaccount) { + // TODO: allow just some attributes to be specified + result.fxaccount = overrides.fxaccount; + } + if (overrides.node_type) { + result.fxaccount.token.node_type = overrides.node_type; + } + } + return result; +}; + +var 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; + }, + getOAuthToken: () => Promise.resolve("some-access-token"), + _destroyOAuthToken: () => Promise.resolve(), + keys: { + getScopedKeys: () => + Promise.resolve({ + "https://identity.mozilla.com/apps/oldsync": { + identifier: "https://identity.mozilla.com/apps/oldsync", + keyRotationSecret: + "0000000000000000000000000000000000000000000000000000000000000000", + keyRotationTimestamp: 1510726317123, + }, + }), + }, + profile: { + getProfile() { + return null; + }, + }, + }; +}; + +// Configure an instance of an FxAccount identity provider with the specified +// config (or the default config if not specified). +var 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 = { + accountStatus() { + return Promise.resolve(true); + }, + }; + Object.setPrototypeOf( + MockFxAccountsClient.prototype, + FxAccountsClient.prototype + ); + let mockFxAClient = new MockFxAccountsClient(); + fxa._internal._fxAccountsClient = mockFxAClient; + + let mockTSC = { + // TokenServerClient + async getTokenUsingOAuth(url, oauthToken) { + Assert.equal( + url, + Services.prefs.getStringPref("identity.sync.tokenserver.uri") + ); + Assert.ok(oauthToken, "oauth token present"); + config.fxaccount.token.uid = config.username; + return config.fxaccount.token; + }, + }; + authService._fxaService = fxa; + authService._tokenServerClient = mockTSC; + // Set the "account" of the sync auth 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; +}; + +var configureIdentity = async function(identityOverrides, server) { + let config = makeIdentityConfig(identityOverrides, server); + // Must be imported after the identity configuration is set up. + let { Service } = ChromeUtils.import("resource://services-sync/service.js"); + + // If a server was specified, ensure FxA has a correct cluster URL available. + if (server && !config.fxaccount.token.endpoint) { + let ep = server.baseURI; + if (!ep.endsWith("/")) { + ep += "/"; + } + ep += "1.1/" + config.username + "/"; + config.fxaccount.token.endpoint = ep; + } + + configureFxAccountIdentity(Service.identity, config); + Services.prefs.setStringPref("services.sync.username", config.username); + // many of these tests assume all the auth stuff is setup and don't hit + // a path which causes that auth to magically happen - so do it now. + await Service.identity._ensureValidToken(); + + // and cheat to avoid requiring each test do an explicit login - give it + // a cluster URL. + if (config.fxaccount.token.endpoint) { + Service.clusterURL = config.fxaccount.token.endpoint; + } +}; + +function syncTestLogging(level = "Trace") { + let logStats = initTestLogging(level); + Services.prefs.setStringPref("services.sync.log.logger", level); + Services.prefs.setStringPref("services.sync.log.logger.engine", ""); + return logStats; +} + +var SyncTestingInfrastructure = async function(server, username) { + let config = makeIdentityConfig({ username }); + await configureIdentity(config, server); + return { + logStats: syncTestLogging(), + fakeFilesystem: new FakeFilesystemService({}), + fakeGUIDService: new FakeGUIDService(), + fakeCryptoService: new FakeCryptoService(), + }; +}; + +/** + * Turn WBO cleartext into fake "encrypted" payload as it goes over the wire. + */ +function encryptPayload(cleartext) { + if (typeof cleartext == "object") { + cleartext = JSON.stringify(cleartext); + } + + return { + ciphertext: cleartext, // ciphertext == cleartext with fake crypto + IV: "irrelevant", + hmac: fakeSHA256HMAC(cleartext), + }; +} + +var sumHistogram = function(name, options = {}) { + let histogram = options.key + ? Services.telemetry.getKeyedHistogramById(name) + : Services.telemetry.getHistogramById(name); + let snapshot = histogram.snapshot(); + let sum = -Infinity; + if (snapshot) { + if (options.key && snapshot[options.key]) { + sum = snapshot[options.key].sum; + } else { + sum = snapshot.sum; + } + } + histogram.clear(); + return sum; +}; |