forked from mirrors/gecko-dev
Theoretically we only need to change this where the strings might be non-ascii but it seems safer in the long run to just avoid the "char" versions entirely. Differential Revision: https://phabricator.services.mozilla.com/D200342
2108 lines
56 KiB
JavaScript
2108 lines
56 KiB
JavaScript
/* Any copyright is dedicated to the Public Domain.
|
|
* http://creativecommons.org/publicdomain/zero/1.0/ */
|
|
|
|
const { ClientEngine, ClientsRec } = ChromeUtils.importESModule(
|
|
"resource://services-sync/engines/clients.sys.mjs"
|
|
);
|
|
const { CryptoWrapper } = ChromeUtils.importESModule(
|
|
"resource://services-sync/record.sys.mjs"
|
|
);
|
|
const { Service } = ChromeUtils.importESModule(
|
|
"resource://services-sync/service.sys.mjs"
|
|
);
|
|
|
|
const MORE_THAN_CLIENTS_TTL_REFRESH = 691200; // 8 days
|
|
const LESS_THAN_CLIENTS_TTL_REFRESH = 86400; // 1 day
|
|
|
|
let engine;
|
|
|
|
/**
|
|
* Unpack the record with this ID, and verify that it has the same version that
|
|
* we should be putting into records.
|
|
*/
|
|
async function check_record_version(user, id) {
|
|
let payload = user.collection("clients").wbo(id).data;
|
|
|
|
let rec = new CryptoWrapper();
|
|
rec.id = id;
|
|
rec.collection = "clients";
|
|
rec.ciphertext = payload.ciphertext;
|
|
rec.hmac = payload.hmac;
|
|
rec.IV = payload.IV;
|
|
|
|
let cleartext = await rec.decrypt(
|
|
Service.collectionKeys.keyForCollection("clients")
|
|
);
|
|
|
|
_("Payload is " + JSON.stringify(cleartext));
|
|
equal(Services.appinfo.version, cleartext.version);
|
|
equal(1, cleartext.protocols.length);
|
|
equal("1.5", cleartext.protocols[0]);
|
|
}
|
|
|
|
// compare 2 different command arrays, taking into account that a flowID
|
|
// attribute must exist, be unique in the commands, but isn't specified in
|
|
// "expected" as the value isn't known.
|
|
function compareCommands(actual, expected, description) {
|
|
let tweakedActual = JSON.parse(JSON.stringify(actual));
|
|
tweakedActual.map(elt => delete elt.flowID);
|
|
deepEqual(tweakedActual, expected, description);
|
|
// each item must have a unique flowID.
|
|
let allIDs = new Set(actual.map(elt => elt.flowID).filter(fid => !!fid));
|
|
equal(allIDs.size, actual.length, "all items have unique IDs");
|
|
}
|
|
|
|
async function syncClientsEngine(server) {
|
|
engine._lastFxADevicesFetch = 0;
|
|
engine.lastModified = server.getCollection("foo", "clients").timestamp;
|
|
await engine._sync();
|
|
}
|
|
|
|
add_task(async function setup() {
|
|
engine = Service.clientsEngine;
|
|
});
|
|
|
|
async function cleanup() {
|
|
for (const pref of Svc.PrefBranch.getChildList("")) {
|
|
Svc.PrefBranch.clearUserPref(pref);
|
|
}
|
|
await engine._tracker.clearChangedIDs();
|
|
await engine._resetClient();
|
|
// un-cleanup the logs (the resetBranch will have reset their levels), since
|
|
// not all the tests use SyncTestingInfrastructure, and it's cheap.
|
|
syncTestLogging();
|
|
// We don't finalize storage at cleanup, since we use the same clients engine
|
|
// instance across all tests.
|
|
}
|
|
|
|
add_task(async function test_bad_hmac() {
|
|
_("Ensure that Clients engine deletes corrupt records.");
|
|
let deletedCollections = [];
|
|
let deletedItems = [];
|
|
let callback = {
|
|
onItemDeleted(username, coll, wboID) {
|
|
deletedItems.push(coll + "/" + wboID);
|
|
},
|
|
onCollectionDeleted(username, coll) {
|
|
deletedCollections.push(coll);
|
|
},
|
|
};
|
|
Object.setPrototypeOf(callback, SyncServerCallback);
|
|
let server = await serverForFoo(engine, callback);
|
|
let user = server.user("foo");
|
|
|
|
function check_clients_count(expectedCount) {
|
|
let coll = user.collection("clients");
|
|
|
|
// Treat a non-existent collection as empty.
|
|
equal(expectedCount, coll ? coll.count() : 0);
|
|
}
|
|
|
|
function check_client_deleted(id) {
|
|
let coll = user.collection("clients");
|
|
let wbo = coll.wbo(id);
|
|
return !wbo || !wbo.payload;
|
|
}
|
|
|
|
async function uploadNewKeys() {
|
|
await generateNewKeys(Service.collectionKeys);
|
|
let serverKeys = Service.collectionKeys.asWBO("crypto", "keys");
|
|
await serverKeys.encrypt(Service.identity.syncKeyBundle);
|
|
ok(
|
|
(await serverKeys.upload(Service.resource(Service.cryptoKeysURL))).success
|
|
);
|
|
}
|
|
|
|
try {
|
|
await configureIdentity({ username: "foo" }, server);
|
|
await Service.login();
|
|
|
|
await generateNewKeys(Service.collectionKeys);
|
|
|
|
_("First sync, client record is uploaded");
|
|
equal(engine.lastRecordUpload, 0);
|
|
ok(engine.isFirstSync);
|
|
check_clients_count(0);
|
|
await syncClientsEngine(server);
|
|
check_clients_count(1);
|
|
ok(engine.lastRecordUpload > 0);
|
|
ok(!engine.isFirstSync);
|
|
|
|
// Our uploaded record has a version.
|
|
await check_record_version(user, engine.localID);
|
|
|
|
// Initial setup can wipe the server, so clean up.
|
|
deletedCollections = [];
|
|
deletedItems = [];
|
|
|
|
_("Change our keys and our client ID, reupload keys.");
|
|
let oldLocalID = engine.localID; // Preserve to test for deletion!
|
|
engine.localID = Utils.makeGUID();
|
|
await engine.resetClient();
|
|
await generateNewKeys(Service.collectionKeys);
|
|
let serverKeys = Service.collectionKeys.asWBO("crypto", "keys");
|
|
await serverKeys.encrypt(Service.identity.syncKeyBundle);
|
|
ok(
|
|
(await serverKeys.upload(Service.resource(Service.cryptoKeysURL))).success
|
|
);
|
|
|
|
_("Sync.");
|
|
await syncClientsEngine(server);
|
|
|
|
_("Old record " + oldLocalID + " was deleted, new one uploaded.");
|
|
check_clients_count(1);
|
|
check_client_deleted(oldLocalID);
|
|
|
|
_(
|
|
"Now change our keys but don't upload them. " +
|
|
"That means we get an HMAC error but redownload keys."
|
|
);
|
|
Service.lastHMACEvent = 0;
|
|
engine.localID = Utils.makeGUID();
|
|
await engine.resetClient();
|
|
await generateNewKeys(Service.collectionKeys);
|
|
deletedCollections = [];
|
|
deletedItems = [];
|
|
check_clients_count(1);
|
|
await syncClientsEngine(server);
|
|
|
|
_("Old record was not deleted, new one uploaded.");
|
|
equal(deletedCollections.length, 0);
|
|
equal(deletedItems.length, 0);
|
|
check_clients_count(2);
|
|
|
|
_(
|
|
"Now try the scenario where our keys are wrong *and* there's a bad record."
|
|
);
|
|
// Clean up and start fresh.
|
|
user.collection("clients")._wbos = {};
|
|
Service.lastHMACEvent = 0;
|
|
engine.localID = Utils.makeGUID();
|
|
await engine.resetClient();
|
|
deletedCollections = [];
|
|
deletedItems = [];
|
|
check_clients_count(0);
|
|
|
|
await uploadNewKeys();
|
|
|
|
// Sync once to upload a record.
|
|
await syncClientsEngine(server);
|
|
check_clients_count(1);
|
|
|
|
// Generate and upload new keys, so the old client record is wrong.
|
|
await uploadNewKeys();
|
|
|
|
// Create a new client record and new keys. Now our keys are wrong, as well
|
|
// as the object on the server. We'll download the new keys and also delete
|
|
// the bad client record.
|
|
oldLocalID = engine.localID; // Preserve to test for deletion!
|
|
engine.localID = Utils.makeGUID();
|
|
await engine.resetClient();
|
|
await generateNewKeys(Service.collectionKeys);
|
|
let oldKey = Service.collectionKeys.keyForCollection();
|
|
|
|
equal(deletedCollections.length, 0);
|
|
equal(deletedItems.length, 0);
|
|
await syncClientsEngine(server);
|
|
equal(deletedItems.length, 1);
|
|
check_client_deleted(oldLocalID);
|
|
check_clients_count(1);
|
|
let newKey = Service.collectionKeys.keyForCollection();
|
|
ok(!oldKey.equals(newKey));
|
|
} finally {
|
|
await cleanup();
|
|
await promiseStopServer(server);
|
|
}
|
|
});
|
|
|
|
add_task(async function test_properties() {
|
|
_("Test lastRecordUpload property");
|
|
try {
|
|
equal(
|
|
Svc.PrefBranch.getPrefType("clients.lastRecordUpload"),
|
|
Ci.nsIPrefBranch.PREF_INVALID
|
|
);
|
|
equal(engine.lastRecordUpload, 0);
|
|
|
|
let now = Date.now();
|
|
engine.lastRecordUpload = now / 1000;
|
|
equal(engine.lastRecordUpload, Math.floor(now / 1000));
|
|
} finally {
|
|
await cleanup();
|
|
}
|
|
});
|
|
|
|
add_task(async function test_full_sync() {
|
|
_("Ensure that Clients engine fetches all records for each sync.");
|
|
|
|
let now = new_timestamp();
|
|
let server = await serverForFoo(engine);
|
|
let user = server.user("foo");
|
|
|
|
await SyncTestingInfrastructure(server);
|
|
await generateNewKeys(Service.collectionKeys);
|
|
|
|
let activeID = Utils.makeGUID();
|
|
user.collection("clients").insertRecord(
|
|
{
|
|
id: activeID,
|
|
name: "Active client",
|
|
type: "desktop",
|
|
commands: [],
|
|
version: "48",
|
|
protocols: ["1.5"],
|
|
},
|
|
now - 10
|
|
);
|
|
|
|
let deletedID = Utils.makeGUID();
|
|
user.collection("clients").insertRecord(
|
|
{
|
|
id: deletedID,
|
|
name: "Client to delete",
|
|
type: "desktop",
|
|
commands: [],
|
|
version: "48",
|
|
protocols: ["1.5"],
|
|
},
|
|
now - 10
|
|
);
|
|
|
|
try {
|
|
let store = engine._store;
|
|
|
|
_("First sync. 2 records downloaded; our record uploaded.");
|
|
strictEqual(engine.lastRecordUpload, 0);
|
|
ok(engine.isFirstSync);
|
|
await syncClientsEngine(server);
|
|
ok(engine.lastRecordUpload > 0);
|
|
ok(!engine.isFirstSync);
|
|
deepEqual(
|
|
user.collection("clients").keys().sort(),
|
|
[activeID, deletedID, engine.localID].sort(),
|
|
"Our record should be uploaded on first sync"
|
|
);
|
|
let ids = await store.getAllIDs();
|
|
deepEqual(
|
|
Object.keys(ids).sort(),
|
|
[activeID, deletedID, engine.localID].sort(),
|
|
"Other clients should be downloaded on first sync"
|
|
);
|
|
|
|
_("Delete a record, then sync again");
|
|
let collection = server.getCollection("foo", "clients");
|
|
collection.remove(deletedID);
|
|
// Simulate a timestamp update in info/collections.
|
|
await syncClientsEngine(server);
|
|
|
|
_("Record should be updated");
|
|
ids = await store.getAllIDs();
|
|
deepEqual(
|
|
Object.keys(ids).sort(),
|
|
[activeID, engine.localID].sort(),
|
|
"Deleted client should be removed on next sync"
|
|
);
|
|
} finally {
|
|
await cleanup();
|
|
|
|
try {
|
|
server.deleteCollections("foo");
|
|
} finally {
|
|
await promiseStopServer(server);
|
|
}
|
|
}
|
|
});
|
|
|
|
add_task(async function test_sync() {
|
|
_("Ensure that Clients engine uploads a new client record once a week.");
|
|
|
|
let server = await serverForFoo(engine);
|
|
let user = server.user("foo");
|
|
|
|
await SyncTestingInfrastructure(server);
|
|
await generateNewKeys(Service.collectionKeys);
|
|
|
|
function clientWBO() {
|
|
return user.collection("clients").wbo(engine.localID);
|
|
}
|
|
|
|
try {
|
|
_("First sync. Client record is uploaded.");
|
|
equal(clientWBO(), undefined);
|
|
equal(engine.lastRecordUpload, 0);
|
|
ok(engine.isFirstSync);
|
|
await syncClientsEngine(server);
|
|
ok(!!clientWBO().payload);
|
|
ok(engine.lastRecordUpload > 0);
|
|
ok(!engine.isFirstSync);
|
|
|
|
_(
|
|
"Let's time travel more than a week back, new record should've been uploaded."
|
|
);
|
|
engine.lastRecordUpload -= MORE_THAN_CLIENTS_TTL_REFRESH;
|
|
let lastweek = engine.lastRecordUpload;
|
|
clientWBO().payload = undefined;
|
|
await syncClientsEngine(server);
|
|
ok(!!clientWBO().payload);
|
|
ok(engine.lastRecordUpload > lastweek);
|
|
ok(!engine.isFirstSync);
|
|
|
|
_("Remove client record.");
|
|
await engine.removeClientData();
|
|
equal(clientWBO().payload, undefined);
|
|
|
|
_("Time travel one day back, no record uploaded.");
|
|
engine.lastRecordUpload -= LESS_THAN_CLIENTS_TTL_REFRESH;
|
|
let yesterday = engine.lastRecordUpload;
|
|
await syncClientsEngine(server);
|
|
equal(clientWBO().payload, undefined);
|
|
equal(engine.lastRecordUpload, yesterday);
|
|
ok(!engine.isFirstSync);
|
|
} finally {
|
|
await cleanup();
|
|
await promiseStopServer(server);
|
|
}
|
|
});
|
|
|
|
add_task(async function test_client_name_change() {
|
|
_("Ensure client name change incurs a client record update.");
|
|
|
|
let tracker = engine._tracker;
|
|
|
|
engine.localID; // Needed to increase the tracker changedIDs count.
|
|
let initialName = engine.localName;
|
|
|
|
tracker.start();
|
|
_("initial name: " + initialName);
|
|
|
|
// Tracker already has data, so clear it.
|
|
await tracker.clearChangedIDs();
|
|
|
|
let initialScore = tracker.score;
|
|
|
|
let changedIDs = await tracker.getChangedIDs();
|
|
equal(Object.keys(changedIDs).length, 0);
|
|
|
|
Services.prefs.setStringPref(
|
|
"identity.fxaccounts.account.device.name",
|
|
"new name"
|
|
);
|
|
await tracker.asyncObserver.promiseObserversComplete();
|
|
|
|
_("new name: " + engine.localName);
|
|
notEqual(initialName, engine.localName);
|
|
changedIDs = await tracker.getChangedIDs();
|
|
equal(Object.keys(changedIDs).length, 1);
|
|
ok(engine.localID in changedIDs);
|
|
ok(tracker.score > initialScore);
|
|
ok(tracker.score >= SCORE_INCREMENT_XLARGE);
|
|
|
|
await tracker.stop();
|
|
|
|
await cleanup();
|
|
});
|
|
|
|
add_task(async function test_fxa_device_id_change() {
|
|
_("Ensure an FxA device ID change incurs a client record update.");
|
|
|
|
let tracker = engine._tracker;
|
|
|
|
engine.localID; // Needed to increase the tracker changedIDs count.
|
|
|
|
tracker.start();
|
|
|
|
// Tracker already has data, so clear it.
|
|
await tracker.clearChangedIDs();
|
|
|
|
let initialScore = tracker.score;
|
|
|
|
let changedIDs = await tracker.getChangedIDs();
|
|
equal(Object.keys(changedIDs).length, 0);
|
|
|
|
Services.obs.notifyObservers(null, "fxaccounts:new_device_id");
|
|
await tracker.asyncObserver.promiseObserversComplete();
|
|
|
|
changedIDs = await tracker.getChangedIDs();
|
|
equal(Object.keys(changedIDs).length, 1);
|
|
ok(engine.localID in changedIDs);
|
|
ok(tracker.score > initialScore);
|
|
ok(tracker.score >= SINGLE_USER_THRESHOLD);
|
|
|
|
await tracker.stop();
|
|
|
|
await cleanup();
|
|
});
|
|
|
|
add_task(async function test_last_modified() {
|
|
_("Ensure that remote records have a sane serverLastModified attribute.");
|
|
|
|
let now = new_timestamp();
|
|
let server = await serverForFoo(engine);
|
|
let user = server.user("foo");
|
|
|
|
await SyncTestingInfrastructure(server);
|
|
await generateNewKeys(Service.collectionKeys);
|
|
|
|
let activeID = Utils.makeGUID();
|
|
user.collection("clients").insertRecord(
|
|
{
|
|
id: activeID,
|
|
name: "Active client",
|
|
type: "desktop",
|
|
commands: [],
|
|
version: "48",
|
|
protocols: ["1.5"],
|
|
},
|
|
now - 10
|
|
);
|
|
|
|
try {
|
|
let collection = user.collection("clients");
|
|
|
|
_("Sync to download the record");
|
|
await syncClientsEngine(server);
|
|
|
|
equal(
|
|
engine._store._remoteClients[activeID].serverLastModified,
|
|
now - 10,
|
|
"last modified in the local record is correctly the server last-modified"
|
|
);
|
|
|
|
_("Modify the record and re-upload it");
|
|
// set a new name to make sure we really did upload.
|
|
engine._store._remoteClients[activeID].name = "New name";
|
|
engine._modified.set(activeID, 0);
|
|
// The sync above also did a POST, so adjust our lastModified.
|
|
engine.lastModified = server.getCollection("foo", "clients").timestamp;
|
|
await engine._uploadOutgoing();
|
|
|
|
_("Local record should have updated timestamp");
|
|
ok(engine._store._remoteClients[activeID].serverLastModified >= now);
|
|
|
|
_("Record on the server should have new name but not serverLastModified");
|
|
let payload = collection.cleartext(activeID);
|
|
equal(payload.name, "New name");
|
|
equal(payload.serverLastModified, undefined);
|
|
} finally {
|
|
await cleanup();
|
|
server.deleteCollections("foo");
|
|
await promiseStopServer(server);
|
|
}
|
|
});
|
|
|
|
add_task(async function test_send_command() {
|
|
_("Verifies _sendCommandToClient puts commands in the outbound queue.");
|
|
|
|
let store = engine._store;
|
|
let tracker = engine._tracker;
|
|
let remoteId = Utils.makeGUID();
|
|
let rec = new ClientsRec("clients", remoteId);
|
|
|
|
await store.create(rec);
|
|
await store.createRecord(remoteId, "clients");
|
|
|
|
let action = "testCommand";
|
|
let args = ["foo", "bar"];
|
|
let extra = { flowID: "flowy" };
|
|
|
|
await engine._sendCommandToClient(action, args, remoteId, extra);
|
|
|
|
let newRecord = store._remoteClients[remoteId];
|
|
let clientCommands = (await engine._readCommands())[remoteId];
|
|
notEqual(newRecord, undefined);
|
|
equal(clientCommands.length, 1);
|
|
|
|
let command = clientCommands[0];
|
|
equal(command.command, action);
|
|
equal(command.args.length, 2);
|
|
deepEqual(command.args, args);
|
|
ok(command.flowID);
|
|
|
|
const changes = await tracker.getChangedIDs();
|
|
notEqual(changes[remoteId], undefined);
|
|
|
|
await cleanup();
|
|
});
|
|
|
|
// The browser UI might call _addClientCommand indirectly without awaiting on the returned promise.
|
|
// We need to make sure this doesn't result on commands not being saved.
|
|
add_task(async function test_add_client_command_race() {
|
|
let promises = [];
|
|
for (let i = 0; i < 100; i++) {
|
|
promises.push(
|
|
engine._addClientCommand(`client-${i}`, { command: "cmd", args: [] })
|
|
);
|
|
}
|
|
await Promise.all(promises);
|
|
|
|
let localCommands = await engine._readCommands();
|
|
for (let i = 0; i < 100; i++) {
|
|
equal(localCommands[`client-${i}`].length, 1);
|
|
}
|
|
});
|
|
|
|
add_task(async function test_command_validation() {
|
|
_("Verifies that command validation works properly.");
|
|
|
|
let store = engine._store;
|
|
|
|
let testCommands = [
|
|
["resetAll", [], true],
|
|
["resetAll", ["foo"], false],
|
|
["resetEngine", ["tabs"], true],
|
|
["resetEngine", [], false],
|
|
["wipeEngine", ["tabs"], true],
|
|
["wipeEngine", [], false],
|
|
["logout", [], true],
|
|
["logout", ["foo"], false],
|
|
["__UNKNOWN__", [], false],
|
|
];
|
|
|
|
for (let [action, args, expectedResult] of testCommands) {
|
|
let remoteId = Utils.makeGUID();
|
|
let rec = new ClientsRec("clients", remoteId);
|
|
|
|
await store.create(rec);
|
|
await store.createRecord(remoteId, "clients");
|
|
|
|
await engine.sendCommand(action, args, remoteId);
|
|
|
|
let newRecord = store._remoteClients[remoteId];
|
|
notEqual(newRecord, undefined);
|
|
|
|
let clientCommands = (await engine._readCommands())[remoteId];
|
|
|
|
if (expectedResult) {
|
|
_("Ensuring command is sent: " + action);
|
|
equal(clientCommands.length, 1);
|
|
|
|
let command = clientCommands[0];
|
|
equal(command.command, action);
|
|
deepEqual(command.args, args);
|
|
|
|
notEqual(engine._tracker, undefined);
|
|
const changes = await engine._tracker.getChangedIDs();
|
|
notEqual(changes[remoteId], undefined);
|
|
} else {
|
|
_("Ensuring command is scrubbed: " + action);
|
|
equal(clientCommands, undefined);
|
|
|
|
if (store._tracker) {
|
|
equal(engine._tracker[remoteId], undefined);
|
|
}
|
|
}
|
|
}
|
|
await cleanup();
|
|
});
|
|
|
|
add_task(async function test_command_duplication() {
|
|
_("Ensures duplicate commands are detected and not added");
|
|
|
|
let store = engine._store;
|
|
let remoteId = Utils.makeGUID();
|
|
let rec = new ClientsRec("clients", remoteId);
|
|
await store.create(rec);
|
|
await store.createRecord(remoteId, "clients");
|
|
|
|
let action = "resetAll";
|
|
let args = [];
|
|
|
|
await engine.sendCommand(action, args, remoteId);
|
|
await engine.sendCommand(action, args, remoteId);
|
|
|
|
let clientCommands = (await engine._readCommands())[remoteId];
|
|
equal(clientCommands.length, 1);
|
|
|
|
_("Check variant args length");
|
|
await engine._saveCommands({});
|
|
|
|
action = "resetEngine";
|
|
await engine.sendCommand(action, [{ x: "foo" }], remoteId);
|
|
await engine.sendCommand(action, [{ x: "bar" }], remoteId);
|
|
|
|
_("Make sure we spot a real dupe argument.");
|
|
await engine.sendCommand(action, [{ x: "bar" }], remoteId);
|
|
|
|
clientCommands = (await engine._readCommands())[remoteId];
|
|
equal(clientCommands.length, 2);
|
|
|
|
await cleanup();
|
|
});
|
|
|
|
add_task(async function test_command_invalid_client() {
|
|
_("Ensures invalid client IDs are caught");
|
|
|
|
let id = Utils.makeGUID();
|
|
let error;
|
|
|
|
try {
|
|
await engine.sendCommand("wipeEngine", ["tabs"], id);
|
|
} catch (ex) {
|
|
error = ex;
|
|
}
|
|
|
|
equal(error.message.indexOf("Unknown remote client ID: "), 0);
|
|
|
|
await cleanup();
|
|
});
|
|
|
|
add_task(async function test_process_incoming_commands() {
|
|
_("Ensures local commands are executed");
|
|
|
|
engine.localCommands = [{ command: "logout", args: [] }];
|
|
|
|
let ev = "weave:service:logout:finish";
|
|
|
|
let logoutPromise = new Promise(resolve => {
|
|
var handler = function () {
|
|
Svc.Obs.remove(ev, handler);
|
|
|
|
resolve();
|
|
};
|
|
|
|
Svc.Obs.add(ev, handler);
|
|
});
|
|
|
|
// logout command causes processIncomingCommands to return explicit false.
|
|
ok(!(await engine.processIncomingCommands()));
|
|
|
|
await logoutPromise;
|
|
|
|
await cleanup();
|
|
});
|
|
|
|
add_task(async function test_filter_duplicate_names() {
|
|
_(
|
|
"Ensure that we exclude clients with identical names that haven't synced in a week."
|
|
);
|
|
|
|
let now = new_timestamp();
|
|
let server = await serverForFoo(engine);
|
|
let user = server.user("foo");
|
|
|
|
await SyncTestingInfrastructure(server);
|
|
await generateNewKeys(Service.collectionKeys);
|
|
|
|
// Synced recently.
|
|
let recentID = Utils.makeGUID();
|
|
user.collection("clients").insertRecord(
|
|
{
|
|
id: recentID,
|
|
name: "My Phone",
|
|
type: "mobile",
|
|
commands: [],
|
|
version: "48",
|
|
protocols: ["1.5"],
|
|
},
|
|
now - 10
|
|
);
|
|
|
|
// Dupe of our client, synced more than 1 week ago.
|
|
let dupeID = Utils.makeGUID();
|
|
user.collection("clients").insertRecord(
|
|
{
|
|
id: dupeID,
|
|
name: engine.localName,
|
|
type: "desktop",
|
|
commands: [],
|
|
version: "48",
|
|
protocols: ["1.5"],
|
|
},
|
|
now - 604820
|
|
);
|
|
|
|
// Synced more than 1 week ago, but not a dupe.
|
|
let oldID = Utils.makeGUID();
|
|
user.collection("clients").insertRecord(
|
|
{
|
|
id: oldID,
|
|
name: "My old desktop",
|
|
type: "desktop",
|
|
commands: [],
|
|
version: "48",
|
|
protocols: ["1.5"],
|
|
},
|
|
now - 604820
|
|
);
|
|
|
|
try {
|
|
let store = engine._store;
|
|
|
|
_("First sync");
|
|
strictEqual(engine.lastRecordUpload, 0);
|
|
ok(engine.isFirstSync);
|
|
await syncClientsEngine(server);
|
|
ok(engine.lastRecordUpload > 0);
|
|
ok(!engine.isFirstSync);
|
|
deepEqual(
|
|
user.collection("clients").keys().sort(),
|
|
[recentID, dupeID, oldID, engine.localID].sort(),
|
|
"Our record should be uploaded on first sync"
|
|
);
|
|
|
|
let ids = await store.getAllIDs();
|
|
deepEqual(
|
|
Object.keys(ids).sort(),
|
|
[recentID, dupeID, oldID, engine.localID].sort(),
|
|
"Duplicate ID should remain in getAllIDs"
|
|
);
|
|
ok(
|
|
await engine._store.itemExists(dupeID),
|
|
"Dupe ID should be considered as existing for Sync methods."
|
|
);
|
|
ok(
|
|
!engine.remoteClientExists(dupeID),
|
|
"Dupe ID should not be considered as existing for external methods."
|
|
);
|
|
|
|
// dupe desktop should not appear in .deviceTypes.
|
|
equal(engine.deviceTypes.get("desktop"), 2);
|
|
equal(engine.deviceTypes.get("mobile"), 1);
|
|
|
|
// dupe desktop should not appear in stats
|
|
deepEqual(engine.stats, {
|
|
hasMobile: 1,
|
|
names: [engine.localName, "My Phone", "My old desktop"],
|
|
numClients: 3,
|
|
});
|
|
|
|
ok(engine.remoteClientExists(oldID), "non-dupe ID should exist.");
|
|
ok(!engine.remoteClientExists(dupeID), "dupe ID should not exist");
|
|
equal(
|
|
engine.remoteClients.length,
|
|
2,
|
|
"dupe should not be in remoteClients"
|
|
);
|
|
|
|
// Check that a subsequent Sync doesn't report anything as being processed.
|
|
let counts;
|
|
Svc.Obs.add("weave:engine:sync:applied", function observe(subject, data) {
|
|
Svc.Obs.remove("weave:engine:sync:applied", observe);
|
|
counts = subject;
|
|
});
|
|
|
|
await syncClientsEngine(server);
|
|
equal(counts.applied, 0); // We didn't report applying any records.
|
|
equal(counts.reconciled, 4); // We reported reconcilliation for all records
|
|
equal(counts.succeeded, 0);
|
|
equal(counts.failed, 0);
|
|
equal(counts.newFailed, 0);
|
|
|
|
_("Broadcast logout to all clients");
|
|
await engine.sendCommand("logout", []);
|
|
await syncClientsEngine(server);
|
|
|
|
let collection = server.getCollection("foo", "clients");
|
|
let recentPayload = collection.cleartext(recentID);
|
|
compareCommands(
|
|
recentPayload.commands,
|
|
[{ command: "logout", args: [] }],
|
|
"Should send commands to the recent client"
|
|
);
|
|
|
|
let oldPayload = collection.cleartext(oldID);
|
|
compareCommands(
|
|
oldPayload.commands,
|
|
[{ command: "logout", args: [] }],
|
|
"Should send commands to the week-old client"
|
|
);
|
|
|
|
let dupePayload = collection.cleartext(dupeID);
|
|
deepEqual(
|
|
dupePayload.commands,
|
|
[],
|
|
"Should not send commands to the dupe client"
|
|
);
|
|
|
|
_("Update the dupe client's modified time");
|
|
collection.insertRecord(
|
|
{
|
|
id: dupeID,
|
|
name: engine.localName,
|
|
type: "desktop",
|
|
commands: [],
|
|
version: "48",
|
|
protocols: ["1.5"],
|
|
},
|
|
now - 10
|
|
);
|
|
|
|
_("Second sync.");
|
|
await syncClientsEngine(server);
|
|
|
|
ids = await store.getAllIDs();
|
|
deepEqual(
|
|
Object.keys(ids).sort(),
|
|
[recentID, oldID, dupeID, engine.localID].sort(),
|
|
"Stale client synced, so it should no longer be marked as a dupe"
|
|
);
|
|
|
|
ok(
|
|
engine.remoteClientExists(dupeID),
|
|
"Dupe ID should appear as it synced."
|
|
);
|
|
|
|
// Recently synced dupe desktop should appear in .deviceTypes.
|
|
equal(engine.deviceTypes.get("desktop"), 3);
|
|
|
|
// Recently synced dupe desktop should now appear in stats
|
|
deepEqual(engine.stats, {
|
|
hasMobile: 1,
|
|
names: [engine.localName, "My Phone", engine.localName, "My old desktop"],
|
|
numClients: 4,
|
|
});
|
|
|
|
ok(
|
|
engine.remoteClientExists(dupeID),
|
|
"recently synced dupe ID should now exist"
|
|
);
|
|
equal(
|
|
engine.remoteClients.length,
|
|
3,
|
|
"recently synced dupe should now be in remoteClients"
|
|
);
|
|
} finally {
|
|
await cleanup();
|
|
|
|
try {
|
|
server.deleteCollections("foo");
|
|
} finally {
|
|
await promiseStopServer(server);
|
|
}
|
|
}
|
|
});
|
|
|
|
add_task(async function test_command_sync() {
|
|
_("Ensure that commands are synced across clients.");
|
|
|
|
await engine._store.wipe();
|
|
await generateNewKeys(Service.collectionKeys);
|
|
|
|
let server = await serverForFoo(engine);
|
|
await SyncTestingInfrastructure(server);
|
|
|
|
let user = server.user("foo");
|
|
let remoteId = Utils.makeGUID();
|
|
|
|
function clientWBO(id) {
|
|
return user.collection("clients").wbo(id);
|
|
}
|
|
|
|
_("Create remote client record");
|
|
user.collection("clients").insertRecord({
|
|
id: remoteId,
|
|
name: "Remote client",
|
|
type: "desktop",
|
|
commands: [],
|
|
version: "48",
|
|
protocols: ["1.5"],
|
|
});
|
|
|
|
try {
|
|
_("Syncing.");
|
|
await syncClientsEngine(server);
|
|
|
|
_("Checking remote record was downloaded.");
|
|
let clientRecord = engine._store._remoteClients[remoteId];
|
|
notEqual(clientRecord, undefined);
|
|
equal(clientRecord.commands.length, 0);
|
|
|
|
_("Send a command to the remote client.");
|
|
await engine.sendCommand("wipeEngine", ["tabs"]);
|
|
let clientCommands = (await engine._readCommands())[remoteId];
|
|
equal(clientCommands.length, 1);
|
|
await syncClientsEngine(server);
|
|
|
|
_("Checking record was uploaded.");
|
|
notEqual(clientWBO(engine.localID).payload, undefined);
|
|
ok(engine.lastRecordUpload > 0);
|
|
ok(!engine.isFirstSync);
|
|
|
|
notEqual(clientWBO(remoteId).payload, undefined);
|
|
|
|
Svc.PrefBranch.setStringPref("client.GUID", remoteId);
|
|
await engine._resetClient();
|
|
equal(engine.localID, remoteId);
|
|
_("Performing sync on resetted client.");
|
|
await syncClientsEngine(server);
|
|
notEqual(engine.localCommands, undefined);
|
|
equal(engine.localCommands.length, 1);
|
|
|
|
let command = engine.localCommands[0];
|
|
equal(command.command, "wipeEngine");
|
|
equal(command.args.length, 1);
|
|
equal(command.args[0], "tabs");
|
|
} finally {
|
|
await cleanup();
|
|
|
|
try {
|
|
let collection = server.getCollection("foo", "clients");
|
|
collection.remove(remoteId);
|
|
} finally {
|
|
await promiseStopServer(server);
|
|
}
|
|
}
|
|
});
|
|
|
|
add_task(async function test_clients_not_in_fxa_list() {
|
|
_("Ensure that clients not in the FxA devices list are marked as stale.");
|
|
|
|
await engine._store.wipe();
|
|
await generateNewKeys(Service.collectionKeys);
|
|
|
|
let server = await serverForFoo(engine);
|
|
await SyncTestingInfrastructure(server);
|
|
|
|
let remoteId = Utils.makeGUID();
|
|
let remoteId2 = Utils.makeGUID();
|
|
let collection = server.getCollection("foo", "clients");
|
|
|
|
_("Create remote client records");
|
|
collection.insertRecord({
|
|
id: remoteId,
|
|
name: "Remote client",
|
|
type: "desktop",
|
|
commands: [],
|
|
version: "48",
|
|
fxaDeviceId: remoteId,
|
|
protocols: ["1.5"],
|
|
});
|
|
|
|
collection.insertRecord({
|
|
id: remoteId2,
|
|
name: "Remote client 2",
|
|
type: "desktop",
|
|
commands: [],
|
|
version: "48",
|
|
fxaDeviceId: remoteId2,
|
|
protocols: ["1.5"],
|
|
});
|
|
|
|
let fxAccounts = engine.fxAccounts;
|
|
engine.fxAccounts = {
|
|
notifyDevices() {
|
|
return Promise.resolve(true);
|
|
},
|
|
device: {
|
|
getLocalId() {
|
|
return fxAccounts.device.getLocalId();
|
|
},
|
|
getLocalName() {
|
|
return fxAccounts.device.getLocalName();
|
|
},
|
|
getLocalType() {
|
|
return fxAccounts.device.getLocalType();
|
|
},
|
|
recentDeviceList: [{ id: remoteId }],
|
|
refreshDeviceList() {
|
|
return Promise.resolve(true);
|
|
},
|
|
},
|
|
_internal: {
|
|
now() {
|
|
return Date.now();
|
|
},
|
|
},
|
|
};
|
|
|
|
try {
|
|
_("Syncing.");
|
|
await syncClientsEngine(server);
|
|
|
|
ok(!engine._store._remoteClients[remoteId].stale);
|
|
ok(engine._store._remoteClients[remoteId2].stale);
|
|
} finally {
|
|
engine.fxAccounts = fxAccounts;
|
|
await cleanup();
|
|
|
|
try {
|
|
collection.remove(remoteId);
|
|
} finally {
|
|
await promiseStopServer(server);
|
|
}
|
|
}
|
|
});
|
|
|
|
add_task(async function test_dupe_device_ids() {
|
|
_(
|
|
"Ensure that we mark devices with duplicate fxaDeviceIds but older lastModified as stale."
|
|
);
|
|
|
|
await engine._store.wipe();
|
|
await generateNewKeys(Service.collectionKeys);
|
|
|
|
let server = await serverForFoo(engine);
|
|
await SyncTestingInfrastructure(server);
|
|
|
|
let remoteId = Utils.makeGUID();
|
|
let remoteId2 = Utils.makeGUID();
|
|
let remoteDeviceId = Utils.makeGUID();
|
|
|
|
let collection = server.getCollection("foo", "clients");
|
|
|
|
_("Create remote client records");
|
|
collection.insertRecord(
|
|
{
|
|
id: remoteId,
|
|
name: "Remote client",
|
|
type: "desktop",
|
|
commands: [],
|
|
version: "48",
|
|
fxaDeviceId: remoteDeviceId,
|
|
protocols: ["1.5"],
|
|
},
|
|
new_timestamp() - 3
|
|
);
|
|
collection.insertRecord({
|
|
id: remoteId2,
|
|
name: "Remote client",
|
|
type: "desktop",
|
|
commands: [],
|
|
version: "48",
|
|
fxaDeviceId: remoteDeviceId,
|
|
protocols: ["1.5"],
|
|
});
|
|
|
|
let fxAccounts = engine.fxAccounts;
|
|
engine.fxAccounts = {
|
|
notifyDevices() {
|
|
return Promise.resolve(true);
|
|
},
|
|
device: {
|
|
getLocalId() {
|
|
return fxAccounts.device.getLocalId();
|
|
},
|
|
getLocalName() {
|
|
return fxAccounts.device.getLocalName();
|
|
},
|
|
getLocalType() {
|
|
return fxAccounts.device.getLocalType();
|
|
},
|
|
recentDeviceList: [{ id: remoteDeviceId }],
|
|
refreshDeviceList() {
|
|
return Promise.resolve(true);
|
|
},
|
|
},
|
|
_internal: {
|
|
now() {
|
|
return Date.now();
|
|
},
|
|
},
|
|
};
|
|
|
|
try {
|
|
_("Syncing.");
|
|
await syncClientsEngine(server);
|
|
|
|
ok(engine._store._remoteClients[remoteId].stale);
|
|
ok(!engine._store._remoteClients[remoteId2].stale);
|
|
} finally {
|
|
engine.fxAccounts = fxAccounts;
|
|
await cleanup();
|
|
|
|
try {
|
|
collection.remove(remoteId);
|
|
} finally {
|
|
await promiseStopServer(server);
|
|
}
|
|
}
|
|
});
|
|
|
|
add_task(async function test_refresh_fxa_device_list() {
|
|
_("Ensure we refresh the fxa device list when we expect to.");
|
|
|
|
await engine._store.wipe();
|
|
engine._lastFxaDeviceRefresh = 0;
|
|
await generateNewKeys(Service.collectionKeys);
|
|
|
|
let server = await serverForFoo(engine);
|
|
await SyncTestingInfrastructure(server);
|
|
|
|
let numRefreshes = 0;
|
|
let now = Date.now();
|
|
let fxAccounts = engine.fxAccounts;
|
|
engine.fxAccounts = {
|
|
notifyDevices() {
|
|
return Promise.resolve(true);
|
|
},
|
|
device: {
|
|
getLocalId() {
|
|
return fxAccounts.device.getLocalId();
|
|
},
|
|
getLocalName() {
|
|
return fxAccounts.device.getLocalName();
|
|
},
|
|
getLocalType() {
|
|
return fxAccounts.device.getLocalType();
|
|
},
|
|
recentDeviceList: [],
|
|
refreshDeviceList() {
|
|
numRefreshes += 1;
|
|
return Promise.resolve(true);
|
|
},
|
|
},
|
|
_internal: {
|
|
now() {
|
|
return now;
|
|
},
|
|
},
|
|
};
|
|
|
|
try {
|
|
_("Syncing.");
|
|
await syncClientsEngine(server);
|
|
Assert.equal(numRefreshes, 1, "first sync should refresh");
|
|
now += 1000; // a second later.
|
|
await syncClientsEngine(server);
|
|
Assert.equal(numRefreshes, 1, "next sync should not refresh");
|
|
now += 60 * 60 * 2 * 1000; // 2 hours later
|
|
await syncClientsEngine(server);
|
|
Assert.equal(numRefreshes, 2, "2 hours later should refresh");
|
|
now += 1000; // a second later.
|
|
Assert.equal(numRefreshes, 2, "next sync should not refresh");
|
|
} finally {
|
|
await cleanup();
|
|
await promiseStopServer(server);
|
|
}
|
|
});
|
|
|
|
add_task(async function test_optional_client_fields() {
|
|
_("Ensure that we produce records with the fields added in Bug 1097222.");
|
|
|
|
const SUPPORTED_PROTOCOL_VERSIONS = ["1.5"];
|
|
let local = await engine._store.createRecord(engine.localID, "clients");
|
|
equal(local.name, engine.localName);
|
|
equal(local.type, engine.localType);
|
|
equal(local.version, Services.appinfo.version);
|
|
deepEqual(local.protocols, SUPPORTED_PROTOCOL_VERSIONS);
|
|
|
|
// Optional fields.
|
|
// Make sure they're what they ought to be...
|
|
equal(local.os, Services.appinfo.OS);
|
|
equal(local.appPackage, Services.appinfo.ID);
|
|
|
|
// ... and also that they're non-empty.
|
|
ok(!!local.os);
|
|
ok(!!local.appPackage);
|
|
ok(!!local.application);
|
|
|
|
// We don't currently populate device or formfactor.
|
|
// See Bug 1100722, Bug 1100723.
|
|
|
|
await cleanup();
|
|
});
|
|
|
|
add_task(async function test_merge_commands() {
|
|
_("Verifies local commands for remote clients are merged with the server's");
|
|
|
|
let now = new_timestamp();
|
|
let server = await serverForFoo(engine);
|
|
await SyncTestingInfrastructure(server);
|
|
await generateNewKeys(Service.collectionKeys);
|
|
|
|
let collection = server.getCollection("foo", "clients");
|
|
|
|
let desktopID = Utils.makeGUID();
|
|
collection.insertRecord(
|
|
{
|
|
id: desktopID,
|
|
name: "Desktop client",
|
|
type: "desktop",
|
|
commands: [
|
|
{
|
|
command: "wipeEngine",
|
|
args: ["history"],
|
|
flowID: Utils.makeGUID(),
|
|
},
|
|
],
|
|
version: "48",
|
|
protocols: ["1.5"],
|
|
},
|
|
now - 10
|
|
);
|
|
|
|
let mobileID = Utils.makeGUID();
|
|
collection.insertRecord(
|
|
{
|
|
id: mobileID,
|
|
name: "Mobile client",
|
|
type: "mobile",
|
|
commands: [
|
|
{
|
|
command: "logout",
|
|
args: [],
|
|
flowID: Utils.makeGUID(),
|
|
},
|
|
],
|
|
version: "48",
|
|
protocols: ["1.5"],
|
|
},
|
|
now - 10
|
|
);
|
|
|
|
try {
|
|
_("First sync. 2 records downloaded.");
|
|
strictEqual(engine.lastRecordUpload, 0);
|
|
ok(engine.isFirstSync);
|
|
await syncClientsEngine(server);
|
|
|
|
_("Broadcast logout to all clients");
|
|
await engine.sendCommand("logout", []);
|
|
await syncClientsEngine(server);
|
|
|
|
let desktopPayload = collection.cleartext(desktopID);
|
|
compareCommands(
|
|
desktopPayload.commands,
|
|
[
|
|
{
|
|
command: "wipeEngine",
|
|
args: ["history"],
|
|
},
|
|
{
|
|
command: "logout",
|
|
args: [],
|
|
},
|
|
],
|
|
"Should send the logout command to the desktop client"
|
|
);
|
|
|
|
let mobilePayload = collection.cleartext(mobileID);
|
|
compareCommands(
|
|
mobilePayload.commands,
|
|
[{ command: "logout", args: [] }],
|
|
"Should not send a duplicate logout to the mobile client"
|
|
);
|
|
} finally {
|
|
await cleanup();
|
|
|
|
try {
|
|
server.deleteCollections("foo");
|
|
} finally {
|
|
await promiseStopServer(server);
|
|
}
|
|
}
|
|
});
|
|
|
|
add_task(async function test_duplicate_remote_commands() {
|
|
_(
|
|
"Verifies local commands for remote clients are sent only once (bug 1289287)"
|
|
);
|
|
|
|
let now = new_timestamp();
|
|
let server = await serverForFoo(engine);
|
|
|
|
await SyncTestingInfrastructure(server);
|
|
await generateNewKeys(Service.collectionKeys);
|
|
|
|
let collection = server.getCollection("foo", "clients");
|
|
|
|
let desktopID = Utils.makeGUID();
|
|
collection.insertRecord(
|
|
{
|
|
id: desktopID,
|
|
name: "Desktop client",
|
|
type: "desktop",
|
|
commands: [],
|
|
version: "48",
|
|
protocols: ["1.5"],
|
|
},
|
|
now - 10
|
|
);
|
|
|
|
try {
|
|
_("First sync. 1 record downloaded.");
|
|
strictEqual(engine.lastRecordUpload, 0);
|
|
ok(engine.isFirstSync);
|
|
await syncClientsEngine(server);
|
|
|
|
_("Send command to client to wipe history engine");
|
|
await engine.sendCommand("wipeEngine", ["history"]);
|
|
await syncClientsEngine(server);
|
|
|
|
_(
|
|
"Simulate the desktop client consuming the command and syncing to the server"
|
|
);
|
|
collection.insertRecord(
|
|
{
|
|
id: desktopID,
|
|
name: "Desktop client",
|
|
type: "desktop",
|
|
commands: [],
|
|
version: "48",
|
|
protocols: ["1.5"],
|
|
},
|
|
now - 10
|
|
);
|
|
|
|
_("Send another command to the desktop client to wipe tabs engine");
|
|
await engine.sendCommand("wipeEngine", ["tabs"], desktopID);
|
|
await syncClientsEngine(server);
|
|
|
|
let desktopPayload = collection.cleartext(desktopID);
|
|
compareCommands(
|
|
desktopPayload.commands,
|
|
[
|
|
{
|
|
command: "wipeEngine",
|
|
args: ["tabs"],
|
|
},
|
|
],
|
|
"Should only send the second command to the desktop client"
|
|
);
|
|
} finally {
|
|
await cleanup();
|
|
|
|
try {
|
|
server.deleteCollections("foo");
|
|
} finally {
|
|
await promiseStopServer(server);
|
|
}
|
|
}
|
|
});
|
|
|
|
add_task(async function test_upload_after_reboot() {
|
|
_("Multiple downloads, reboot, then upload (bug 1289287)");
|
|
|
|
let now = new_timestamp();
|
|
let server = await serverForFoo(engine);
|
|
|
|
await SyncTestingInfrastructure(server);
|
|
await generateNewKeys(Service.collectionKeys);
|
|
|
|
let collection = server.getCollection("foo", "clients");
|
|
|
|
let deviceBID = Utils.makeGUID();
|
|
let deviceCID = Utils.makeGUID();
|
|
collection.insertRecord(
|
|
{
|
|
id: deviceBID,
|
|
name: "Device B",
|
|
type: "desktop",
|
|
commands: [
|
|
{
|
|
command: "wipeEngine",
|
|
args: ["history"],
|
|
flowID: Utils.makeGUID(),
|
|
},
|
|
],
|
|
version: "48",
|
|
protocols: ["1.5"],
|
|
},
|
|
now - 10
|
|
);
|
|
collection.insertRecord(
|
|
{
|
|
id: deviceCID,
|
|
name: "Device C",
|
|
type: "desktop",
|
|
commands: [],
|
|
version: "48",
|
|
protocols: ["1.5"],
|
|
},
|
|
now - 10
|
|
);
|
|
|
|
try {
|
|
_("First sync. 2 records downloaded.");
|
|
strictEqual(engine.lastRecordUpload, 0);
|
|
ok(engine.isFirstSync);
|
|
await syncClientsEngine(server);
|
|
|
|
_("Send command to client to wipe tab engine");
|
|
await engine.sendCommand("wipeEngine", ["tabs"], deviceBID);
|
|
|
|
const oldUploadOutgoing = SyncEngine.prototype._uploadOutgoing;
|
|
SyncEngine.prototype._uploadOutgoing = async () =>
|
|
engine._onRecordsWritten([], [deviceBID]);
|
|
await syncClientsEngine(server);
|
|
|
|
let deviceBPayload = collection.cleartext(deviceBID);
|
|
compareCommands(
|
|
deviceBPayload.commands,
|
|
[
|
|
{
|
|
command: "wipeEngine",
|
|
args: ["history"],
|
|
},
|
|
],
|
|
"Should be the same because the upload failed"
|
|
);
|
|
|
|
_("Simulate the client B consuming the command and syncing to the server");
|
|
collection.insertRecord(
|
|
{
|
|
id: deviceBID,
|
|
name: "Device B",
|
|
type: "desktop",
|
|
commands: [],
|
|
version: "48",
|
|
protocols: ["1.5"],
|
|
},
|
|
now - 10
|
|
);
|
|
|
|
// Simulate reboot
|
|
SyncEngine.prototype._uploadOutgoing = oldUploadOutgoing;
|
|
engine = Service.clientsEngine = new ClientEngine(Service);
|
|
await engine.initialize();
|
|
|
|
await syncClientsEngine(server);
|
|
|
|
deviceBPayload = collection.cleartext(deviceBID);
|
|
compareCommands(
|
|
deviceBPayload.commands,
|
|
[
|
|
{
|
|
command: "wipeEngine",
|
|
args: ["tabs"],
|
|
},
|
|
],
|
|
"Should only had written our outgoing command"
|
|
);
|
|
} finally {
|
|
await cleanup();
|
|
|
|
try {
|
|
server.deleteCollections("foo");
|
|
} finally {
|
|
await promiseStopServer(server);
|
|
}
|
|
}
|
|
});
|
|
|
|
add_task(async function test_keep_cleared_commands_after_reboot() {
|
|
_(
|
|
"Download commands, fail upload, reboot, then apply new commands (bug 1289287)"
|
|
);
|
|
|
|
let now = new_timestamp();
|
|
let server = await serverForFoo(engine);
|
|
|
|
await SyncTestingInfrastructure(server);
|
|
await generateNewKeys(Service.collectionKeys);
|
|
|
|
let collection = server.getCollection("foo", "clients");
|
|
|
|
let deviceBID = Utils.makeGUID();
|
|
let deviceCID = Utils.makeGUID();
|
|
collection.insertRecord(
|
|
{
|
|
id: engine.localID,
|
|
name: "Device A",
|
|
type: "desktop",
|
|
commands: [
|
|
{
|
|
command: "wipeEngine",
|
|
args: ["history"],
|
|
flowID: Utils.makeGUID(),
|
|
},
|
|
{
|
|
command: "wipeEngine",
|
|
args: ["tabs"],
|
|
flowID: Utils.makeGUID(),
|
|
},
|
|
],
|
|
version: "48",
|
|
protocols: ["1.5"],
|
|
},
|
|
now - 10
|
|
);
|
|
collection.insertRecord(
|
|
{
|
|
id: deviceBID,
|
|
name: "Device B",
|
|
type: "desktop",
|
|
commands: [],
|
|
version: "48",
|
|
protocols: ["1.5"],
|
|
},
|
|
now - 10
|
|
);
|
|
collection.insertRecord(
|
|
{
|
|
id: deviceCID,
|
|
name: "Device C",
|
|
type: "desktop",
|
|
commands: [],
|
|
version: "48",
|
|
protocols: ["1.5"],
|
|
},
|
|
now - 10
|
|
);
|
|
|
|
try {
|
|
_("First sync. Download remote and our record.");
|
|
strictEqual(engine.lastRecordUpload, 0);
|
|
ok(engine.isFirstSync);
|
|
|
|
const oldUploadOutgoing = SyncEngine.prototype._uploadOutgoing;
|
|
SyncEngine.prototype._uploadOutgoing = async () =>
|
|
engine._onRecordsWritten([], [deviceBID]);
|
|
let commandsProcessed = 0;
|
|
engine.service.wipeClient = _engine => {
|
|
commandsProcessed++;
|
|
};
|
|
|
|
await syncClientsEngine(server);
|
|
await engine.processIncomingCommands(); // Not called by the engine.sync(), gotta call it ourselves
|
|
equal(commandsProcessed, 2, "We processed 2 commands");
|
|
|
|
let localRemoteRecord = collection.cleartext(engine.localID);
|
|
compareCommands(
|
|
localRemoteRecord.commands,
|
|
[
|
|
{
|
|
command: "wipeEngine",
|
|
args: ["history"],
|
|
},
|
|
{
|
|
command: "wipeEngine",
|
|
args: ["tabs"],
|
|
},
|
|
],
|
|
"Should be the same because the upload failed"
|
|
);
|
|
|
|
// Another client sends a wipe command
|
|
collection.insertRecord(
|
|
{
|
|
id: engine.localID,
|
|
name: "Device A",
|
|
type: "desktop",
|
|
commands: [
|
|
{
|
|
command: "wipeEngine",
|
|
args: ["history"],
|
|
flowID: Utils.makeGUID(),
|
|
},
|
|
{
|
|
command: "wipeEngine",
|
|
args: ["tabs"],
|
|
flowID: Utils.makeGUID(),
|
|
},
|
|
{
|
|
command: "wipeEngine",
|
|
args: ["bookmarks"],
|
|
flowID: Utils.makeGUID(),
|
|
},
|
|
],
|
|
version: "48",
|
|
protocols: ["1.5"],
|
|
},
|
|
now - 5
|
|
);
|
|
|
|
// Simulate reboot
|
|
SyncEngine.prototype._uploadOutgoing = oldUploadOutgoing;
|
|
engine = Service.clientsEngine = new ClientEngine(Service);
|
|
await engine.initialize();
|
|
|
|
commandsProcessed = 0;
|
|
engine.service.wipeClient = _engine => {
|
|
commandsProcessed++;
|
|
};
|
|
await syncClientsEngine(server);
|
|
await engine.processIncomingCommands();
|
|
equal(
|
|
commandsProcessed,
|
|
1,
|
|
"We processed one command (the other were cleared)"
|
|
);
|
|
|
|
localRemoteRecord = collection.cleartext(deviceBID);
|
|
deepEqual(localRemoteRecord.commands, [], "Should be empty");
|
|
} finally {
|
|
await cleanup();
|
|
|
|
// Reset service (remove mocks)
|
|
engine = Service.clientsEngine = new ClientEngine(Service);
|
|
await engine.initialize();
|
|
await engine._resetClient();
|
|
|
|
try {
|
|
server.deleteCollections("foo");
|
|
} finally {
|
|
await promiseStopServer(server);
|
|
}
|
|
}
|
|
});
|
|
|
|
add_task(async function test_deleted_commands() {
|
|
_("Verifies commands for a deleted client are discarded");
|
|
|
|
let now = new_timestamp();
|
|
let server = await serverForFoo(engine);
|
|
|
|
await SyncTestingInfrastructure(server);
|
|
await generateNewKeys(Service.collectionKeys);
|
|
|
|
let collection = server.getCollection("foo", "clients");
|
|
|
|
let activeID = Utils.makeGUID();
|
|
collection.insertRecord(
|
|
{
|
|
id: activeID,
|
|
name: "Active client",
|
|
type: "desktop",
|
|
commands: [],
|
|
version: "48",
|
|
protocols: ["1.5"],
|
|
},
|
|
now - 10
|
|
);
|
|
|
|
let deletedID = Utils.makeGUID();
|
|
collection.insertRecord(
|
|
{
|
|
id: deletedID,
|
|
name: "Client to delete",
|
|
type: "desktop",
|
|
commands: [],
|
|
version: "48",
|
|
protocols: ["1.5"],
|
|
},
|
|
now - 10
|
|
);
|
|
|
|
try {
|
|
_("First sync. 2 records downloaded.");
|
|
await syncClientsEngine(server);
|
|
|
|
_("Delete a record on the server.");
|
|
collection.remove(deletedID);
|
|
|
|
_("Broadcast a command to all clients");
|
|
await engine.sendCommand("logout", []);
|
|
await syncClientsEngine(server);
|
|
|
|
deepEqual(
|
|
collection.keys().sort(),
|
|
[activeID, engine.localID].sort(),
|
|
"Should not reupload deleted clients"
|
|
);
|
|
|
|
let activePayload = collection.cleartext(activeID);
|
|
compareCommands(
|
|
activePayload.commands,
|
|
[{ command: "logout", args: [] }],
|
|
"Should send the command to the active client"
|
|
);
|
|
} finally {
|
|
await cleanup();
|
|
|
|
try {
|
|
server.deleteCollections("foo");
|
|
} finally {
|
|
await promiseStopServer(server);
|
|
}
|
|
}
|
|
});
|
|
|
|
add_task(async function test_command_sync() {
|
|
_("Notify other clients when writing their record.");
|
|
|
|
await engine._store.wipe();
|
|
await generateNewKeys(Service.collectionKeys);
|
|
|
|
let server = await serverForFoo(engine);
|
|
await SyncTestingInfrastructure(server);
|
|
|
|
let collection = server.getCollection("foo", "clients");
|
|
let remoteId = Utils.makeGUID();
|
|
let remoteId2 = Utils.makeGUID();
|
|
|
|
_("Create remote client record 1");
|
|
collection.insertRecord({
|
|
id: remoteId,
|
|
name: "Remote client",
|
|
type: "desktop",
|
|
commands: [],
|
|
version: "48",
|
|
protocols: ["1.5"],
|
|
});
|
|
|
|
_("Create remote client record 2");
|
|
collection.insertRecord({
|
|
id: remoteId2,
|
|
name: "Remote client 2",
|
|
type: "mobile",
|
|
commands: [],
|
|
version: "48",
|
|
protocols: ["1.5"],
|
|
});
|
|
|
|
try {
|
|
equal(collection.count(), 2, "2 remote records written");
|
|
await syncClientsEngine(server);
|
|
equal(
|
|
collection.count(),
|
|
3,
|
|
"3 remote records written (+1 for the synced local record)"
|
|
);
|
|
|
|
await engine.sendCommand("wipeEngine", ["tabs"]);
|
|
await engine._tracker.addChangedID(engine.localID);
|
|
const getClientFxaDeviceId = sinon
|
|
.stub(engine, "getClientFxaDeviceId")
|
|
.callsFake(id => "fxa-" + id);
|
|
const engineMock = sinon.mock(engine);
|
|
let _notifyCollectionChanged = engineMock
|
|
.expects("_notifyCollectionChanged")
|
|
.withArgs(["fxa-" + remoteId, "fxa-" + remoteId2]);
|
|
_("Syncing.");
|
|
await syncClientsEngine(server);
|
|
_notifyCollectionChanged.verify();
|
|
|
|
engineMock.restore();
|
|
getClientFxaDeviceId.restore();
|
|
} finally {
|
|
await cleanup();
|
|
await engine._tracker.clearChangedIDs();
|
|
|
|
try {
|
|
server.deleteCollections("foo");
|
|
} finally {
|
|
await promiseStopServer(server);
|
|
}
|
|
}
|
|
});
|
|
|
|
add_task(async function ensureSameFlowIDs() {
|
|
let events = [];
|
|
let origRecordTelemetryEvent = Service.recordTelemetryEvent;
|
|
Service.recordTelemetryEvent = (object, method, value, extra) => {
|
|
events.push({ object, method, value, extra });
|
|
};
|
|
|
|
let server = await serverForFoo(engine);
|
|
try {
|
|
// Setup 2 clients, send them a command, and ensure we get to events
|
|
// written, both with the same flowID.
|
|
await SyncTestingInfrastructure(server);
|
|
let collection = server.getCollection("foo", "clients");
|
|
|
|
let remoteId = Utils.makeGUID();
|
|
let remoteId2 = Utils.makeGUID();
|
|
|
|
_("Create remote client record 1");
|
|
collection.insertRecord({
|
|
id: remoteId,
|
|
name: "Remote client",
|
|
type: "desktop",
|
|
commands: [],
|
|
version: "48",
|
|
protocols: ["1.5"],
|
|
});
|
|
|
|
_("Create remote client record 2");
|
|
collection.insertRecord({
|
|
id: remoteId2,
|
|
name: "Remote client 2",
|
|
type: "mobile",
|
|
commands: [],
|
|
version: "48",
|
|
protocols: ["1.5"],
|
|
});
|
|
|
|
await syncClientsEngine(server);
|
|
await engine.sendCommand("wipeEngine", ["tabs"]);
|
|
await syncClientsEngine(server);
|
|
equal(events.length, 2);
|
|
// we don't know what the flowID is, but do know it should be the same.
|
|
equal(events[0].extra.flowID, events[1].extra.flowID);
|
|
// Wipe remote clients to ensure deduping doesn't prevent us from adding the command.
|
|
for (let client of Object.values(engine._store._remoteClients)) {
|
|
client.commands = [];
|
|
}
|
|
// check it's correctly used when we specify a flow ID
|
|
events.length = 0;
|
|
let flowID = Utils.makeGUID();
|
|
await engine.sendCommand("wipeEngine", ["tabs"], null, { flowID });
|
|
await syncClientsEngine(server);
|
|
equal(events.length, 2);
|
|
equal(events[0].extra.flowID, flowID);
|
|
equal(events[1].extra.flowID, flowID);
|
|
|
|
// Wipe remote clients to ensure deduping doesn't prevent us from adding the command.
|
|
for (let client of Object.values(engine._store._remoteClients)) {
|
|
client.commands = [];
|
|
}
|
|
|
|
// and that it works when something else is in "extra"
|
|
events.length = 0;
|
|
await engine.sendCommand("wipeEngine", ["tabs"], null, {
|
|
reason: "testing",
|
|
});
|
|
await syncClientsEngine(server);
|
|
equal(events.length, 2);
|
|
equal(events[0].extra.flowID, events[1].extra.flowID);
|
|
equal(events[0].extra.reason, "testing");
|
|
equal(events[1].extra.reason, "testing");
|
|
// Wipe remote clients to ensure deduping doesn't prevent us from adding the command.
|
|
for (let client of Object.values(engine._store._remoteClients)) {
|
|
client.commands = [];
|
|
}
|
|
|
|
// and when both are specified.
|
|
events.length = 0;
|
|
await engine.sendCommand("wipeEngine", ["tabs"], null, {
|
|
reason: "testing",
|
|
flowID,
|
|
});
|
|
await syncClientsEngine(server);
|
|
equal(events.length, 2);
|
|
equal(events[0].extra.flowID, flowID);
|
|
equal(events[1].extra.flowID, flowID);
|
|
equal(events[0].extra.reason, "testing");
|
|
equal(events[1].extra.reason, "testing");
|
|
// Wipe remote clients to ensure deduping doesn't prevent us from adding the command.
|
|
for (let client of Object.values(engine._store._remoteClients)) {
|
|
client.commands = [];
|
|
}
|
|
} finally {
|
|
Service.recordTelemetryEvent = origRecordTelemetryEvent;
|
|
cleanup();
|
|
await promiseStopServer(server);
|
|
}
|
|
});
|
|
|
|
add_task(async function test_duplicate_commands_telemetry() {
|
|
let events = [];
|
|
let origRecordTelemetryEvent = Service.recordTelemetryEvent;
|
|
Service.recordTelemetryEvent = (object, method, value, extra) => {
|
|
events.push({ object, method, value, extra });
|
|
};
|
|
|
|
let server = await serverForFoo(engine);
|
|
try {
|
|
await SyncTestingInfrastructure(server);
|
|
let collection = server.getCollection("foo", "clients");
|
|
|
|
let remoteId = Utils.makeGUID();
|
|
let remoteId2 = Utils.makeGUID();
|
|
|
|
_("Create remote client record 1");
|
|
collection.insertRecord({
|
|
id: remoteId,
|
|
name: "Remote client",
|
|
type: "desktop",
|
|
commands: [],
|
|
version: "48",
|
|
protocols: ["1.5"],
|
|
});
|
|
|
|
_("Create remote client record 2");
|
|
collection.insertRecord({
|
|
id: remoteId2,
|
|
name: "Remote client 2",
|
|
type: "mobile",
|
|
commands: [],
|
|
version: "48",
|
|
protocols: ["1.5"],
|
|
});
|
|
|
|
await syncClientsEngine(server);
|
|
// Make sure deduping works before syncing
|
|
await engine.sendCommand("wipeEngine", ["history"], remoteId);
|
|
await engine.sendCommand("wipeEngine", ["history"], remoteId);
|
|
equal(events.length, 1);
|
|
await syncClientsEngine(server);
|
|
// And after syncing.
|
|
await engine.sendCommand("wipeEngine", ["history"], remoteId);
|
|
equal(events.length, 1);
|
|
// Ensure we aren't deduping commands to different clients
|
|
await engine.sendCommand("wipeEngine", ["history"], remoteId2);
|
|
equal(events.length, 2);
|
|
} finally {
|
|
Service.recordTelemetryEvent = origRecordTelemetryEvent;
|
|
cleanup();
|
|
await promiseStopServer(server);
|
|
}
|
|
});
|
|
|
|
add_task(async function test_other_clients_notified_on_first_sync() {
|
|
_(
|
|
"Ensure that other clients are notified when we upload our client record for the first time."
|
|
);
|
|
|
|
await engine.resetLastSync();
|
|
await engine._store.wipe();
|
|
await generateNewKeys(Service.collectionKeys);
|
|
|
|
let server = await serverForFoo(engine);
|
|
await SyncTestingInfrastructure(server);
|
|
|
|
const fxAccounts = engine.fxAccounts;
|
|
let calls = 0;
|
|
engine.fxAccounts = {
|
|
device: {
|
|
getLocalId() {
|
|
return fxAccounts.device.getLocalId();
|
|
},
|
|
getLocalName() {
|
|
return fxAccounts.device.getLocalName();
|
|
},
|
|
getLocalType() {
|
|
return fxAccounts.device.getLocalType();
|
|
},
|
|
},
|
|
notifyDevices() {
|
|
calls++;
|
|
return Promise.resolve(true);
|
|
},
|
|
_internal: {
|
|
now() {
|
|
return Date.now();
|
|
},
|
|
},
|
|
};
|
|
|
|
try {
|
|
engine.lastRecordUpload = 0;
|
|
_("First sync, should notify other clients");
|
|
await syncClientsEngine(server);
|
|
equal(calls, 1);
|
|
|
|
_("Second sync, should not notify other clients");
|
|
await syncClientsEngine(server);
|
|
equal(calls, 1);
|
|
} finally {
|
|
engine.fxAccounts = fxAccounts;
|
|
cleanup();
|
|
await promiseStopServer(server);
|
|
}
|
|
});
|
|
|
|
add_task(
|
|
async function device_disconnected_notification_updates_known_stale_clients() {
|
|
const spyUpdate = sinon.spy(engine, "updateKnownStaleClients");
|
|
|
|
Services.obs.notifyObservers(
|
|
null,
|
|
"fxaccounts:device_disconnected",
|
|
JSON.stringify({ isLocalDevice: false })
|
|
);
|
|
ok(spyUpdate.calledOnce, "updateKnownStaleClients should be called");
|
|
spyUpdate.resetHistory();
|
|
|
|
Services.obs.notifyObservers(
|
|
null,
|
|
"fxaccounts:device_disconnected",
|
|
JSON.stringify({ isLocalDevice: true })
|
|
);
|
|
ok(spyUpdate.notCalled, "updateKnownStaleClients should not be called");
|
|
|
|
spyUpdate.restore();
|
|
}
|
|
);
|
|
|
|
add_task(async function update_known_stale_clients() {
|
|
const makeFakeClient = id => ({ id, fxaDeviceId: `fxa-${id}` });
|
|
const clients = [
|
|
makeFakeClient("one"),
|
|
makeFakeClient("two"),
|
|
makeFakeClient("three"),
|
|
];
|
|
const stubRemoteClients = sinon
|
|
.stub(engine._store, "_remoteClients")
|
|
.get(() => {
|
|
return clients;
|
|
});
|
|
const stubFetchFxADevices = sinon
|
|
.stub(engine, "_fetchFxADevices")
|
|
.callsFake(() => {
|
|
engine._knownStaleFxADeviceIds = ["fxa-one", "fxa-two"];
|
|
});
|
|
|
|
engine._knownStaleFxADeviceIds = null;
|
|
await engine.updateKnownStaleClients();
|
|
ok(clients[0].stale);
|
|
ok(clients[1].stale);
|
|
ok(!clients[2].stale);
|
|
|
|
stubRemoteClients.restore();
|
|
stubFetchFxADevices.restore();
|
|
});
|
|
|
|
add_task(async function test_create_record_command_limit() {
|
|
await engine._store.wipe();
|
|
await generateNewKeys(Service.collectionKeys);
|
|
|
|
let server = await serverForFoo(engine);
|
|
await SyncTestingInfrastructure(server);
|
|
|
|
const fakeLimit = 4 * 1024;
|
|
|
|
let maxSizeStub = sinon
|
|
.stub(Service, "getMemcacheMaxRecordPayloadSize")
|
|
.callsFake(() => fakeLimit);
|
|
|
|
let user = server.user("foo");
|
|
let remoteId = Utils.makeGUID();
|
|
|
|
_("Create remote client record");
|
|
user.collection("clients").insertRecord({
|
|
id: remoteId,
|
|
name: "Remote client",
|
|
type: "desktop",
|
|
commands: [],
|
|
version: "57",
|
|
protocols: ["1.5"],
|
|
});
|
|
|
|
try {
|
|
_("Initial sync.");
|
|
await syncClientsEngine(server);
|
|
|
|
_("Send a fairly sane number of commands.");
|
|
|
|
for (let i = 0; i < 5; ++i) {
|
|
await engine.sendCommand("wipeEngine", [`history: ${i}`], remoteId);
|
|
}
|
|
|
|
await syncClientsEngine(server);
|
|
|
|
_("Make sure they all fit and weren't dropped.");
|
|
let parsedServerRecord = user.collection("clients").cleartext(remoteId);
|
|
|
|
equal(parsedServerRecord.commands.length, 5);
|
|
|
|
await engine.sendCommand("wipeEngine", ["history"], remoteId);
|
|
|
|
_("Send a not-sane number of commands.");
|
|
// Much higher than the maximum number of commands we could actually fit.
|
|
for (let i = 0; i < 500; ++i) {
|
|
await engine.sendCommand("wipeEngine", [`tabs: ${i}`], remoteId);
|
|
}
|
|
|
|
await syncClientsEngine(server);
|
|
|
|
_("Ensure we didn't overflow the server limit.");
|
|
let wbo = user.collection("clients").wbo(remoteId);
|
|
less(wbo.payload.length, fakeLimit);
|
|
|
|
_(
|
|
"And that the data we uploaded is both sane json and containing some commands."
|
|
);
|
|
let remoteCommands = wbo.getCleartext().commands;
|
|
greater(remoteCommands.length, 2);
|
|
let firstCommand = remoteCommands[0];
|
|
_(
|
|
"The first command should still be present, since it had a high priority"
|
|
);
|
|
equal(firstCommand.command, "wipeEngine");
|
|
_("And the last command in the list should be the last command we sent.");
|
|
let lastCommand = remoteCommands[remoteCommands.length - 1];
|
|
equal(lastCommand.command, "wipeEngine");
|
|
deepEqual(lastCommand.args, ["tabs: 499"]);
|
|
} finally {
|
|
maxSizeStub.restore();
|
|
await cleanup();
|
|
try {
|
|
let collection = server.getCollection("foo", "clients");
|
|
collection.remove(remoteId);
|
|
} finally {
|
|
await promiseStopServer(server);
|
|
}
|
|
}
|
|
});
|