michael@0: /* Any copyright is dedicated to the Public Domain. michael@0: http://creativecommons.org/publicdomain/zero/1.0/ */ michael@0: michael@0: Cu.import("resource://services-sync/engines.js"); michael@0: Cu.import("resource://services-sync/service.js"); michael@0: Cu.import("resource://services-sync/util.js"); michael@0: Cu.import("resource://testing-common/services/sync/rotaryengine.js"); michael@0: Cu.import("resource://testing-common/services/sync/utils.js"); michael@0: michael@0: // Track HMAC error counts. michael@0: let hmacErrorCount = 0; michael@0: (function () { michael@0: let hHE = Service.handleHMACEvent; michael@0: Service.handleHMACEvent = function () { michael@0: hmacErrorCount++; michael@0: return hHE.call(Service); michael@0: }; michael@0: })(); michael@0: michael@0: function shared_setup() { michael@0: hmacErrorCount = 0; michael@0: michael@0: // Do not instantiate SyncTestingInfrastructure; we need real crypto. michael@0: ensureLegacyIdentityManager(); michael@0: setBasicCredentials("foo", "foo", "aabcdeabcdeabcdeabcdeabcde"); michael@0: michael@0: // Make sure RotaryEngine is the only one we sync. michael@0: Service.engineManager._engines = {}; michael@0: Service.engineManager.register(RotaryEngine); michael@0: let engine = Service.engineManager.get("rotary"); michael@0: engine.enabled = true; michael@0: engine.lastSync = 123; // Needs to be non-zero so that tracker is queried. michael@0: engine._store.items = {flying: "LNER Class A3 4472", michael@0: scotsman: "Flying Scotsman"}; michael@0: engine._tracker.addChangedID('scotsman', 0); michael@0: do_check_eq(1, Service.engineManager.getEnabled().length); michael@0: michael@0: let engines = {rotary: {version: engine.version, michael@0: syncID: engine.syncID}, michael@0: clients: {version: Service.clientsEngine.version, michael@0: syncID: Service.clientsEngine.syncID}}; michael@0: michael@0: // Common server objects. michael@0: let global = new ServerWBO("global", {engines: engines}); michael@0: let keysWBO = new ServerWBO("keys"); michael@0: let rotaryColl = new ServerCollection({}, true); michael@0: let clientsColl = new ServerCollection({}, true); michael@0: michael@0: return [engine, rotaryColl, clientsColl, keysWBO, global]; michael@0: } michael@0: michael@0: add_test(function hmac_error_during_404() { michael@0: _("Attempt to replicate the HMAC error setup."); michael@0: let [engine, rotaryColl, clientsColl, keysWBO, global] = shared_setup(); michael@0: michael@0: // Hand out 404s for crypto/keys. michael@0: let keysHandler = keysWBO.handler(); michael@0: let key404Counter = 0; michael@0: let keys404Handler = function (request, response) { michael@0: if (key404Counter > 0) { michael@0: let body = "Not Found"; michael@0: response.setStatusLine(request.httpVersion, 404, body); michael@0: response.bodyOutputStream.write(body, body.length); michael@0: key404Counter--; michael@0: return; michael@0: } michael@0: keysHandler(request, response); michael@0: }; michael@0: michael@0: let collectionsHelper = track_collections_helper(); michael@0: let upd = collectionsHelper.with_updated_collection; michael@0: let collections = collectionsHelper.collections; michael@0: let handlers = { michael@0: "/1.1/foo/info/collections": collectionsHelper.handler, michael@0: "/1.1/foo/storage/meta/global": upd("meta", global.handler()), michael@0: "/1.1/foo/storage/crypto/keys": upd("crypto", keys404Handler), michael@0: "/1.1/foo/storage/clients": upd("clients", clientsColl.handler()), michael@0: "/1.1/foo/storage/rotary": upd("rotary", rotaryColl.handler()) michael@0: }; michael@0: michael@0: let server = sync_httpd_setup(handlers); michael@0: Service.serverURL = server.baseURI; michael@0: michael@0: try { michael@0: _("Syncing."); michael@0: Service.sync(); michael@0: _("Partially resetting client, as if after a restart, and forcing redownload."); michael@0: Service.collectionKeys.clear(); michael@0: engine.lastSync = 0; // So that we redownload records. michael@0: key404Counter = 1; michael@0: _("---------------------------"); michael@0: Service.sync(); michael@0: _("---------------------------"); michael@0: michael@0: // Two rotary items, one client record... no errors. michael@0: do_check_eq(hmacErrorCount, 0) michael@0: } finally { michael@0: Svc.Prefs.resetBranch(""); michael@0: Service.recordManager.clearCache(); michael@0: server.stop(run_next_test); michael@0: } michael@0: }); michael@0: michael@0: add_test(function hmac_error_during_node_reassignment() { michael@0: _("Attempt to replicate an HMAC error during node reassignment."); michael@0: let [engine, rotaryColl, clientsColl, keysWBO, global] = shared_setup(); michael@0: michael@0: let collectionsHelper = track_collections_helper(); michael@0: let upd = collectionsHelper.with_updated_collection; michael@0: michael@0: // We'll provide a 401 mid-way through the sync. This function michael@0: // simulates shifting to a node which has no data. michael@0: function on401() { michael@0: _("Deleting server data..."); michael@0: global.delete(); michael@0: rotaryColl.delete(); michael@0: keysWBO.delete(); michael@0: clientsColl.delete(); michael@0: delete collectionsHelper.collections.rotary; michael@0: delete collectionsHelper.collections.crypto; michael@0: delete collectionsHelper.collections.clients; michael@0: _("Deleted server data."); michael@0: } michael@0: michael@0: let should401 = false; michael@0: function upd401(coll, handler) { michael@0: return function (request, response) { michael@0: if (should401 && (request.method != "DELETE")) { michael@0: on401(); michael@0: should401 = false; michael@0: let body = "\"reassigned!\""; michael@0: response.setStatusLine(request.httpVersion, 401, "Node reassignment."); michael@0: response.bodyOutputStream.write(body, body.length); michael@0: return; michael@0: } michael@0: handler(request, response); michael@0: }; michael@0: } michael@0: michael@0: function sameNodeHandler(request, response) { michael@0: // Set this so that _setCluster will think we've really changed. michael@0: let url = Service.serverURL.replace("localhost", "LOCALHOST"); michael@0: _("Client requesting reassignment; pointing them to " + url); michael@0: response.setStatusLine(request.httpVersion, 200, "OK"); michael@0: response.bodyOutputStream.write(url, url.length); michael@0: } michael@0: michael@0: let handlers = { michael@0: "/user/1.0/foo/node/weave": sameNodeHandler, michael@0: "/1.1/foo/info/collections": collectionsHelper.handler, michael@0: "/1.1/foo/storage/meta/global": upd("meta", global.handler()), michael@0: "/1.1/foo/storage/crypto/keys": upd("crypto", keysWBO.handler()), michael@0: "/1.1/foo/storage/clients": upd401("clients", clientsColl.handler()), michael@0: "/1.1/foo/storage/rotary": upd("rotary", rotaryColl.handler()) michael@0: }; michael@0: michael@0: let server = sync_httpd_setup(handlers); michael@0: Service.serverURL = server.baseURI; michael@0: _("Syncing."); michael@0: // First hit of clients will 401. This will happen after meta/global and michael@0: // keys -- i.e., in the middle of the sync, but before RotaryEngine. michael@0: should401 = true; michael@0: michael@0: // Use observers to perform actions when our sync finishes. michael@0: // This allows us to observe the automatic next-tick sync that occurs after michael@0: // an abort. michael@0: function onSyncError() { michael@0: do_throw("Should not get a sync error!"); michael@0: } michael@0: function onSyncFinished() {} michael@0: let obs = { michael@0: observe: function observe(subject, topic, data) { michael@0: switch (topic) { michael@0: case "weave:service:sync:error": michael@0: onSyncError(); michael@0: break; michael@0: case "weave:service:sync:finish": michael@0: onSyncFinished(); michael@0: break; michael@0: } michael@0: } michael@0: }; michael@0: michael@0: Svc.Obs.add("weave:service:sync:finish", obs); michael@0: Svc.Obs.add("weave:service:sync:error", obs); michael@0: michael@0: // This kicks off the actual test. Split into a function here to allow this michael@0: // source file to broadly follow actual execution order. michael@0: function onwards() { michael@0: _("== Invoking first sync."); michael@0: Service.sync(); michael@0: _("We should not simultaneously have data but no keys on the server."); michael@0: let hasData = rotaryColl.wbo("flying") || michael@0: rotaryColl.wbo("scotsman"); michael@0: let hasKeys = keysWBO.modified; michael@0: michael@0: _("We correctly handle 401s by aborting the sync and starting again."); michael@0: do_check_true(!hasData == !hasKeys); michael@0: michael@0: _("Be prepared for the second (automatic) sync..."); michael@0: } michael@0: michael@0: _("Make sure that syncing again causes recovery."); michael@0: onSyncFinished = function() { michael@0: _("== First sync done."); michael@0: _("---------------------------"); michael@0: onSyncFinished = function() { michael@0: _("== Second (automatic) sync done."); michael@0: hasData = rotaryColl.wbo("flying") || michael@0: rotaryColl.wbo("scotsman"); michael@0: hasKeys = keysWBO.modified; michael@0: do_check_true(!hasData == !hasKeys); michael@0: michael@0: // Kick off another sync. Can't just call it, because we're inside the michael@0: // lock... michael@0: Utils.nextTick(function() { michael@0: _("Now a fresh sync will get no HMAC errors."); michael@0: _("Partially resetting client, as if after a restart, and forcing redownload."); michael@0: Service.collectionKeys.clear(); michael@0: engine.lastSync = 0; michael@0: hmacErrorCount = 0; michael@0: michael@0: onSyncFinished = function() { michael@0: // Two rotary items, one client record... no errors. michael@0: do_check_eq(hmacErrorCount, 0) michael@0: michael@0: Svc.Obs.remove("weave:service:sync:finish", obs); michael@0: Svc.Obs.remove("weave:service:sync:error", obs); michael@0: michael@0: Svc.Prefs.resetBranch(""); michael@0: Service.recordManager.clearCache(); michael@0: server.stop(run_next_test); michael@0: }; michael@0: michael@0: Service.sync(); michael@0: }, michael@0: this); michael@0: }; michael@0: }; michael@0: michael@0: onwards(); michael@0: }); michael@0: michael@0: function run_test() { michael@0: initTestLogging("Trace"); michael@0: run_next_test(); michael@0: }