/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
const { Service } = ChromeUtils.importESModule(
"resource://services-sync/service.sys.mjs"
);
// Track HMAC error counts.
var hmacErrorCount = 0;
(
function () {
let hHE = Service.handleHMACEvent;
Service.handleHMACEvent = async
function () {
hmacErrorCount++;
return hHE.call(Service);
};
})();
async
function shared_setup() {
enableValidationPrefs();
syncTestLogging();
hmacErrorCount = 0;
let clientsEngine = Service.clientsEngine;
let clientsSyncID = await clientsEngine.resetLocalSyncID();
// Make sure RotaryEngine is the only one we sync.
let { engine, syncID, tracker } = await registerRotaryEngine();
await engine.setLastSync(123);
// Needs to be non-zero so that tracker is queried.
engine._store.items = {
flying:
"LNER Class A3 4472",
scotsman:
"Flying Scotsman",
};
await tracker.addChangedID(
"scotsman", 0);
Assert.equal(1, Service.engineManager.getEnabled().length);
let engines = {
rotary: { version: engine.version, syncID },
clients: { version: clientsEngine.version, syncID: clientsSyncID },
};
// Common server objects.
let global =
new ServerWBO(
"global", { engines });
let keysWBO =
new ServerWBO(
"keys");
let rotaryColl =
new ServerCollection({},
true);
let clientsColl =
new ServerCollection({},
true);
return [engine, rotaryColl, clientsColl, keysWBO, global, tracker];
}
add_task(async
function hmac_error_during_404() {
_(
"Attempt to replicate the HMAC error setup.");
let [engine, rotaryColl, clientsColl, keysWBO, global, tracker] =
await shared_setup();
// Hand out 404s for crypto/keys.
let keysHandler = keysWBO.handler();
let key404Counter = 0;
let keys404Handler =
function (request, response) {
if (key404Counter > 0) {
let body =
"Not Found";
response.setStatusLine(request.httpVersion, 404, body);
response.bodyOutputStream.write(body, body.length);
key404Counter--;
return;
}
keysHandler(request, response);
};
let collectionsHelper = track_collections_helper();
let upd = collectionsHelper.with_updated_collection;
let handlers = {
"/1.1/foo/info/collections": collectionsHelper.handler,
"/1.1/foo/storage/meta/global": upd(
"meta", global.handler()),
"/1.1/foo/storage/crypto/keys": upd(
"crypto", keys404Handler),
"/1.1/foo/storage/clients": upd(
"clients", clientsColl.handler()),
"/1.1/foo/storage/rotary": upd(
"rotary", rotaryColl.handler()),
};
let server = sync_httpd_setup(handlers);
// Do not instantiate SyncTestingInfrastructure; we need real crypto.
await configureIdentity({ username:
"foo" }, server);
await Service.login();
try {
_(
"Syncing.");
await sync_and_validate_telem();
_(
"Partially resetting client, as if after a restart, and forcing redownload."
);
Service.collectionKeys.clear();
await engine.setLastSync(0);
// So that we redownload records.
key404Counter = 1;
_(
"---------------------------");
await sync_and_validate_telem();
_(
"---------------------------");
// Two rotary items, one client record... no errors.
Assert.equal(hmacErrorCount, 0);
}
finally {
await tracker.clearChangedIDs();
await Service.engineManager.unregister(engine);
for (
const pref of Svc.PrefBranch.getChildList(
"")) {
Svc.PrefBranch.clearUserPref(pref);
}
Service.recordManager.clearCache();
await promiseStopServer(server);
}
});
add_task(async
function hmac_error_during_node_reassignment() {
_(
"Attempt to replicate an HMAC error during node reassignment.");
let [engine, rotaryColl, clientsColl, keysWBO, global, tracker] =
await shared_setup();
let collectionsHelper = track_collections_helper();
let upd = collectionsHelper.with_updated_collection;
// We'll provide a 401 mid-way through the sync. This function
// simulates shifting to a node which has no data.
function on401() {
_(
"Deleting server data...");
global.
delete();
rotaryColl.
delete();
keysWBO.
delete();
clientsColl.
delete();
delete collectionsHelper.collections.rotary;
delete collectionsHelper.collections.crypto;
delete collectionsHelper.collections.clients;
_(
"Deleted server data.");
}
let should401 =
false;
function upd401(coll, handler) {
return function (request, response) {
if (should401 && request.method !=
"DELETE") {
on401();
should401 =
false;
let body =
'"reassigned!"';
response.setStatusLine(request.httpVersion, 401,
"Node reassignment.");
response.bodyOutputStream.write(body, body.length);
return;
}
handler(request, response);
};
}
let handlers = {
"/1.1/foo/info/collections": collectionsHelper.handler,
"/1.1/foo/storage/meta/global": upd(
"meta", global.handler()),
"/1.1/foo/storage/crypto/keys": upd(
"crypto", keysWBO.handler()),
"/1.1/foo/storage/clients": upd401(
"clients", clientsColl.handler()),
"/1.1/foo/storage/rotary": upd(
"rotary", rotaryColl.handler()),
};
let server = sync_httpd_setup(handlers);
// Do not instantiate SyncTestingInfrastructure; we need real crypto.
await configureIdentity({ username:
"foo" }, server);
_(
"Syncing.");
// First hit of clients will 401. This will happen after meta/global and
// keys -- i.e., in the middle of the sync, but before RotaryEngine.
should401 =
true;
// Use observers to perform actions when our sync finishes.
// This allows us to observe the automatic next-tick sync that occurs after
// an abort.
function onSyncError() {
do_throw(
"Should not get a sync error!");
}
let onSyncFinished =
function () {};
let obs = {
observe:
function observe(subject, topic) {
switch (topic) {
case "weave:service:sync:error":
onSyncError();
break;
case "weave:service:sync:finish":
onSyncFinished();
break;
}
},
};
Svc.Obs.add(
"weave:service:sync:finish", obs);
Svc.Obs.add(
"weave:service:sync:error", obs);
// This kicks off the actual test. Split into a function here to allow this
// source file to broadly follow actual execution order.
async
function onwards() {
_(
"== Invoking first sync.");
await Service.sync();
_(
"We should not simultaneously have data but no keys on the server.");
let hasData = rotaryColl.wbo(
"flying") || rotaryColl.wbo(
"scotsman");
let hasKeys = keysWBO.modified;
_(
"We correctly handle 401s by aborting the sync and starting again.");
Assert.ok(!hasData == !hasKeys);
_(
"Be prepared for the second (automatic) sync...");
}
_(
"Make sure that syncing again causes recovery.");
let callbacksPromise =
new Promise(resolve => {
onSyncFinished =
function () {
_(
"== First sync done.");
_(
"---------------------------");
onSyncFinished =
function () {
_(
"== Second (automatic) sync done.");
let hasData = rotaryColl.wbo(
"flying") || rotaryColl.wbo(
"scotsman");
let hasKeys = keysWBO.modified;
Assert.ok(!hasData == !hasKeys);
// Kick off another sync. Can't just call it, because we're inside the
// lock...
(async () => {
await Async.promiseYield();
_(
"Now a fresh sync will get no HMAC errors.");
_(
"Partially resetting client, as if after a restart, and forcing redownload."
);
Service.collectionKeys.clear();
await engine.setLastSync(0);
hmacErrorCount = 0;
onSyncFinished = async
function () {
// Two rotary items, one client record... no errors.
Assert.equal(hmacErrorCount, 0);
Svc.Obs.remove(
"weave:service:sync:finish", obs);
Svc.Obs.remove(
"weave:service:sync:error", obs);
await tracker.clearChangedIDs();
await Service.engineManager.unregister(engine);
for (
const pref of Svc.PrefBranch.getChildList(
"")) {
Svc.PrefBranch.clearUserPref(pref);
}
Service.recordManager.clearCache();
server.stop(resolve);
};
Service.sync();
})().
catch(console.error);
};
};
});
await onwards();
await callbacksPromise;
});