fune/services/fxaccounts/tests/xpcshell/test_storage_manager.js
Cristian Tuns b3bf09cc0d Backed out 6 changesets (bug 1816934, bug 1817182, bug 1817179, bug 1817183) for causing dt failures in browser_jsterm_autocomplete_null.js CLOSED TREE
Backed out changeset 17d4c013ed92 (bug 1817183)
Backed out changeset cfed8d9c23f3 (bug 1817183)
Backed out changeset 62fe2f589efe (bug 1817182)
Backed out changeset 557bd773fb85 (bug 1817179)
Backed out changeset 7f8a7865868b (bug 1816934)
Backed out changeset d6c1d4c0d2a0 (bug 1816934)
2023-02-17 10:51:33 -05:00

590 lines
17 KiB
JavaScript

/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// Tests for the FxA storage manager.
const { FxAccountsStorageManager } = ChromeUtils.import(
"resource://gre/modules/FxAccountsStorage.jsm"
);
const { DATA_FORMAT_VERSION, log } = ChromeUtils.import(
"resource://gre/modules/FxAccountsCommon.js"
);
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,
};
}
this.data = data;
this.numReads = 0;
}
MockedPlainStorage.prototype = {
async get() {
this.numReads++;
Assert.equal(this.numReads, 1, "should only ever be 1 read of acct data");
return this.data;
},
async set(data) {
this.data = data;
},
};
function MockedSecureStorage(accountData) {
let data = null;
if (accountData) {
data = {
version: DATA_FORMAT_VERSION,
accountData,
};
}
this.data = data;
this.numReads = 0;
}
MockedSecureStorage.prototype = {
fetchCount: 0,
locked: false,
/* eslint-disable object-shorthand */
// This constructor must be declared without
// object shorthand or we get an exception of
// "TypeError: this.STORAGE_LOCKED is not a constructor"
STORAGE_LOCKED: function() {},
/* eslint-enable object-shorthand */
async get(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;
},
async set(uid, contents) {
this.data = contents;
},
};
function add_storage_task(testFunction) {
add_task(async function() {
print("Starting test with secure storage manager");
await testFunction(new FxAccountsStorageManager());
});
add_task(async function() {
print("Starting test with simple storage manager");
await testFunction(new FxAccountsStorageManager({ useSecure: false }));
});
}
// initialized without account data and there's nothing to read. Not logged in.
add_storage_task(async function checkInitializedEmpty(sm) {
if (sm.secureStorage) {
sm.secureStorage = new MockedSecureStorage(null);
}
await sm.initialize();
Assert.strictEqual(await sm.getAccountData(), null);
await Assert.rejects(
sm.updateAccountData({ kXCS: "kXCS" }),
/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(async function checkNewUser(sm) {
let initialAccountData = {
uid: "uid",
email: "someone@somewhere.com",
kXCS: "kXCS",
device: {
id: "device id",
},
};
sm.plainStorage = new MockedPlainStorage();
if (sm.secureStorage) {
sm.secureStorage = new MockedSecureStorage(null);
}
await sm.initialize(initialAccountData);
let accountData = await sm.getAccountData();
Assert.equal(accountData.uid, initialAccountData.uid);
Assert.equal(accountData.email, initialAccountData.email);
Assert.equal(accountData.kXCS, initialAccountData.kXCS);
Assert.deepEqual(accountData.device, initialAccountData.device);
// 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.deepEqual(
sm.plainStorage.data.accountData.device,
initialAccountData.device
);
// check secure
if (sm.secureStorage) {
Assert.equal(
sm.secureStorage.data.accountData.kXCS,
initialAccountData.kXCS
);
} else {
Assert.equal(
sm.plainStorage.data.accountData.kXCS,
initialAccountData.kXCS
);
}
});
// Initialized without account data but storage has it available.
add_storage_task(async function checkEverythingRead(sm) {
sm.plainStorage = new MockedPlainStorage({
uid: "uid",
email: "someone@somewhere.com",
device: {
id: "wibble",
registrationVersion: null,
},
});
if (sm.secureStorage) {
sm.secureStorage = new MockedSecureStorage(null);
}
await sm.initialize();
let accountData = await sm.getAccountData();
Assert.ok(accountData, "read account data");
Assert.equal(accountData.uid, "uid");
Assert.equal(accountData.email, "someone@somewhere.com");
Assert.deepEqual(accountData.device, {
id: "wibble",
registrationVersion: null,
});
// Update the data - we should be able to fetch it back and it should appear
// in our storage.
await sm.updateAccountData({
verified: true,
kSync: "kSync",
kXCS: "kXCS",
kExtSync: "kExtSync",
kExtKbHash: "kExtKbHash",
device: {
id: "wibble",
registrationVersion: DEVICE_REGISTRATION_VERSION,
},
});
accountData = await sm.getAccountData();
Assert.equal(accountData.kSync, "kSync");
Assert.equal(accountData.kXCS, "kXCS");
Assert.equal(accountData.kExtSync, "kExtSync");
Assert.equal(accountData.kExtKbHash, "kExtKbHash");
Assert.deepEqual(accountData.device, {
id: "wibble",
registrationVersion: DEVICE_REGISTRATION_VERSION,
});
// Check the new value was written to storage.
await sm._promiseStorageComplete; // storage is written in the background.
Assert.equal(sm.plainStorage.data.accountData.verified, true);
Assert.deepEqual(sm.plainStorage.data.accountData.device, {
id: "wibble",
registrationVersion: DEVICE_REGISTRATION_VERSION,
});
// derive keys are secure
if (sm.secureStorage) {
Assert.equal(sm.secureStorage.data.accountData.kExtKbHash, "kExtKbHash");
Assert.equal(sm.secureStorage.data.accountData.kExtSync, "kExtSync");
Assert.equal(sm.secureStorage.data.accountData.kXCS, "kXCS");
Assert.equal(sm.secureStorage.data.accountData.kSync, "kSync");
} else {
Assert.equal(sm.plainStorage.data.accountData.kExtKbHash, "kExtKbHash");
Assert.equal(sm.plainStorage.data.accountData.kExtSync, "kExtSync");
Assert.equal(sm.plainStorage.data.accountData.kXCS, "kXCS");
Assert.equal(sm.plainStorage.data.accountData.kSync, "kSync");
}
});
add_storage_task(async function checkInvalidUpdates(sm) {
sm.plainStorage = new MockedPlainStorage({
uid: "uid",
email: "someone@somewhere.com",
});
if (sm.secureStorage) {
sm.secureStorage = new MockedSecureStorage(null);
}
await sm.initialize();
await Assert.rejects(
sm.updateAccountData({ uid: "another" }),
/Can't change uid/
);
});
add_storage_task(async function checkNullUpdatesRemovedUnlocked(sm) {
if (sm.secureStorage) {
sm.plainStorage = new MockedPlainStorage({
uid: "uid",
email: "someone@somewhere.com",
});
sm.secureStorage = new MockedSecureStorage({
kSync: "kSync",
kXCS: "kXCS",
kExtSync: "kExtSync",
kExtKbHash: "kExtKbHash",
});
} else {
sm.plainStorage = new MockedPlainStorage({
uid: "uid",
email: "someone@somewhere.com",
kSync: "kSync",
kXCS: "kXCS",
kExtSync: "kExtSync",
kExtKbHash: "kExtKbHash",
});
}
await sm.initialize();
await sm.updateAccountData({ kXCS: null });
let accountData = await sm.getAccountData();
Assert.ok(!accountData.kXCS);
Assert.equal(accountData.kSync, "kSync");
});
add_storage_task(async function checkNullRemovesUnlistedFields(sm) {
// kA and kB are not listed in FXA_PWDMGR_*_FIELDS, but we still want to
// be able to delete them (migration case).
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",
});
}
await sm.initialize();
await sm.updateAccountData({ kA: null, kB: null });
let accountData = await sm.getAccountData();
Assert.ok(!accountData.kA);
Assert.ok(!accountData.kB);
});
add_storage_task(async function checkDelete(sm) {
if (sm.secureStorage) {
sm.plainStorage = new MockedPlainStorage({
uid: "uid",
email: "someone@somewhere.com",
});
sm.secureStorage = new MockedSecureStorage({
kSync: "kSync",
kXCS: "kXCS",
kExtSync: "kExtSync",
kExtKbHash: "kExtKbHash",
});
} else {
sm.plainStorage = new MockedPlainStorage({
uid: "uid",
email: "someone@somewhere.com",
kSync: "kSync",
kXCS: "kXCS",
kExtSync: "kExtSync",
kExtKbHash: "kExtKbHash",
});
}
await sm.initialize();
await 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(await sm.getAccountData(), null);
});
// Some tests only for the secure storage manager.
add_task(async function checkNullUpdatesRemovedLocked() {
let sm = new FxAccountsStorageManager();
sm.plainStorage = new MockedPlainStorage({
uid: "uid",
email: "someone@somewhere.com",
});
sm.secureStorage = new MockedSecureStorage({
kSync: "kSync",
kXCS: "kXCS",
kExtSync: "kExtSync",
kExtKbHash: "kExtKbHash",
});
sm.secureStorage.locked = true;
await sm.initialize();
await sm.updateAccountData({ kSync: null });
let accountData = await sm.getAccountData();
Assert.ok(!accountData.kSync);
// still no kXCS as we are locked.
Assert.ok(!accountData.kXCS);
// now unlock - should still be no kSync but kXCS should appear.
sm.secureStorage.locked = false;
accountData = await sm.getAccountData();
Assert.ok(!accountData.kSync);
Assert.equal(accountData.kXCS, "kXCS");
// And secure storage should have been written with our previously-cached
// data.
Assert.strictEqual(sm.secureStorage.data.accountData.kSync, undefined);
Assert.strictEqual(sm.secureStorage.data.accountData.kXCS, "kXCS");
});
add_task(async function checkEverythingReadSecure() {
let sm = new FxAccountsStorageManager();
sm.plainStorage = new MockedPlainStorage({
uid: "uid",
email: "someone@somewhere.com",
});
sm.secureStorage = new MockedSecureStorage({ kXCS: "kXCS" });
await sm.initialize();
let accountData = await sm.getAccountData();
Assert.ok(accountData, "read account data");
Assert.equal(accountData.uid, "uid");
Assert.equal(accountData.email, "someone@somewhere.com");
Assert.equal(accountData.kXCS, "kXCS");
});
add_task(async function checkExplicitGet() {
let sm = new FxAccountsStorageManager();
sm.plainStorage = new MockedPlainStorage({
uid: "uid",
email: "someone@somewhere.com",
});
sm.secureStorage = new MockedSecureStorage({ kXCS: "kXCS" });
await sm.initialize();
let accountData = await sm.getAccountData(["uid", "kXCS"]);
Assert.ok(accountData, "read account data");
Assert.equal(accountData.uid, "uid");
Assert.equal(accountData.kXCS, "kXCS");
// We didn't ask for email so shouldn't have got it.
Assert.strictEqual(accountData.email, undefined);
});
add_task(async function checkExplicitGetNoSecureRead() {
let sm = new FxAccountsStorageManager();
sm.plainStorage = new MockedPlainStorage({
uid: "uid",
email: "someone@somewhere.com",
});
sm.secureStorage = new MockedSecureStorage({ kXCS: "kXCS" });
await sm.initialize();
Assert.equal(sm.secureStorage.fetchCount, 0);
// request 2 fields in secure storage - it should have caused a single fetch.
let accountData = await 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.kXCS, undefined);
Assert.equal(sm.secureStorage.fetchCount, 1);
});
add_task(async function checkLockedUpdates() {
let sm = new FxAccountsStorageManager();
sm.plainStorage = new MockedPlainStorage({
uid: "uid",
email: "someone@somewhere.com",
});
sm.secureStorage = new MockedSecureStorage({
kSync: "old-kSync",
kXCS: "kXCS",
});
sm.secureStorage.locked = true;
await sm.initialize();
let accountData = await sm.getAccountData();
// requesting kSync and kXCS will fail as storage is locked.
Assert.ok(!accountData.kSync);
Assert.ok(!accountData.kXCS);
// While locked we can still update it and see the updated value.
sm.updateAccountData({ kSync: "new-kSync" });
accountData = await sm.getAccountData();
Assert.equal(accountData.kSync, "new-kSync");
// unlock.
sm.secureStorage.locked = false;
accountData = await sm.getAccountData();
// should reflect the value we updated and the one we didn't.
Assert.equal(accountData.kSync, "new-kSync");
Assert.equal(accountData.kXCS, "kXCS");
// And storage should also reflect it.
Assert.strictEqual(sm.secureStorage.data.accountData.kSync, "new-kSync");
Assert.strictEqual(sm.secureStorage.data.accountData.kXCS, "kXCS");
});
// 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.
async function setupStorageManagerForQueueTest() {
let sm = new FxAccountsStorageManager();
sm.plainStorage = new MockedPlainStorage({
uid: "uid",
email: "someone@somewhere.com",
});
sm.secureStorage = new MockedSecureStorage({ kXCS: "kXCS" });
sm.secureStorage.locked = true;
await 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(async function checkQueueSemantics() {
let { sm, resolveBlocked } = await 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.
await subsequentPromise;
Assert.ok(subsequentCalled);
await sm.finalize();
});
// Check that a queued promise being rejected works correctly.
add_task(async function checkQueueSemanticsOnError() {
let {
sm,
blockedPromise,
rejectBlocked,
} = await 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.
await subsequentPromise;
Assert.ok(subsequentCalled);
// But the first promise should reflect the rejection.
try {
await blockedPromise;
Assert.ok(false, "expected this promise to reject");
} catch (ex) {
Assert.equal(ex, "oh no");
}
await sm.finalize();
});
// And some tests for the specific operations that are queued.
add_task(async function checkQueuedReadAndUpdate() {
let { sm, resolveBlocked } = await 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();
await resultPromise;
Assert.ok(_doReadCalled);
await sm.finalize();
});
add_task(async function checkQueuedWrite() {
let { sm, resolveBlocked } = await setupStorageManagerForQueueTest();
// Mock the underlying operations
let __writeCalled = false;
sm.__write = () => {
__writeCalled = true;
return Promise.resolve();
};
let writePromise = sm._write();
Assert.ok(!__writeCalled);
resolveBlocked();
await writePromise;
Assert.ok(__writeCalled);
await sm.finalize();
});
add_task(async function checkQueuedDelete() {
let { sm, resolveBlocked } = await setupStorageManagerForQueueTest();
// Mock the underlying operations
let _deleteCalled = false;
sm._deleteAccountData = () => {
_deleteCalled = true;
return Promise.resolve();
};
let resultPromise = sm.deleteAccountData();
Assert.ok(!_deleteCalled);
resolveBlocked();
await resultPromise;
Assert.ok(_deleteCalled);
await sm.finalize();
});