/* 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); } });