summaryrefslogtreecommitdiffstats
path: root/services/sync/tests/unit/test_telemetry.js
diff options
context:
space:
mode:
Diffstat (limited to 'services/sync/tests/unit/test_telemetry.js')
-rw-r--r--services/sync/tests/unit/test_telemetry.js564
1 files changed, 564 insertions, 0 deletions
diff --git a/services/sync/tests/unit/test_telemetry.js b/services/sync/tests/unit/test_telemetry.js
new file mode 100644
index 000000000..50a3d136b
--- /dev/null
+++ b/services/sync/tests/unit/test_telemetry.js
@@ -0,0 +1,564 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+Cu.import("resource://services-common/observers.js");
+Cu.import("resource://services-sync/telemetry.js");
+Cu.import("resource://services-sync/service.js");
+Cu.import("resource://services-sync/record.js");
+Cu.import("resource://services-sync/resource.js");
+Cu.import("resource://services-sync/constants.js");
+Cu.import("resource://services-sync/engines.js");
+Cu.import("resource://services-sync/engines/bookmarks.js");
+Cu.import("resource://services-sync/engines/clients.js");
+Cu.import("resource://testing-common/services/sync/utils.js");
+Cu.import("resource://testing-common/services/sync/fxa_utils.js");
+Cu.import("resource://testing-common/services/sync/rotaryengine.js");
+Cu.import("resource://gre/modules/osfile.jsm", this);
+
+Cu.import("resource://gre/modules/PlacesUtils.jsm");
+Cu.import("resource://services-sync/util.js");
+
+initTestLogging("Trace");
+
+function SteamStore(engine) {
+ Store.call(this, "Steam", engine);
+}
+
+SteamStore.prototype = {
+ __proto__: Store.prototype,
+};
+
+function SteamTracker(name, engine) {
+ Tracker.call(this, name || "Steam", engine);
+}
+
+SteamTracker.prototype = {
+ __proto__: Tracker.prototype
+};
+
+function SteamEngine(service) {
+ Engine.call(this, "steam", service);
+}
+
+SteamEngine.prototype = {
+ __proto__: Engine.prototype,
+ _storeObj: SteamStore,
+ _trackerObj: SteamTracker,
+ _errToThrow: null,
+ _sync() {
+ if (this._errToThrow) {
+ throw this._errToThrow;
+ }
+ }
+};
+
+function BogusEngine(service) {
+ Engine.call(this, "bogus", service);
+}
+
+BogusEngine.prototype = Object.create(SteamEngine.prototype);
+
+function cleanAndGo(server) {
+ Svc.Prefs.resetBranch("");
+ Svc.Prefs.set("log.logger.engine.rotary", "Trace");
+ Service.recordManager.clearCache();
+ return new Promise(resolve => server.stop(resolve));
+}
+
+// Avoid addon manager complaining about not being initialized
+Service.engineManager.unregister("addons");
+
+add_identity_test(this, function *test_basic() {
+ let helper = track_collections_helper();
+ let upd = helper.with_updated_collection;
+
+ yield configureIdentity({ username: "johndoe" });
+ let handlers = {
+ "/1.1/johndoe/info/collections": helper.handler,
+ "/1.1/johndoe/storage/crypto/keys": upd("crypto", new ServerWBO("keys").handler()),
+ "/1.1/johndoe/storage/meta/global": upd("meta", new ServerWBO("global").handler())
+ };
+
+ let collections = ["clients", "bookmarks", "forms", "history", "passwords", "prefs", "tabs"];
+
+ for (let coll of collections) {
+ handlers["/1.1/johndoe/storage/" + coll] = upd(coll, new ServerCollection({}, true).handler());
+ }
+
+ let server = httpd_setup(handlers);
+ Service.serverURL = server.baseURI;
+
+ yield sync_and_validate_telem(true);
+
+ yield new Promise(resolve => server.stop(resolve));
+});
+
+add_task(function* test_processIncoming_error() {
+ let engine = new BookmarksEngine(Service);
+ let store = engine._store;
+ let server = serverForUsers({"foo": "password"}, {
+ meta: {global: {engines: {bookmarks: {version: engine.version,
+ syncID: engine.syncID}}}},
+ bookmarks: {}
+ });
+ new SyncTestingInfrastructure(server.server);
+ let collection = server.user("foo").collection("bookmarks");
+ try {
+ // Create a bogus record that when synced down will provoke a
+ // network error which in turn provokes an exception in _processIncoming.
+ const BOGUS_GUID = "zzzzzzzzzzzz";
+ let bogus_record = collection.insert(BOGUS_GUID, "I'm a bogus record!");
+ bogus_record.get = function get() {
+ throw "Sync this!";
+ };
+ // Make the 10 minutes old so it will only be synced in the toFetch phase.
+ bogus_record.modified = Date.now() / 1000 - 60 * 10;
+ engine.lastSync = Date.now() / 1000 - 60;
+ engine.toFetch = [BOGUS_GUID];
+
+ let error, ping;
+ try {
+ yield sync_engine_and_validate_telem(engine, true, errPing => ping = errPing);
+ } catch(ex) {
+ error = ex;
+ }
+ ok(!!error);
+ ok(!!ping);
+ equal(ping.uid, "0".repeat(32));
+ deepEqual(ping.failureReason, {
+ name: "othererror",
+ error: "error.engine.reason.record_download_fail"
+ });
+
+ equal(ping.engines.length, 1);
+ equal(ping.engines[0].name, "bookmarks");
+ deepEqual(ping.engines[0].failureReason, {
+ name: "othererror",
+ error: "error.engine.reason.record_download_fail"
+ });
+
+ } finally {
+ store.wipe();
+ yield cleanAndGo(server);
+ }
+});
+
+add_task(function *test_uploading() {
+ let engine = new BookmarksEngine(Service);
+ let store = engine._store;
+ let server = serverForUsers({"foo": "password"}, {
+ meta: {global: {engines: {bookmarks: {version: engine.version,
+ syncID: engine.syncID}}}},
+ bookmarks: {}
+ });
+ new SyncTestingInfrastructure(server.server);
+
+ let parent = PlacesUtils.toolbarFolderId;
+ let uri = Utils.makeURI("http://getfirefox.com/");
+ let title = "Get Firefox";
+
+ let bmk_id = PlacesUtils.bookmarks.insertBookmark(parent, uri,
+ PlacesUtils.bookmarks.DEFAULT_INDEX, "Get Firefox!");
+
+ let guid = store.GUIDForId(bmk_id);
+ let record = store.createRecord(guid);
+
+ let collection = server.user("foo").collection("bookmarks");
+ try {
+ let ping = yield sync_engine_and_validate_telem(engine, false);
+ ok(!!ping);
+ equal(ping.engines.length, 1);
+ equal(ping.engines[0].name, "bookmarks");
+ ok(!!ping.engines[0].outgoing);
+ greater(ping.engines[0].outgoing[0].sent, 0)
+ ok(!ping.engines[0].incoming);
+
+ PlacesUtils.bookmarks.setItemTitle(bmk_id, "New Title");
+
+ store.wipe();
+ engine.resetClient();
+
+ ping = yield sync_engine_and_validate_telem(engine, false);
+ equal(ping.engines.length, 1);
+ equal(ping.engines[0].name, "bookmarks");
+ equal(ping.engines[0].outgoing.length, 1);
+ ok(!!ping.engines[0].incoming);
+
+ } finally {
+ // Clean up.
+ store.wipe();
+ yield cleanAndGo(server);
+ }
+});
+
+add_task(function *test_upload_failed() {
+ Service.identity.username = "foo";
+ let collection = new ServerCollection();
+ collection._wbos.flying = new ServerWBO('flying');
+
+ let server = sync_httpd_setup({
+ "/1.1/foo/storage/rotary": collection.handler()
+ });
+
+ let syncTesting = new SyncTestingInfrastructure(server);
+
+ let engine = new RotaryEngine(Service);
+ engine.lastSync = 123; // needs to be non-zero so that tracker is queried
+ engine.lastSyncLocal = 456;
+ engine._store.items = {
+ flying: "LNER Class A3 4472",
+ scotsman: "Flying Scotsman",
+ peppercorn: "Peppercorn Class"
+ };
+ const FLYING_CHANGED = 12345;
+ const SCOTSMAN_CHANGED = 23456;
+ const PEPPERCORN_CHANGED = 34567;
+ engine._tracker.addChangedID("flying", FLYING_CHANGED);
+ engine._tracker.addChangedID("scotsman", SCOTSMAN_CHANGED);
+ engine._tracker.addChangedID("peppercorn", PEPPERCORN_CHANGED);
+
+ let meta_global = Service.recordManager.set(engine.metaURL, new WBORecord(engine.metaURL));
+ meta_global.payload.engines = { rotary: { version: engine.version, syncID: engine.syncID } };
+
+ try {
+ engine.enabled = true;
+ let ping = yield sync_engine_and_validate_telem(engine, true);
+ ok(!!ping);
+ equal(ping.engines.length, 1);
+ equal(ping.engines[0].incoming, null);
+ deepEqual(ping.engines[0].outgoing, [{ sent: 3, failed: 2 }]);
+ engine.lastSync = 123;
+ engine.lastSyncLocal = 456;
+
+ ping = yield sync_engine_and_validate_telem(engine, true);
+ ok(!!ping);
+ equal(ping.engines.length, 1);
+ equal(ping.engines[0].incoming.reconciled, 1);
+ deepEqual(ping.engines[0].outgoing, [{ sent: 2, failed: 2 }]);
+
+ } finally {
+ yield cleanAndGo(server);
+ }
+});
+
+add_task(function *test_sync_partialUpload() {
+ Service.identity.username = "foo";
+
+ let collection = new ServerCollection();
+ let server = sync_httpd_setup({
+ "/1.1/foo/storage/rotary": collection.handler()
+ });
+ let syncTesting = new SyncTestingInfrastructure(server);
+ generateNewKeys(Service.collectionKeys);
+
+ let engine = new RotaryEngine(Service);
+ engine.lastSync = 123;
+ engine.lastSyncLocal = 456;
+
+
+ // Create a bunch of records (and server side handlers)
+ for (let i = 0; i < 234; i++) {
+ let id = 'record-no-' + i;
+ engine._store.items[id] = "Record No. " + i;
+ engine._tracker.addChangedID(id, i);
+ // Let two items in the first upload batch fail.
+ if (i != 23 && i != 42) {
+ collection.insert(id);
+ }
+ }
+
+ let meta_global = Service.recordManager.set(engine.metaURL,
+ new WBORecord(engine.metaURL));
+ meta_global.payload.engines = {rotary: {version: engine.version,
+ syncID: engine.syncID}};
+
+ try {
+ engine.enabled = true;
+ let ping = yield sync_engine_and_validate_telem(engine, true);
+
+ ok(!!ping);
+ ok(!ping.failureReason);
+ equal(ping.engines.length, 1);
+ equal(ping.engines[0].name, "rotary");
+ ok(!ping.engines[0].incoming);
+ ok(!ping.engines[0].failureReason);
+ deepEqual(ping.engines[0].outgoing, [{ sent: 234, failed: 2 }]);
+
+ collection.post = function() { throw "Failure"; }
+
+ engine._store.items["record-no-1000"] = "Record No. 1000";
+ engine._tracker.addChangedID("record-no-1000", 1000);
+ collection.insert("record-no-1000", 1000);
+
+ engine.lastSync = 123;
+ engine.lastSyncLocal = 456;
+ ping = null;
+
+ try {
+ // should throw
+ yield sync_engine_and_validate_telem(engine, true, errPing => ping = errPing);
+ } catch (e) {}
+ // It would be nice if we had a more descriptive error for this...
+ let uploadFailureError = {
+ name: "othererror",
+ error: "error.engine.reason.record_upload_fail"
+ };
+
+ ok(!!ping);
+ deepEqual(ping.failureReason, uploadFailureError);
+ equal(ping.engines.length, 1);
+ equal(ping.engines[0].name, "rotary");
+ deepEqual(ping.engines[0].incoming, {
+ failed: 1,
+ newFailed: 1,
+ reconciled: 232
+ });
+ ok(!ping.engines[0].outgoing);
+ deepEqual(ping.engines[0].failureReason, uploadFailureError);
+
+ } finally {
+ yield cleanAndGo(server);
+ }
+});
+
+add_task(function* test_generic_engine_fail() {
+ Service.engineManager.register(SteamEngine);
+ let engine = Service.engineManager.get("steam");
+ engine.enabled = true;
+ let store = engine._store;
+ let server = serverForUsers({"foo": "password"}, {
+ meta: {global: {engines: {steam: {version: engine.version,
+ syncID: engine.syncID}}}},
+ steam: {}
+ });
+ new SyncTestingInfrastructure(server.server);
+ let e = new Error("generic failure message")
+ engine._errToThrow = e;
+
+ try {
+ let ping = yield sync_and_validate_telem(true);
+ equal(ping.status.service, SYNC_FAILED_PARTIAL);
+ deepEqual(ping.engines.find(e => e.name === "steam").failureReason, {
+ name: "unexpectederror",
+ error: String(e)
+ });
+ } finally {
+ Service.engineManager.unregister(engine);
+ yield cleanAndGo(server);
+ }
+});
+
+add_task(function* test_engine_fail_ioerror() {
+ Service.engineManager.register(SteamEngine);
+ let engine = Service.engineManager.get("steam");
+ engine.enabled = true;
+ let store = engine._store;
+ let server = serverForUsers({"foo": "password"}, {
+ meta: {global: {engines: {steam: {version: engine.version,
+ syncID: engine.syncID}}}},
+ steam: {}
+ });
+ new SyncTestingInfrastructure(server.server);
+ // create an IOError to re-throw as part of Sync.
+ try {
+ // (Note that fakeservices.js has replaced Utils.jsonMove etc, but for
+ // this test we need the real one so we get real exceptions from the
+ // filesystem.)
+ yield Utils._real_jsonMove("file-does-not-exist", "anything", {});
+ } catch (ex) {
+ engine._errToThrow = ex;
+ }
+ ok(engine._errToThrow, "expecting exception");
+
+ try {
+ let ping = yield sync_and_validate_telem(true);
+ equal(ping.status.service, SYNC_FAILED_PARTIAL);
+ let failureReason = ping.engines.find(e => e.name === "steam").failureReason;
+ equal(failureReason.name, "unexpectederror");
+ // ensure the profile dir in the exception message has been stripped.
+ ok(!failureReason.error.includes(OS.Constants.Path.profileDir), failureReason.error);
+ ok(failureReason.error.includes("[profileDir]"), failureReason.error);
+ } finally {
+ Service.engineManager.unregister(engine);
+ yield cleanAndGo(server);
+ }
+});
+
+add_task(function* test_initial_sync_engines() {
+ Service.engineManager.register(SteamEngine);
+ let engine = Service.engineManager.get("steam");
+ engine.enabled = true;
+ let store = engine._store;
+ let engines = {};
+ // These are the only ones who actually have things to sync at startup.
+ let engineNames = ["clients", "bookmarks", "prefs", "tabs"];
+ let conf = { meta: { global: { engines } } };
+ for (let e of engineNames) {
+ engines[e] = { version: engine.version, syncID: engine.syncID };
+ conf[e] = {};
+ }
+ let server = serverForUsers({"foo": "password"}, conf);
+ new SyncTestingInfrastructure(server.server);
+ try {
+ let ping = yield wait_for_ping(() => Service.sync(), true);
+
+ equal(ping.engines.find(e => e.name === "clients").outgoing[0].sent, 1);
+ equal(ping.engines.find(e => e.name === "tabs").outgoing[0].sent, 1);
+
+ // for the rest we don't care about specifics
+ for (let e of ping.engines) {
+ if (!engineNames.includes(engine.name)) {
+ continue;
+ }
+ greaterOrEqual(e.took, 1);
+ ok(!!e.outgoing)
+ equal(e.outgoing.length, 1);
+ notEqual(e.outgoing[0].sent, undefined);
+ equal(e.outgoing[0].failed, undefined);
+ }
+ } finally {
+ yield cleanAndGo(server);
+ }
+});
+
+add_task(function* test_nserror() {
+ Service.engineManager.register(SteamEngine);
+ let engine = Service.engineManager.get("steam");
+ engine.enabled = true;
+ let store = engine._store;
+ let server = serverForUsers({"foo": "password"}, {
+ meta: {global: {engines: {steam: {version: engine.version,
+ syncID: engine.syncID}}}},
+ steam: {}
+ });
+ new SyncTestingInfrastructure(server.server);
+ engine._errToThrow = Components.Exception("NS_ERROR_UNKNOWN_HOST", Cr.NS_ERROR_UNKNOWN_HOST);
+ try {
+ let ping = yield sync_and_validate_telem(true);
+ deepEqual(ping.status, {
+ service: SYNC_FAILED_PARTIAL,
+ sync: LOGIN_FAILED_NETWORK_ERROR
+ });
+ let enginePing = ping.engines.find(e => e.name === "steam");
+ deepEqual(enginePing.failureReason, {
+ name: "nserror",
+ code: Cr.NS_ERROR_UNKNOWN_HOST
+ });
+ } finally {
+ Service.engineManager.unregister(engine);
+ yield cleanAndGo(server);
+ }
+});
+
+add_identity_test(this, function *test_discarding() {
+ let helper = track_collections_helper();
+ let upd = helper.with_updated_collection;
+ let telem = get_sync_test_telemetry();
+ telem.maxPayloadCount = 2;
+ telem.submissionInterval = Infinity;
+ let oldSubmit = telem.submit;
+
+ let server;
+ try {
+
+ yield configureIdentity({ username: "johndoe" });
+ let handlers = {
+ "/1.1/johndoe/info/collections": helper.handler,
+ "/1.1/johndoe/storage/crypto/keys": upd("crypto", new ServerWBO("keys").handler()),
+ "/1.1/johndoe/storage/meta/global": upd("meta", new ServerWBO("global").handler())
+ };
+
+ let collections = ["clients", "bookmarks", "forms", "history", "passwords", "prefs", "tabs"];
+
+ for (let coll of collections) {
+ handlers["/1.1/johndoe/storage/" + coll] = upd(coll, new ServerCollection({}, true).handler());
+ }
+
+ server = httpd_setup(handlers);
+ Service.serverURL = server.baseURI;
+ telem.submit = () => ok(false, "Submitted telemetry ping when we should not have");
+
+ for (let i = 0; i < 5; ++i) {
+ Service.sync();
+ }
+ telem.submit = oldSubmit;
+ telem.submissionInterval = -1;
+ let ping = yield sync_and_validate_telem(true, true); // with this we've synced 6 times
+ equal(ping.syncs.length, 2);
+ equal(ping.discarded, 4);
+ } finally {
+ telem.maxPayloadCount = 500;
+ telem.submissionInterval = -1;
+ telem.submit = oldSubmit;
+ if (server) {
+ yield new Promise(resolve => server.stop(resolve));
+ }
+ }
+})
+
+add_task(function* test_no_foreign_engines_in_error_ping() {
+ Service.engineManager.register(BogusEngine);
+ let engine = Service.engineManager.get("bogus");
+ engine.enabled = true;
+ let store = engine._store;
+ let server = serverForUsers({"foo": "password"}, {
+ meta: {global: {engines: {bogus: {version: engine.version, syncID: engine.syncID}}}},
+ steam: {}
+ });
+ engine._errToThrow = new Error("Oh no!");
+ new SyncTestingInfrastructure(server.server);
+ try {
+ let ping = yield sync_and_validate_telem(true);
+ equal(ping.status.service, SYNC_FAILED_PARTIAL);
+ ok(ping.engines.every(e => e.name !== "bogus"));
+ } finally {
+ Service.engineManager.unregister(engine);
+ yield cleanAndGo(server);
+ }
+});
+
+add_task(function* test_sql_error() {
+ Service.engineManager.register(SteamEngine);
+ let engine = Service.engineManager.get("steam");
+ engine.enabled = true;
+ let store = engine._store;
+ let server = serverForUsers({"foo": "password"}, {
+ meta: {global: {engines: {steam: {version: engine.version,
+ syncID: engine.syncID}}}},
+ steam: {}
+ });
+ new SyncTestingInfrastructure(server.server);
+ engine._sync = function() {
+ // Just grab a DB connection and issue a bogus SQL statement synchronously.
+ let db = PlacesUtils.history.QueryInterface(Ci.nsPIPlacesDatabase).DBConnection;
+ Async.querySpinningly(db.createAsyncStatement("select bar from foo"));
+ };
+ try {
+ let ping = yield sync_and_validate_telem(true);
+ let enginePing = ping.engines.find(e => e.name === "steam");
+ deepEqual(enginePing.failureReason, { name: "sqlerror", code: 1 });
+ } finally {
+ Service.engineManager.unregister(engine);
+ yield cleanAndGo(server);
+ }
+});
+
+add_task(function* test_no_foreign_engines_in_success_ping() {
+ Service.engineManager.register(BogusEngine);
+ let engine = Service.engineManager.get("bogus");
+ engine.enabled = true;
+ let store = engine._store;
+ let server = serverForUsers({"foo": "password"}, {
+ meta: {global: {engines: {bogus: {version: engine.version, syncID: engine.syncID}}}},
+ steam: {}
+ });
+
+ new SyncTestingInfrastructure(server.server);
+ try {
+ let ping = yield sync_and_validate_telem();
+ ok(ping.engines.every(e => e.name !== "bogus"));
+ } finally {
+ Service.engineManager.unregister(engine);
+ yield cleanAndGo(server);
+ }
+}); \ No newline at end of file