diff options
Diffstat (limited to 'services/fxaccounts/tests/xpcshell/test_storage_manager.js')
-rw-r--r-- | services/fxaccounts/tests/xpcshell/test_storage_manager.js | 477 |
1 files changed, 477 insertions, 0 deletions
diff --git a/services/fxaccounts/tests/xpcshell/test_storage_manager.js b/services/fxaccounts/tests/xpcshell/test_storage_manager.js new file mode 100644 index 000000000..6a293a0ff --- /dev/null +++ b/services/fxaccounts/tests/xpcshell/test_storage_manager.js @@ -0,0 +1,477 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Tests for the FxA storage manager. + +Cu.import("resource://gre/modules/Task.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/FxAccountsStorage.jsm"); +Cu.import("resource://gre/modules/FxAccountsCommon.js"); +Cu.import("resource://gre/modules/Log.jsm"); + +initTestLogging("Trace"); +log.level = Log.Level.Trace; + +const DEVICE_REGISTRATION_VERSION = 42; + +// A couple of mocks we can use. +function MockedPlainStorage(accountData) { + let data = null; + if (accountData) { + data = { + version: DATA_FORMAT_VERSION, + accountData: accountData, + } + } + this.data = data; + this.numReads = 0; +} +MockedPlainStorage.prototype = { + get: Task.async(function* () { + this.numReads++; + Assert.equal(this.numReads, 1, "should only ever be 1 read of acct data"); + return this.data; + }), + + set: Task.async(function* (data) { + this.data = data; + }), +}; + +function MockedSecureStorage(accountData) { + let data = null; + if (accountData) { + data = { + version: DATA_FORMAT_VERSION, + accountData: accountData, + } + } + this.data = data; + this.numReads = 0; +} + +MockedSecureStorage.prototype = { + fetchCount: 0, + locked: false, + STORAGE_LOCKED: function() {}, + get: Task.async(function* (uid, email) { + this.fetchCount++; + if (this.locked) { + throw new this.STORAGE_LOCKED(); + } + this.numReads++; + Assert.equal(this.numReads, 1, "should only ever be 1 read of unlocked data"); + return this.data; + }), + + set: Task.async(function* (uid, contents) { + this.data = contents; + }), +} + +function add_storage_task(testFunction) { + add_task(function* () { + print("Starting test with secure storage manager"); + yield testFunction(new FxAccountsStorageManager()); + }); + add_task(function* () { + print("Starting test with simple storage manager"); + yield testFunction(new FxAccountsStorageManager({useSecure: false})); + }); +} + +// initialized without account data and there's nothing to read. Not logged in. +add_storage_task(function* checkInitializedEmpty(sm) { + if (sm.secureStorage) { + sm.secureStorage = new MockedSecureStorage(null); + } + yield sm.initialize(); + Assert.strictEqual((yield sm.getAccountData()), null); + Assert.rejects(sm.updateAccountData({kA: "kA"}), "No user is logged in") +}); + +// Initialized with account data (ie, simulating a new user being logged in). +// Should reflect the initial data and be written to storage. +add_storage_task(function* checkNewUser(sm) { + let initialAccountData = { + uid: "uid", + email: "someone@somewhere.com", + kA: "kA", + deviceId: "device id" + }; + sm.plainStorage = new MockedPlainStorage() + if (sm.secureStorage) { + sm.secureStorage = new MockedSecureStorage(null); + } + yield sm.initialize(initialAccountData); + let accountData = yield sm.getAccountData(); + Assert.equal(accountData.uid, initialAccountData.uid); + Assert.equal(accountData.email, initialAccountData.email); + Assert.equal(accountData.kA, initialAccountData.kA); + Assert.equal(accountData.deviceId, initialAccountData.deviceId); + + // and it should have been written to storage. + Assert.equal(sm.plainStorage.data.accountData.uid, initialAccountData.uid); + Assert.equal(sm.plainStorage.data.accountData.email, initialAccountData.email); + Assert.equal(sm.plainStorage.data.accountData.deviceId, initialAccountData.deviceId); + // check secure + if (sm.secureStorage) { + Assert.equal(sm.secureStorage.data.accountData.kA, initialAccountData.kA); + } else { + Assert.equal(sm.plainStorage.data.accountData.kA, initialAccountData.kA); + } +}); + +// Initialized without account data but storage has it available. +add_storage_task(function* checkEverythingRead(sm) { + sm.plainStorage = new MockedPlainStorage({ + uid: "uid", + email: "someone@somewhere.com", + deviceId: "wibble", + deviceRegistrationVersion: null + }); + if (sm.secureStorage) { + sm.secureStorage = new MockedSecureStorage(null); + } + yield sm.initialize(); + let accountData = yield sm.getAccountData(); + Assert.ok(accountData, "read account data"); + Assert.equal(accountData.uid, "uid"); + Assert.equal(accountData.email, "someone@somewhere.com"); + Assert.equal(accountData.deviceId, "wibble"); + Assert.equal(accountData.deviceRegistrationVersion, null); + // Update the data - we should be able to fetch it back and it should appear + // in our storage. + yield sm.updateAccountData({ + verified: true, + kA: "kA", + kB: "kB", + deviceRegistrationVersion: DEVICE_REGISTRATION_VERSION + }); + accountData = yield sm.getAccountData(); + Assert.equal(accountData.kB, "kB"); + Assert.equal(accountData.kA, "kA"); + Assert.equal(accountData.deviceId, "wibble"); + Assert.equal(accountData.deviceRegistrationVersion, DEVICE_REGISTRATION_VERSION); + // Check the new value was written to storage. + yield sm._promiseStorageComplete; // storage is written in the background. + // "verified", "deviceId" and "deviceRegistrationVersion" are plain-text fields. + Assert.equal(sm.plainStorage.data.accountData.verified, true); + Assert.equal(sm.plainStorage.data.accountData.deviceId, "wibble"); + Assert.equal(sm.plainStorage.data.accountData.deviceRegistrationVersion, DEVICE_REGISTRATION_VERSION); + // "kA" and "foo" are secure + if (sm.secureStorage) { + Assert.equal(sm.secureStorage.data.accountData.kA, "kA"); + Assert.equal(sm.secureStorage.data.accountData.kB, "kB"); + } else { + Assert.equal(sm.plainStorage.data.accountData.kA, "kA"); + Assert.equal(sm.plainStorage.data.accountData.kB, "kB"); + } +}); + +add_storage_task(function* checkInvalidUpdates(sm) { + sm.plainStorage = new MockedPlainStorage({uid: "uid", email: "someone@somewhere.com"}) + if (sm.secureStorage) { + sm.secureStorage = new MockedSecureStorage(null); + } + Assert.rejects(sm.updateAccountData({uid: "another"}), "Can't change"); + Assert.rejects(sm.updateAccountData({email: "someoneelse"}), "Can't change"); +}); + +add_storage_task(function* checkNullUpdatesRemovedUnlocked(sm) { + if (sm.secureStorage) { + sm.plainStorage = new MockedPlainStorage({uid: "uid", email: "someone@somewhere.com"}) + sm.secureStorage = new MockedSecureStorage({kA: "kA", kB: "kB"}); + } else { + sm.plainStorage = new MockedPlainStorage({uid: "uid", email: "someone@somewhere.com", + kA: "kA", kB: "kB"}); + } + yield sm.initialize(); + + yield sm.updateAccountData({kA: null}); + let accountData = yield sm.getAccountData(); + Assert.ok(!accountData.kA); + Assert.equal(accountData.kB, "kB"); +}); + +add_storage_task(function* checkDelete(sm) { + if (sm.secureStorage) { + sm.plainStorage = new MockedPlainStorage({uid: "uid", email: "someone@somewhere.com"}) + sm.secureStorage = new MockedSecureStorage({kA: "kA", kB: "kB"}); + } else { + sm.plainStorage = new MockedPlainStorage({uid: "uid", email: "someone@somewhere.com", + kA: "kA", kB: "kB"}); + } + yield sm.initialize(); + + yield sm.deleteAccountData(); + // Storage should have been reset to null. + Assert.equal(sm.plainStorage.data, null); + if (sm.secureStorage) { + Assert.equal(sm.secureStorage.data, null); + } + // And everything should reflect no user. + Assert.equal((yield sm.getAccountData()), null); +}); + +// Some tests only for the secure storage manager. +add_task(function* checkNullUpdatesRemovedLocked() { + let sm = new FxAccountsStorageManager(); + sm.plainStorage = new MockedPlainStorage({uid: "uid", email: "someone@somewhere.com"}) + sm.secureStorage = new MockedSecureStorage({kA: "kA", kB: "kB"}); + sm.secureStorage.locked = true; + yield sm.initialize(); + + yield sm.updateAccountData({kA: null}); + let accountData = yield sm.getAccountData(); + Assert.ok(!accountData.kA); + // still no kB as we are locked. + Assert.ok(!accountData.kB); + + // now unlock - should still be no kA but kB should appear. + sm.secureStorage.locked = false; + accountData = yield sm.getAccountData(); + Assert.ok(!accountData.kA); + Assert.equal(accountData.kB, "kB"); + // And secure storage should have been written with our previously-cached + // data. + Assert.strictEqual(sm.secureStorage.data.accountData.kA, undefined); + Assert.strictEqual(sm.secureStorage.data.accountData.kB, "kB"); +}); + +add_task(function* checkEverythingReadSecure() { + let sm = new FxAccountsStorageManager(); + sm.plainStorage = new MockedPlainStorage({uid: "uid", email: "someone@somewhere.com"}) + sm.secureStorage = new MockedSecureStorage({kA: "kA"}); + yield sm.initialize(); + + let accountData = yield sm.getAccountData(); + Assert.ok(accountData, "read account data"); + Assert.equal(accountData.uid, "uid"); + Assert.equal(accountData.email, "someone@somewhere.com"); + Assert.equal(accountData.kA, "kA"); +}); + +add_task(function* checkMemoryFieldsNotReturnedByDefault() { + let sm = new FxAccountsStorageManager(); + sm.plainStorage = new MockedPlainStorage({uid: "uid", email: "someone@somewhere.com"}) + sm.secureStorage = new MockedSecureStorage({kA: "kA"}); + yield sm.initialize(); + + // keyPair is a memory field. + yield sm.updateAccountData({keyPair: "the keypair value"}); + let accountData = yield sm.getAccountData(); + + // Requesting everything should *not* return in memory fields. + Assert.strictEqual(accountData.keyPair, undefined); + // But requesting them specifically does get them. + accountData = yield sm.getAccountData("keyPair"); + Assert.strictEqual(accountData.keyPair, "the keypair value"); +}); + +add_task(function* checkExplicitGet() { + let sm = new FxAccountsStorageManager(); + sm.plainStorage = new MockedPlainStorage({uid: "uid", email: "someone@somewhere.com"}) + sm.secureStorage = new MockedSecureStorage({kA: "kA"}); + yield sm.initialize(); + + let accountData = yield sm.getAccountData(["uid", "kA"]); + Assert.ok(accountData, "read account data"); + Assert.equal(accountData.uid, "uid"); + Assert.equal(accountData.kA, "kA"); + // We didn't ask for email so shouldn't have got it. + Assert.strictEqual(accountData.email, undefined); +}); + +add_task(function* checkExplicitGetNoSecureRead() { + let sm = new FxAccountsStorageManager(); + sm.plainStorage = new MockedPlainStorage({uid: "uid", email: "someone@somewhere.com"}) + sm.secureStorage = new MockedSecureStorage({kA: "kA"}); + yield sm.initialize(); + + Assert.equal(sm.secureStorage.fetchCount, 0); + // request 2 fields in secure storage - it should have caused a single fetch. + let accountData = yield sm.getAccountData(["email", "uid"]); + Assert.ok(accountData, "read account data"); + Assert.equal(accountData.uid, "uid"); + Assert.equal(accountData.email, "someone@somewhere.com"); + Assert.strictEqual(accountData.kA, undefined); + Assert.equal(sm.secureStorage.fetchCount, 1); +}); + +add_task(function* checkLockedUpdates() { + let sm = new FxAccountsStorageManager(); + sm.plainStorage = new MockedPlainStorage({uid: "uid", email: "someone@somewhere.com"}) + sm.secureStorage = new MockedSecureStorage({kA: "old-kA", kB: "kB"}); + sm.secureStorage.locked = true; + yield sm.initialize(); + + let accountData = yield sm.getAccountData(); + // requesting kA and kB will fail as storage is locked. + Assert.ok(!accountData.kA); + Assert.ok(!accountData.kB); + // While locked we can still update it and see the updated value. + sm.updateAccountData({kA: "new-kA"}); + accountData = yield sm.getAccountData(); + Assert.equal(accountData.kA, "new-kA"); + // unlock. + sm.secureStorage.locked = false; + accountData = yield sm.getAccountData(); + // should reflect the value we updated and the one we didn't. + Assert.equal(accountData.kA, "new-kA"); + Assert.equal(accountData.kB, "kB"); + // And storage should also reflect it. + Assert.strictEqual(sm.secureStorage.data.accountData.kA, "new-kA"); + Assert.strictEqual(sm.secureStorage.data.accountData.kB, "kB"); +}); + +// Some tests for the "storage queue" functionality. + +// A helper for our queued tests. It creates a StorageManager and then queues +// an unresolved promise. The tests then do additional setup and checks, then +// resolves or rejects the blocked promise. +var setupStorageManagerForQueueTest = Task.async(function* () { + let sm = new FxAccountsStorageManager(); + sm.plainStorage = new MockedPlainStorage({uid: "uid", email: "someone@somewhere.com"}) + sm.secureStorage = new MockedSecureStorage({kA: "kA"}); + sm.secureStorage.locked = true; + yield sm.initialize(); + + let resolveBlocked, rejectBlocked; + let blockedPromise = new Promise((resolve, reject) => { + resolveBlocked = resolve; + rejectBlocked = reject; + }); + + sm._queueStorageOperation(() => blockedPromise); + return {sm, blockedPromise, resolveBlocked, rejectBlocked} +}); + +// First the general functionality. +add_task(function* checkQueueSemantics() { + let { sm, resolveBlocked } = yield setupStorageManagerForQueueTest(); + + // We've one unresolved promise in the queue - add another promise. + let resolveSubsequent; + let subsequentPromise = new Promise(resolve => { + resolveSubsequent = resolve; + }); + let subsequentCalled = false; + + sm._queueStorageOperation(() => { + subsequentCalled = true; + resolveSubsequent(); + return subsequentPromise; + }); + + // Our "subsequent" function should not have been called yet. + Assert.ok(!subsequentCalled); + + // Release our blocked promise. + resolveBlocked(); + + // Our subsequent promise should end up resolved. + yield subsequentPromise; + Assert.ok(subsequentCalled); + yield sm.finalize(); +}); + +// Check that a queued promise being rejected works correctly. +add_task(function* checkQueueSemanticsOnError() { + let { sm, blockedPromise, rejectBlocked } = yield setupStorageManagerForQueueTest(); + + let resolveSubsequent; + let subsequentPromise = new Promise(resolve => { + resolveSubsequent = resolve; + }); + let subsequentCalled = false; + + sm._queueStorageOperation(() => { + subsequentCalled = true; + resolveSubsequent(); + return subsequentPromise; + }); + + // Our "subsequent" function should not have been called yet. + Assert.ok(!subsequentCalled); + + // Reject our blocked promise - the subsequent operations should still work + // correctly. + rejectBlocked("oh no"); + + // Our subsequent promise should end up resolved. + yield subsequentPromise; + Assert.ok(subsequentCalled); + + // But the first promise should reflect the rejection. + try { + yield blockedPromise; + Assert.ok(false, "expected this promise to reject"); + } catch (ex) { + Assert.equal(ex, "oh no"); + } + yield sm.finalize(); +}); + + +// And some tests for the specific operations that are queued. +add_task(function* checkQueuedReadAndUpdate() { + let { sm, resolveBlocked } = yield setupStorageManagerForQueueTest(); + // Mock the underlying operations + // _doReadAndUpdateSecure is queued by _maybeReadAndUpdateSecure + let _doReadCalled = false; + sm._doReadAndUpdateSecure = () => { + _doReadCalled = true; + return Promise.resolve(); + } + + let resultPromise = sm._maybeReadAndUpdateSecure(); + Assert.ok(!_doReadCalled); + + resolveBlocked(); + yield resultPromise; + Assert.ok(_doReadCalled); + yield sm.finalize(); +}); + +add_task(function* checkQueuedWrite() { + let { sm, resolveBlocked } = yield setupStorageManagerForQueueTest(); + // Mock the underlying operations + let __writeCalled = false; + sm.__write = () => { + __writeCalled = true; + return Promise.resolve(); + } + + let writePromise = sm._write(); + Assert.ok(!__writeCalled); + + resolveBlocked(); + yield writePromise; + Assert.ok(__writeCalled); + yield sm.finalize(); +}); + +add_task(function* checkQueuedDelete() { + let { sm, resolveBlocked } = yield setupStorageManagerForQueueTest(); + // Mock the underlying operations + let _deleteCalled = false; + sm._deleteAccountData = () => { + _deleteCalled = true; + return Promise.resolve(); + } + + let resultPromise = sm.deleteAccountData(); + Assert.ok(!_deleteCalled); + + resolveBlocked(); + yield resultPromise; + Assert.ok(_deleteCalled); + yield sm.finalize(); +}); + +function run_test() { + run_next_test(); +} |