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.sys.mjs114
-rw-r--r--services/sync/modules-testing/fxa_utils.sys.mjs55
-rw-r--r--services/sync/modules-testing/rotaryengine.sys.mjs120
-rw-r--r--services/sync/modules-testing/utils.sys.mjs319
4 files changed, 608 insertions, 0 deletions
diff --git a/services/sync/modules-testing/fakeservices.sys.mjs b/services/sync/modules-testing/fakeservices.sys.mjs
new file mode 100644
index 0000000000..4fd7534bf1
--- /dev/null
+++ b/services/sync/modules-testing/fakeservices.sys.mjs
@@ -0,0 +1,114 @@
+/* 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/. */
+
+import { Weave } from "resource://services-sync/main.sys.mjs";
+import { RawCryptoWrapper } from "resource://services-sync/record.sys.mjs";
+import { Utils } from "resource://services-sync/util.sys.mjs";
+
+export 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();
+ };
+}
+
+export function fakeSHA256HMAC(message) {
+ message = message.substr(0, 64);
+ while (message.length < 64) {
+ message += " ";
+ }
+ return message;
+}
+
+export 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.
+ */
+export 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.sys.mjs b/services/sync/modules-testing/fxa_utils.sys.mjs
new file mode 100644
index 0000000000..c953f0eaa3
--- /dev/null
+++ b/services/sync/modules-testing/fxa_utils.sys.mjs
@@ -0,0 +1,55 @@
+/* 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/. */
+
+import { Log } from "resource://gre/modules/Log.sys.mjs";
+
+import { Weave } from "resource://services-sync/main.sys.mjs";
+import { SyncAuthManager } from "resource://services-sync/sync_auth.sys.mjs";
+
+import { TokenServerClient } from "resource://services-common/tokenserverclient.sys.mjs";
+import { configureFxAccountIdentity } from "resource://testing-common/services/sync/utils.sys.mjs";
+
+// Create a new sync_auth object and initialize it with a
+// mocked TokenServerClient which always receives the specified response.
+export 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.sys.mjs b/services/sync/modules-testing/rotaryengine.sys.mjs
new file mode 100644
index 0000000000..d7f2165e4d
--- /dev/null
+++ b/services/sync/modules-testing/rotaryengine.sys.mjs
@@ -0,0 +1,120 @@
+/* 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/. */
+
+import {
+ Store,
+ SyncEngine,
+ LegacyTracker,
+} from "resource://services-sync/engines.sys.mjs";
+
+import { CryptoWrapper } from "resource://services-sync/record.sys.mjs";
+import { SerializableSet, Utils } from "resource://services-sync/util.sys.mjs";
+
+/*
+ * A fake engine implementation.
+ * This is used all over the place.
+ *
+ * Complete with record, store, and tracker implementations.
+ */
+
+export function RotaryRecord(collection, id) {
+ CryptoWrapper.call(this, collection, id);
+}
+
+RotaryRecord.prototype = {};
+Object.setPrototypeOf(RotaryRecord.prototype, CryptoWrapper.prototype);
+Utils.deferGetSet(RotaryRecord, "cleartext", ["denomination"]);
+
+export 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);
+
+export function RotaryTracker(name, engine) {
+ LegacyTracker.call(this, name, engine);
+}
+
+RotaryTracker.prototype = {};
+Object.setPrototypeOf(RotaryTracker.prototype, LegacyTracker.prototype);
+
+export 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.sys.mjs b/services/sync/modules-testing/utils.sys.mjs
new file mode 100644
index 0000000000..498bf9872a
--- /dev/null
+++ b/services/sync/modules-testing/utils.sys.mjs
@@ -0,0 +1,319 @@
+/* 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/. */
+
+import { CommonUtils } from "resource://services-common/utils.sys.mjs";
+
+import { Assert } from "resource://testing-common/Assert.sys.mjs";
+
+import { initTestLogging } from "resource://testing-common/services/common/logging.sys.mjs";
+import {
+ FakeCryptoService,
+ FakeFilesystemService,
+ FakeGUIDService,
+ fakeSHA256HMAC,
+} from "resource://testing-common/services/sync/fakeservices.sys.mjs";
+
+import {
+ FxAccounts,
+ AccountState,
+} from "resource://gre/modules/FxAccounts.sys.mjs";
+import { FxAccountsClient } from "resource://gre/modules/FxAccountsClient.sys.mjs";
+
+import { SCOPE_OLD_SYNC } from "resource://gre/modules/FxAccountsCommon.sys.mjs";
+
+// A mock "storage manager" for FxAccounts that doesn't actually write anywhere.
+export 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).
+ */
+export function waitForZeroTimer(callback) {
+ let ticks = 2;
+ function wait() {
+ if (ticks) {
+ ticks -= 1;
+ CommonUtils.nextTick(wait);
+ return;
+ }
+ callback();
+ }
+ CommonUtils.namedTimer(wait, 150, {}, "timer");
+}
+
+export var promiseZeroTimer = function () {
+ return new Promise(resolve => {
+ waitForZeroTimer(resolve);
+ });
+};
+
+export 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.
+export 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",
+ scopedKeys: {
+ [SCOPE_OLD_SYNC]: {
+ kid: "1234567890123-u7u7u7u7u7u7u7u7u7u7uw",
+ k: "qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqg",
+ 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;
+};
+
+export 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).
+export 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;
+};
+
+export var configureIdentity = async function (identityOverrides, server) {
+ let config = makeIdentityConfig(identityOverrides, server);
+ // Must be imported after the identity configuration is set up.
+ let { Service } = ChromeUtils.importESModule(
+ "resource://services-sync/service.sys.mjs"
+ );
+
+ // 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;
+ }
+};
+
+export 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;
+}
+
+export 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.
+ */
+export function encryptPayload(cleartext) {
+ if (typeof cleartext == "object") {
+ cleartext = JSON.stringify(cleartext);
+ }
+
+ return {
+ ciphertext: cleartext, // ciphertext == cleartext with fake crypto
+ IV: "irrelevant",
+ hmac: fakeSHA256HMAC(cleartext),
+ };
+}
+
+export 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;
+};