services/sync/tests/unit/test_syncengine_sync.js

Wed, 31 Dec 2014 06:09:35 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 06:09:35 +0100
changeset 0
6474c204b198
permissions
-rw-r--r--

Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.

     1 /* Any copyright is dedicated to the Public Domain.
     2  * http://creativecommons.org/publicdomain/zero/1.0/ */
     4 Cu.import("resource://services-sync/constants.js");
     5 Cu.import("resource://services-sync/engines.js");
     6 Cu.import("resource://services-sync/policies.js");
     7 Cu.import("resource://services-sync/record.js");
     8 Cu.import("resource://services-sync/resource.js");
     9 Cu.import("resource://services-sync/service.js");
    10 Cu.import("resource://services-sync/util.js");
    11 Cu.import("resource://testing-common/services/sync/rotaryengine.js");
    12 Cu.import("resource://testing-common/services/sync/utils.js");
    14 function makeRotaryEngine() {
    15   return new RotaryEngine(Service);
    16 }
    18 function cleanAndGo(server) {
    19   Svc.Prefs.resetBranch("");
    20   Svc.Prefs.set("log.logger.engine.rotary", "Trace");
    21   Service.recordManager.clearCache();
    22   server.stop(run_next_test);
    23 }
    25 function configureService(server, username, password) {
    26   Service.clusterURL = server.baseURI;
    28   Service.identity.account = username || "foo";
    29   Service.identity.basicPassword = password || "password";
    30 }
    32 function createServerAndConfigureClient() {
    33   let engine = new RotaryEngine(Service);
    35   let contents = {
    36     meta: {global: {engines: {rotary: {version: engine.version,
    37                                        syncID:  engine.syncID}}}},
    38     crypto: {},
    39     rotary: {}
    40   };
    42   const USER = "foo";
    43   let server = new SyncServer();
    44   server.registerUser(USER, "password");
    45   server.createContents(USER, contents);
    46   server.start();
    48   Service.serverURL = server.baseURI;
    49   Service.clusterURL = server.baseURI;
    50   Service.identity.username = USER;
    51   Service._updateCachedURLs();
    53   return [engine, server, USER];
    54 }
    56 function run_test() {
    57   generateNewKeys(Service.collectionKeys);
    58   Svc.Prefs.set("log.logger.engine.rotary", "Trace");
    59   run_next_test();
    60 }
    62 /*
    63  * Tests
    64  *
    65  * SyncEngine._sync() is divided into four rather independent steps:
    66  *
    67  * - _syncStartup()
    68  * - _processIncoming()
    69  * - _uploadOutgoing()
    70  * - _syncFinish()
    71  *
    72  * In the spirit of unit testing, these are tested individually for
    73  * different scenarios below.
    74  */
    76 add_test(function test_syncStartup_emptyOrOutdatedGlobalsResetsSync() {
    77   _("SyncEngine._syncStartup resets sync and wipes server data if there's no or an outdated global record");
    79   // Some server side data that's going to be wiped
    80   let collection = new ServerCollection();
    81   collection.insert('flying',
    82                     encryptPayload({id: 'flying',
    83                                     denomination: "LNER Class A3 4472"}));
    84   collection.insert('scotsman',
    85                     encryptPayload({id: 'scotsman',
    86                                     denomination: "Flying Scotsman"}));
    88   let server = sync_httpd_setup({
    89       "/1.1/foo/storage/rotary": collection.handler()
    90   });
    92   let syncTesting = new SyncTestingInfrastructure(server);
    93   Service.identity.username = "foo";
    95   let engine = makeRotaryEngine();
    96   engine._store.items = {rekolok: "Rekonstruktionslokomotive"};
    97   try {
    99     // Confirm initial environment
   100     do_check_eq(engine._tracker.changedIDs["rekolok"], undefined);
   101     let metaGlobal = Service.recordManager.get(engine.metaURL);
   102     do_check_eq(metaGlobal.payload.engines, undefined);
   103     do_check_true(!!collection.payload("flying"));
   104     do_check_true(!!collection.payload("scotsman"));
   106     engine.lastSync = Date.now() / 1000;
   107     engine.lastSyncLocal = Date.now();
   109     // Trying to prompt a wipe -- we no longer track CryptoMeta per engine,
   110     // so it has nothing to check.
   111     engine._syncStartup();
   113     // The meta/global WBO has been filled with data about the engine
   114     let engineData = metaGlobal.payload.engines["rotary"];
   115     do_check_eq(engineData.version, engine.version);
   116     do_check_eq(engineData.syncID, engine.syncID);
   118     // Sync was reset and server data was wiped
   119     do_check_eq(engine.lastSync, 0);
   120     do_check_eq(collection.payload("flying"), undefined);
   121     do_check_eq(collection.payload("scotsman"), undefined);
   123   } finally {
   124     cleanAndGo(server);
   125   }
   126 });
   128 add_test(function test_syncStartup_serverHasNewerVersion() {
   129   _("SyncEngine._syncStartup ");
   131   let global = new ServerWBO('global', {engines: {rotary: {version: 23456}}});
   132   let server = httpd_setup({
   133       "/1.1/foo/storage/meta/global": global.handler()
   134   });
   136   let syncTesting = new SyncTestingInfrastructure(server);
   137   Service.identity.username = "foo";
   139   let engine = makeRotaryEngine();
   140   try {
   142     // The server has a newer version of the data and our engine can
   143     // handle.  That should give us an exception.
   144     let error;
   145     try {
   146       engine._syncStartup();
   147     } catch (ex) {
   148       error = ex;
   149     }
   150     do_check_eq(error.failureCode, VERSION_OUT_OF_DATE);
   152   } finally {
   153     cleanAndGo(server);
   154   }
   155 });
   158 add_test(function test_syncStartup_syncIDMismatchResetsClient() {
   159   _("SyncEngine._syncStartup resets sync if syncIDs don't match");
   161   let server = sync_httpd_setup({});
   162   let syncTesting = new SyncTestingInfrastructure(server);
   163   Service.identity.username = "foo";
   165   // global record with a different syncID than our engine has
   166   let engine = makeRotaryEngine();
   167   let global = new ServerWBO('global',
   168                              {engines: {rotary: {version: engine.version,
   169                                                 syncID: 'foobar'}}});
   170   server.registerPathHandler("/1.1/foo/storage/meta/global", global.handler());
   172   try {
   174     // Confirm initial environment
   175     do_check_eq(engine.syncID, 'fake-guid-0');
   176     do_check_eq(engine._tracker.changedIDs["rekolok"], undefined);
   178     engine.lastSync = Date.now() / 1000;
   179     engine.lastSyncLocal = Date.now();
   180     engine._syncStartup();
   182     // The engine has assumed the server's syncID
   183     do_check_eq(engine.syncID, 'foobar');
   185     // Sync was reset
   186     do_check_eq(engine.lastSync, 0);
   188   } finally {
   189     cleanAndGo(server);
   190   }
   191 });
   194 add_test(function test_processIncoming_emptyServer() {
   195   _("SyncEngine._processIncoming working with an empty server backend");
   197   let collection = new ServerCollection();
   198   let server = sync_httpd_setup({
   199       "/1.1/foo/storage/rotary": collection.handler()
   200   });
   202   let syncTesting = new SyncTestingInfrastructure(server);
   203   Service.identity.username = "foo";
   205   let engine = makeRotaryEngine();
   206   try {
   208     // Merely ensure that this code path is run without any errors
   209     engine._processIncoming();
   210     do_check_eq(engine.lastSync, 0);
   212   } finally {
   213     cleanAndGo(server);
   214   }
   215 });
   218 add_test(function test_processIncoming_createFromServer() {
   219   _("SyncEngine._processIncoming creates new records from server data");
   221   // Some server records that will be downloaded
   222   let collection = new ServerCollection();
   223   collection.insert('flying',
   224                     encryptPayload({id: 'flying',
   225                                     denomination: "LNER Class A3 4472"}));
   226   collection.insert('scotsman',
   227                     encryptPayload({id: 'scotsman',
   228                                     denomination: "Flying Scotsman"}));
   230   // Two pathological cases involving relative URIs gone wrong.
   231   let pathologicalPayload = encryptPayload({id: '../pathological',
   232                                             denomination: "Pathological Case"});
   233   collection.insert('../pathological', pathologicalPayload);
   235   let server = sync_httpd_setup({
   236       "/1.1/foo/storage/rotary": collection.handler(),
   237       "/1.1/foo/storage/rotary/flying": collection.wbo("flying").handler(),
   238       "/1.1/foo/storage/rotary/scotsman": collection.wbo("scotsman").handler()
   239   });
   241   let syncTesting = new SyncTestingInfrastructure(server);
   242   Service.identity.username = "foo";
   244   generateNewKeys(Service.collectionKeys);
   246   let engine = makeRotaryEngine();
   247   let meta_global = Service.recordManager.set(engine.metaURL,
   248                                               new WBORecord(engine.metaURL));
   249   meta_global.payload.engines = {rotary: {version: engine.version,
   250                                          syncID: engine.syncID}};
   252   try {
   254     // Confirm initial environment
   255     do_check_eq(engine.lastSync, 0);
   256     do_check_eq(engine.lastModified, null);
   257     do_check_eq(engine._store.items.flying, undefined);
   258     do_check_eq(engine._store.items.scotsman, undefined);
   259     do_check_eq(engine._store.items['../pathological'], undefined);
   261     engine._syncStartup();
   262     engine._processIncoming();
   264     // Timestamps of last sync and last server modification are set.
   265     do_check_true(engine.lastSync > 0);
   266     do_check_true(engine.lastModified > 0);
   268     // Local records have been created from the server data.
   269     do_check_eq(engine._store.items.flying, "LNER Class A3 4472");
   270     do_check_eq(engine._store.items.scotsman, "Flying Scotsman");
   271     do_check_eq(engine._store.items['../pathological'], "Pathological Case");
   273   } finally {
   274     cleanAndGo(server);
   275   }
   276 });
   279 add_test(function test_processIncoming_reconcile() {
   280   _("SyncEngine._processIncoming updates local records");
   282   let collection = new ServerCollection();
   284   // This server record is newer than the corresponding client one,
   285   // so it'll update its data.
   286   collection.insert('newrecord',
   287                     encryptPayload({id: 'newrecord',
   288                                     denomination: "New stuff..."}));
   290   // This server record is newer than the corresponding client one,
   291   // so it'll update its data.
   292   collection.insert('newerserver',
   293                     encryptPayload({id: 'newerserver',
   294                                     denomination: "New data!"}));
   296   // This server record is 2 mins older than the client counterpart
   297   // but identical to it, so we're expecting the client record's
   298   // changedID to be reset.
   299   collection.insert('olderidentical',
   300                     encryptPayload({id: 'olderidentical',
   301                                     denomination: "Older but identical"}));
   302   collection._wbos.olderidentical.modified -= 120;
   304   // This item simply has different data than the corresponding client
   305   // record (which is unmodified), so it will update the client as well
   306   collection.insert('updateclient',
   307                     encryptPayload({id: 'updateclient',
   308                                     denomination: "Get this!"}));
   310   // This is a dupe of 'original'.
   311   collection.insert('duplication',
   312                     encryptPayload({id: 'duplication',
   313                                     denomination: "Original Entry"}));
   315   // This record is marked as deleted, so we're expecting the client
   316   // record to be removed.
   317   collection.insert('nukeme',
   318                     encryptPayload({id: 'nukeme',
   319                                     denomination: "Nuke me!",
   320                                     deleted: true}));
   322   let server = sync_httpd_setup({
   323       "/1.1/foo/storage/rotary": collection.handler()
   324   });
   326   let syncTesting = new SyncTestingInfrastructure(server);
   327   Service.identity.username = "foo";
   329   let engine = makeRotaryEngine();
   330   engine._store.items = {newerserver: "New data, but not as new as server!",
   331                          olderidentical: "Older but identical",
   332                          updateclient: "Got data?",
   333                          original: "Original Entry",
   334                          long_original: "Long Original Entry",
   335                          nukeme: "Nuke me!"};
   336   // Make this record 1 min old, thus older than the one on the server
   337   engine._tracker.addChangedID('newerserver', Date.now()/1000 - 60);
   338   // This record has been changed 2 mins later than the one on the server
   339   engine._tracker.addChangedID('olderidentical', Date.now()/1000);
   341   let meta_global = Service.recordManager.set(engine.metaURL,
   342                                               new WBORecord(engine.metaURL));
   343   meta_global.payload.engines = {rotary: {version: engine.version,
   344                                          syncID: engine.syncID}};
   346   try {
   348     // Confirm initial environment
   349     do_check_eq(engine._store.items.newrecord, undefined);
   350     do_check_eq(engine._store.items.newerserver, "New data, but not as new as server!");
   351     do_check_eq(engine._store.items.olderidentical, "Older but identical");
   352     do_check_eq(engine._store.items.updateclient, "Got data?");
   353     do_check_eq(engine._store.items.nukeme, "Nuke me!");
   354     do_check_true(engine._tracker.changedIDs['olderidentical'] > 0);
   356     engine._syncStartup();
   357     engine._processIncoming();
   359     // Timestamps of last sync and last server modification are set.
   360     do_check_true(engine.lastSync > 0);
   361     do_check_true(engine.lastModified > 0);
   363     // The new record is created.
   364     do_check_eq(engine._store.items.newrecord, "New stuff...");
   366     // The 'newerserver' record is updated since the server data is newer.
   367     do_check_eq(engine._store.items.newerserver, "New data!");
   369     // The data for 'olderidentical' is identical on the server, so
   370     // it's no longer marked as changed anymore.
   371     do_check_eq(engine._store.items.olderidentical, "Older but identical");
   372     do_check_eq(engine._tracker.changedIDs['olderidentical'], undefined);
   374     // Updated with server data.
   375     do_check_eq(engine._store.items.updateclient, "Get this!");
   377     // The incoming ID is preferred.
   378     do_check_eq(engine._store.items.original, undefined);
   379     do_check_eq(engine._store.items.duplication, "Original Entry");
   380     do_check_neq(engine._delete.ids.indexOf("original"), -1);
   382     // The 'nukeme' record marked as deleted is removed.
   383     do_check_eq(engine._store.items.nukeme, undefined);
   384   } finally {
   385     cleanAndGo(server);
   386   }
   387 });
   389 add_test(function test_processIncoming_reconcile_local_deleted() {
   390   _("Ensure local, duplicate ID is deleted on server.");
   392   // When a duplicate is resolved, the local ID (which is never taken) should
   393   // be deleted on the server.
   394   let [engine, server, user] = createServerAndConfigureClient();
   396   let now = Date.now() / 1000 - 10;
   397   engine.lastSync = now;
   398   engine.lastModified = now + 1;
   400   let record = encryptPayload({id: "DUPE_INCOMING", denomination: "incoming"});
   401   let wbo = new ServerWBO("DUPE_INCOMING", record, now + 2);
   402   server.insertWBO(user, "rotary", wbo);
   404   let record = encryptPayload({id: "DUPE_LOCAL", denomination: "local"});
   405   let wbo = new ServerWBO("DUPE_LOCAL", record, now - 1);
   406   server.insertWBO(user, "rotary", wbo);
   408   engine._store.create({id: "DUPE_LOCAL", denomination: "local"});
   409   do_check_true(engine._store.itemExists("DUPE_LOCAL"));
   410   do_check_eq("DUPE_LOCAL", engine._findDupe({id: "DUPE_INCOMING"}));
   412   engine._sync();
   414   do_check_attribute_count(engine._store.items, 1);
   415   do_check_true("DUPE_INCOMING" in engine._store.items);
   417   let collection = server.getCollection(user, "rotary");
   418   do_check_eq(1, collection.count());
   419   do_check_neq(undefined, collection.wbo("DUPE_INCOMING"));
   421   cleanAndGo(server);
   422 });
   424 add_test(function test_processIncoming_reconcile_equivalent() {
   425   _("Ensure proper handling of incoming records that match local.");
   427   let [engine, server, user] = createServerAndConfigureClient();
   429   let now = Date.now() / 1000 - 10;
   430   engine.lastSync = now;
   431   engine.lastModified = now + 1;
   433   let record = encryptPayload({id: "entry", denomination: "denomination"});
   434   let wbo = new ServerWBO("entry", record, now + 2);
   435   server.insertWBO(user, "rotary", wbo);
   437   engine._store.items = {entry: "denomination"};
   438   do_check_true(engine._store.itemExists("entry"));
   440   engine._sync();
   442   do_check_attribute_count(engine._store.items, 1);
   444   cleanAndGo(server);
   445 });
   447 add_test(function test_processIncoming_reconcile_locally_deleted_dupe_new() {
   448   _("Ensure locally deleted duplicate record newer than incoming is handled.");
   450   // This is a somewhat complicated test. It ensures that if a client receives
   451   // a modified record for an item that is deleted locally but with a different
   452   // ID that the incoming record is ignored. This is a corner case for record
   453   // handling, but it needs to be supported.
   454   let [engine, server, user] = createServerAndConfigureClient();
   456   let now = Date.now() / 1000 - 10;
   457   engine.lastSync = now;
   458   engine.lastModified = now + 1;
   460   let record = encryptPayload({id: "DUPE_INCOMING", denomination: "incoming"});
   461   let wbo = new ServerWBO("DUPE_INCOMING", record, now + 2);
   462   server.insertWBO(user, "rotary", wbo);
   464   // Simulate a locally-deleted item.
   465   engine._store.items = {};
   466   engine._tracker.addChangedID("DUPE_LOCAL", now + 3);
   467   do_check_false(engine._store.itemExists("DUPE_LOCAL"));
   468   do_check_false(engine._store.itemExists("DUPE_INCOMING"));
   469   do_check_eq("DUPE_LOCAL", engine._findDupe({id: "DUPE_INCOMING"}));
   471   engine._sync();
   473   // After the sync, the server's payload for the original ID should be marked
   474   // as deleted.
   475   do_check_empty(engine._store.items);
   476   let collection = server.getCollection(user, "rotary");
   477   do_check_eq(1, collection.count());
   478   let wbo = collection.wbo("DUPE_INCOMING");
   479   do_check_neq(null, wbo);
   480   let payload = JSON.parse(JSON.parse(wbo.payload).ciphertext);
   481   do_check_true(payload.deleted);
   483   cleanAndGo(server);
   484 });
   486 add_test(function test_processIncoming_reconcile_locally_deleted_dupe_old() {
   487   _("Ensure locally deleted duplicate record older than incoming is restored.");
   489   // This is similar to the above test except it tests the condition where the
   490   // incoming record is newer than the local deletion, therefore overriding it.
   492   let [engine, server, user] = createServerAndConfigureClient();
   494   let now = Date.now() / 1000 - 10;
   495   engine.lastSync = now;
   496   engine.lastModified = now + 1;
   498   let record = encryptPayload({id: "DUPE_INCOMING", denomination: "incoming"});
   499   let wbo = new ServerWBO("DUPE_INCOMING", record, now + 2);
   500   server.insertWBO(user, "rotary", wbo);
   502   // Simulate a locally-deleted item.
   503   engine._store.items = {};
   504   engine._tracker.addChangedID("DUPE_LOCAL", now + 1);
   505   do_check_false(engine._store.itemExists("DUPE_LOCAL"));
   506   do_check_false(engine._store.itemExists("DUPE_INCOMING"));
   507   do_check_eq("DUPE_LOCAL", engine._findDupe({id: "DUPE_INCOMING"}));
   509   engine._sync();
   511   // Since the remote change is newer, the incoming item should exist locally.
   512   do_check_attribute_count(engine._store.items, 1);
   513   do_check_true("DUPE_INCOMING" in engine._store.items);
   514   do_check_eq("incoming", engine._store.items.DUPE_INCOMING);
   516   let collection = server.getCollection(user, "rotary");
   517   do_check_eq(1, collection.count());
   518   let wbo = collection.wbo("DUPE_INCOMING");
   519   let payload = JSON.parse(JSON.parse(wbo.payload).ciphertext);
   520   do_check_eq("incoming", payload.denomination);
   522   cleanAndGo(server);
   523 });
   525 add_test(function test_processIncoming_reconcile_changed_dupe() {
   526   _("Ensure that locally changed duplicate record is handled properly.");
   528   let [engine, server, user] = createServerAndConfigureClient();
   530   let now = Date.now() / 1000 - 10;
   531   engine.lastSync = now;
   532   engine.lastModified = now + 1;
   534   // The local record is newer than the incoming one, so it should be retained.
   535   let record = encryptPayload({id: "DUPE_INCOMING", denomination: "incoming"});
   536   let wbo = new ServerWBO("DUPE_INCOMING", record, now + 2);
   537   server.insertWBO(user, "rotary", wbo);
   539   engine._store.create({id: "DUPE_LOCAL", denomination: "local"});
   540   engine._tracker.addChangedID("DUPE_LOCAL", now + 3);
   541   do_check_true(engine._store.itemExists("DUPE_LOCAL"));
   542   do_check_eq("DUPE_LOCAL", engine._findDupe({id: "DUPE_INCOMING"}));
   544   engine._sync();
   546   // The ID should have been changed to incoming.
   547   do_check_attribute_count(engine._store.items, 1);
   548   do_check_true("DUPE_INCOMING" in engine._store.items);
   550   // On the server, the local ID should be deleted and the incoming ID should
   551   // have its payload set to what was in the local record.
   552   let collection = server.getCollection(user, "rotary");
   553   do_check_eq(1, collection.count());
   554   let wbo = collection.wbo("DUPE_INCOMING");
   555   do_check_neq(undefined, wbo);
   556   let payload = JSON.parse(JSON.parse(wbo.payload).ciphertext);
   557   do_check_eq("local", payload.denomination);
   559   cleanAndGo(server);
   560 });
   562 add_test(function test_processIncoming_reconcile_changed_dupe_new() {
   563   _("Ensure locally changed duplicate record older than incoming is ignored.");
   565   // This test is similar to the above except the incoming record is younger
   566   // than the local record. The incoming record should be authoritative.
   567   let [engine, server, user] = createServerAndConfigureClient();
   569   let now = Date.now() / 1000 - 10;
   570   engine.lastSync = now;
   571   engine.lastModified = now + 1;
   573   let record = encryptPayload({id: "DUPE_INCOMING", denomination: "incoming"});
   574   let wbo = new ServerWBO("DUPE_INCOMING", record, now + 2);
   575   server.insertWBO(user, "rotary", wbo);
   577   engine._store.create({id: "DUPE_LOCAL", denomination: "local"});
   578   engine._tracker.addChangedID("DUPE_LOCAL", now + 1);
   579   do_check_true(engine._store.itemExists("DUPE_LOCAL"));
   580   do_check_eq("DUPE_LOCAL", engine._findDupe({id: "DUPE_INCOMING"}));
   582   engine._sync();
   584   // The ID should have been changed to incoming.
   585   do_check_attribute_count(engine._store.items, 1);
   586   do_check_true("DUPE_INCOMING" in engine._store.items);
   588   // On the server, the local ID should be deleted and the incoming ID should
   589   // have its payload retained.
   590   let collection = server.getCollection(user, "rotary");
   591   do_check_eq(1, collection.count());
   592   let wbo = collection.wbo("DUPE_INCOMING");
   593   do_check_neq(undefined, wbo);
   594   let payload = JSON.parse(JSON.parse(wbo.payload).ciphertext);
   595   do_check_eq("incoming", payload.denomination);
   596   cleanAndGo(server);
   597 });
   599 add_test(function test_processIncoming_mobile_batchSize() {
   600   _("SyncEngine._processIncoming doesn't fetch everything at once on mobile clients");
   602   Svc.Prefs.set("client.type", "mobile");
   603   Service.identity.username = "foo";
   605   // A collection that logs each GET
   606   let collection = new ServerCollection();
   607   collection.get_log = [];
   608   collection._get = collection.get;
   609   collection.get = function (options) {
   610     this.get_log.push(options);
   611     return this._get(options);
   612   };
   614   // Let's create some 234 server side records. They're all at least
   615   // 10 minutes old.
   616   for (let i = 0; i < 234; i++) {
   617     let id = 'record-no-' + i;
   618     let payload = encryptPayload({id: id, denomination: "Record No. " + i});
   619     let wbo = new ServerWBO(id, payload);
   620     wbo.modified = Date.now()/1000 - 60*(i+10);
   621     collection.insertWBO(wbo);
   622   }
   624   let server = sync_httpd_setup({
   625       "/1.1/foo/storage/rotary": collection.handler()
   626   });
   628   let syncTesting = new SyncTestingInfrastructure(server);
   630   let engine = makeRotaryEngine();
   631   let meta_global = Service.recordManager.set(engine.metaURL,
   632                                               new WBORecord(engine.metaURL));
   633   meta_global.payload.engines = {rotary: {version: engine.version,
   634                                          syncID: engine.syncID}};
   636   try {
   638     _("On a mobile client, we get new records from the server in batches of 50.");
   639     engine._syncStartup();
   640     engine._processIncoming();
   641     do_check_attribute_count(engine._store.items, 234);
   642     do_check_true('record-no-0' in engine._store.items);
   643     do_check_true('record-no-49' in engine._store.items);
   644     do_check_true('record-no-50' in engine._store.items);
   645     do_check_true('record-no-233' in engine._store.items);
   647     // Verify that the right number of GET requests with the right
   648     // kind of parameters were made.
   649     do_check_eq(collection.get_log.length,
   650                 Math.ceil(234 / MOBILE_BATCH_SIZE) + 1);
   651     do_check_eq(collection.get_log[0].full, 1);
   652     do_check_eq(collection.get_log[0].limit, MOBILE_BATCH_SIZE);
   653     do_check_eq(collection.get_log[1].full, undefined);
   654     do_check_eq(collection.get_log[1].limit, undefined);
   655     for (let i = 1; i <= Math.floor(234 / MOBILE_BATCH_SIZE); i++) {
   656       do_check_eq(collection.get_log[i+1].full, 1);
   657       do_check_eq(collection.get_log[i+1].limit, undefined);
   658       if (i < Math.floor(234 / MOBILE_BATCH_SIZE))
   659         do_check_eq(collection.get_log[i+1].ids.length, MOBILE_BATCH_SIZE);
   660       else
   661         do_check_eq(collection.get_log[i+1].ids.length, 234 % MOBILE_BATCH_SIZE);
   662     }
   664   } finally {
   665     cleanAndGo(server);
   666   }
   667 });
   670 add_test(function test_processIncoming_store_toFetch() {
   671   _("If processIncoming fails in the middle of a batch on mobile, state is saved in toFetch and lastSync.");
   672   Service.identity.username = "foo";
   673   Svc.Prefs.set("client.type", "mobile");
   675   // A collection that throws at the fourth get.
   676   let collection = new ServerCollection();
   677   collection._get_calls = 0;
   678   collection._get = collection.get;
   679   collection.get = function() {
   680     this._get_calls += 1;
   681     if (this._get_calls > 3) {
   682       throw "Abort on fourth call!";
   683     }
   684     return this._get.apply(this, arguments);
   685   };
   687   // Let's create three batches worth of server side records.
   688   for (var i = 0; i < MOBILE_BATCH_SIZE * 3; i++) {
   689     let id = 'record-no-' + i;
   690     let payload = encryptPayload({id: id, denomination: "Record No. " + id});
   691     let wbo = new ServerWBO(id, payload);
   692     wbo.modified = Date.now()/1000 + 60 * (i - MOBILE_BATCH_SIZE * 3);
   693     collection.insertWBO(wbo);
   694   }
   696   let engine = makeRotaryEngine();
   697   engine.enabled = true;
   699   let server = sync_httpd_setup({
   700       "/1.1/foo/storage/rotary": collection.handler()
   701   });
   703   let syncTesting = new SyncTestingInfrastructure(server);
   705   let meta_global = Service.recordManager.set(engine.metaURL,
   706                                               new WBORecord(engine.metaURL));
   707   meta_global.payload.engines = {rotary: {version: engine.version,
   708                                          syncID: engine.syncID}};
   709   try {
   711     // Confirm initial environment
   712     do_check_eq(engine.lastSync, 0);
   713     do_check_empty(engine._store.items);
   715     let error;
   716     try {
   717       engine.sync();
   718     } catch (ex) {
   719       error = ex;
   720     }
   721     do_check_true(!!error);
   723     // Only the first two batches have been applied.
   724     do_check_eq(Object.keys(engine._store.items).length,
   725                 MOBILE_BATCH_SIZE * 2);
   727     // The third batch is stuck in toFetch. lastSync has been moved forward to
   728     // the last successful item's timestamp.
   729     do_check_eq(engine.toFetch.length, MOBILE_BATCH_SIZE);
   730     do_check_eq(engine.lastSync, collection.wbo("record-no-99").modified);
   732   } finally {
   733     cleanAndGo(server);
   734   }
   735 });
   738 add_test(function test_processIncoming_resume_toFetch() {
   739   _("toFetch and previousFailed items left over from previous syncs are fetched on the next sync, along with new items.");
   740   Service.identity.username = "foo";
   742   const LASTSYNC = Date.now() / 1000;
   744   // Server records that will be downloaded
   745   let collection = new ServerCollection();
   746   collection.insert('flying',
   747                     encryptPayload({id: 'flying',
   748                                     denomination: "LNER Class A3 4472"}));
   749   collection.insert('scotsman',
   750                     encryptPayload({id: 'scotsman',
   751                                     denomination: "Flying Scotsman"}));
   752   collection.insert('rekolok',
   753                     encryptPayload({id: 'rekolok',
   754                                     denomination: "Rekonstruktionslokomotive"}));
   755   for (let i = 0; i < 3; i++) {
   756     let id = 'failed' + i;
   757     let payload = encryptPayload({id: id, denomination: "Record No. " + i});
   758     let wbo = new ServerWBO(id, payload);
   759     wbo.modified = LASTSYNC - 10;
   760     collection.insertWBO(wbo);
   761   }
   763   collection.wbo("flying").modified =
   764     collection.wbo("scotsman").modified = LASTSYNC - 10;
   765   collection._wbos.rekolok.modified = LASTSYNC + 10;
   767   // Time travel 10 seconds into the future but still download the above WBOs.
   768   let engine = makeRotaryEngine();
   769   engine.lastSync = LASTSYNC;
   770   engine.toFetch = ["flying", "scotsman"];
   771   engine.previousFailed = ["failed0", "failed1", "failed2"];
   773   let server = sync_httpd_setup({
   774       "/1.1/foo/storage/rotary": collection.handler()
   775   });
   777   let syncTesting = new SyncTestingInfrastructure(server);
   779   let meta_global = Service.recordManager.set(engine.metaURL,
   780                                               new WBORecord(engine.metaURL));
   781   meta_global.payload.engines = {rotary: {version: engine.version,
   782                                          syncID: engine.syncID}};
   783   try {
   785     // Confirm initial environment
   786     do_check_eq(engine._store.items.flying, undefined);
   787     do_check_eq(engine._store.items.scotsman, undefined);
   788     do_check_eq(engine._store.items.rekolok, undefined);
   790     engine._syncStartup();
   791     engine._processIncoming();
   793     // Local records have been created from the server data.
   794     do_check_eq(engine._store.items.flying, "LNER Class A3 4472");
   795     do_check_eq(engine._store.items.scotsman, "Flying Scotsman");
   796     do_check_eq(engine._store.items.rekolok, "Rekonstruktionslokomotive");
   797     do_check_eq(engine._store.items.failed0, "Record No. 0");
   798     do_check_eq(engine._store.items.failed1, "Record No. 1");
   799     do_check_eq(engine._store.items.failed2, "Record No. 2");
   800     do_check_eq(engine.previousFailed.length, 0);
   801   } finally {
   802     cleanAndGo(server);
   803   }
   804 });
   807 add_test(function test_processIncoming_applyIncomingBatchSize_smaller() {
   808   _("Ensure that a number of incoming items less than applyIncomingBatchSize is still applied.");
   809   Service.identity.username = "foo";
   811   // Engine that doesn't like the first and last record it's given.
   812   const APPLY_BATCH_SIZE = 10;
   813   let engine = makeRotaryEngine();
   814   engine.applyIncomingBatchSize = APPLY_BATCH_SIZE;
   815   engine._store._applyIncomingBatch = engine._store.applyIncomingBatch;
   816   engine._store.applyIncomingBatch = function (records) {
   817     let failed1 = records.shift();
   818     let failed2 = records.pop();
   819     this._applyIncomingBatch(records);
   820     return [failed1.id, failed2.id];
   821   };
   823   // Let's create less than a batch worth of server side records.
   824   let collection = new ServerCollection();
   825   for (let i = 0; i < APPLY_BATCH_SIZE - 1; i++) {
   826     let id = 'record-no-' + i;
   827     let payload = encryptPayload({id: id, denomination: "Record No. " + id});
   828     collection.insert(id, payload);
   829   }
   831   let server = sync_httpd_setup({
   832       "/1.1/foo/storage/rotary": collection.handler()
   833   });
   835   let syncTesting = new SyncTestingInfrastructure(server);
   837   let meta_global = Service.recordManager.set(engine.metaURL,
   838                                               new WBORecord(engine.metaURL));
   839   meta_global.payload.engines = {rotary: {version: engine.version,
   840                                          syncID: engine.syncID}};
   841   try {
   843     // Confirm initial environment
   844     do_check_empty(engine._store.items);
   846     engine._syncStartup();
   847     engine._processIncoming();
   849     // Records have been applied and the expected failures have failed.
   850     do_check_attribute_count(engine._store.items, APPLY_BATCH_SIZE - 1 - 2);
   851     do_check_eq(engine.toFetch.length, 0);
   852     do_check_eq(engine.previousFailed.length, 2);
   853     do_check_eq(engine.previousFailed[0], "record-no-0");
   854     do_check_eq(engine.previousFailed[1], "record-no-8");
   856   } finally {
   857     cleanAndGo(server);
   858   }
   859 });
   862 add_test(function test_processIncoming_applyIncomingBatchSize_multiple() {
   863   _("Ensure that incoming items are applied according to applyIncomingBatchSize.");
   864   Service.identity.username = "foo";
   866   const APPLY_BATCH_SIZE = 10;
   868   // Engine that applies records in batches.
   869   let engine = makeRotaryEngine();
   870   engine.applyIncomingBatchSize = APPLY_BATCH_SIZE;
   871   let batchCalls = 0;
   872   engine._store._applyIncomingBatch = engine._store.applyIncomingBatch;
   873   engine._store.applyIncomingBatch = function (records) {
   874     batchCalls += 1;
   875     do_check_eq(records.length, APPLY_BATCH_SIZE);
   876     this._applyIncomingBatch.apply(this, arguments);
   877   };
   879   // Let's create three batches worth of server side records.
   880   let collection = new ServerCollection();
   881   for (let i = 0; i < APPLY_BATCH_SIZE * 3; i++) {
   882     let id = 'record-no-' + i;
   883     let payload = encryptPayload({id: id, denomination: "Record No. " + id});
   884     collection.insert(id, payload);
   885   }
   887   let server = sync_httpd_setup({
   888       "/1.1/foo/storage/rotary": collection.handler()
   889   });
   891   let syncTesting = new SyncTestingInfrastructure(server);
   893   let meta_global = Service.recordManager.set(engine.metaURL,
   894                                               new WBORecord(engine.metaURL));
   895   meta_global.payload.engines = {rotary: {version: engine.version,
   896                                          syncID: engine.syncID}};
   897   try {
   899     // Confirm initial environment
   900     do_check_empty(engine._store.items);
   902     engine._syncStartup();
   903     engine._processIncoming();
   905     // Records have been applied in 3 batches.
   906     do_check_eq(batchCalls, 3);
   907     do_check_attribute_count(engine._store.items, APPLY_BATCH_SIZE * 3);
   909   } finally {
   910     cleanAndGo(server);
   911   }
   912 });
   915 add_test(function test_processIncoming_notify_count() {
   916   _("Ensure that failed records are reported only once.");
   917   Service.identity.username = "foo";
   919   const APPLY_BATCH_SIZE = 5;
   920   const NUMBER_OF_RECORDS = 15;
   922   // Engine that fails the first record.
   923   let engine = makeRotaryEngine();
   924   engine.applyIncomingBatchSize = APPLY_BATCH_SIZE;
   925   engine._store._applyIncomingBatch = engine._store.applyIncomingBatch;
   926   engine._store.applyIncomingBatch = function (records) {
   927     engine._store._applyIncomingBatch(records.slice(1));
   928     return [records[0].id];
   929   };
   931   // Create a batch of server side records.
   932   let collection = new ServerCollection();
   933   for (var i = 0; i < NUMBER_OF_RECORDS; i++) {
   934     let id = 'record-no-' + i;
   935     let payload = encryptPayload({id: id, denomination: "Record No. " + id});
   936     collection.insert(id, payload);
   937   }
   939   let server = sync_httpd_setup({
   940       "/1.1/foo/storage/rotary": collection.handler()
   941   });
   943   let syncTesting = new SyncTestingInfrastructure(server);
   945   let meta_global = Service.recordManager.set(engine.metaURL,
   946                                               new WBORecord(engine.metaURL));
   947   meta_global.payload.engines = {rotary: {version: engine.version,
   948                                          syncID: engine.syncID}};
   949   try {
   950     // Confirm initial environment.
   951     do_check_eq(engine.lastSync, 0);
   952     do_check_eq(engine.toFetch.length, 0);
   953     do_check_eq(engine.previousFailed.length, 0);
   954     do_check_empty(engine._store.items);
   956     let called = 0;
   957     let counts;
   958     function onApplied(count) {
   959       _("Called with " + JSON.stringify(counts));
   960       counts = count;
   961       called++;
   962     }
   963     Svc.Obs.add("weave:engine:sync:applied", onApplied);
   965     // Do sync.
   966     engine._syncStartup();
   967     engine._processIncoming();
   969     // Confirm failures.
   970     do_check_attribute_count(engine._store.items, 12);
   971     do_check_eq(engine.previousFailed.length, 3);
   972     do_check_eq(engine.previousFailed[0], "record-no-0");
   973     do_check_eq(engine.previousFailed[1], "record-no-5");
   974     do_check_eq(engine.previousFailed[2], "record-no-10");
   976     // There are newly failed records and they are reported.
   977     do_check_eq(called, 1);
   978     do_check_eq(counts.failed, 3);
   979     do_check_eq(counts.applied, 15);
   980     do_check_eq(counts.newFailed, 3);
   981     do_check_eq(counts.succeeded, 12);
   983     // Sync again, 1 of the failed items are the same, the rest didn't fail.
   984     engine._processIncoming();
   986     // Confirming removed failures.
   987     do_check_attribute_count(engine._store.items, 14);
   988     do_check_eq(engine.previousFailed.length, 1);
   989     do_check_eq(engine.previousFailed[0], "record-no-0");
   991     do_check_eq(called, 2);
   992     do_check_eq(counts.failed, 1);
   993     do_check_eq(counts.applied, 3);
   994     do_check_eq(counts.newFailed, 0);
   995     do_check_eq(counts.succeeded, 2);
   997     Svc.Obs.remove("weave:engine:sync:applied", onApplied);
   998   } finally {
   999     cleanAndGo(server);
  1001 });
  1004 add_test(function test_processIncoming_previousFailed() {
  1005   _("Ensure that failed records are retried.");
  1006   Service.identity.username = "foo";
  1007   Svc.Prefs.set("client.type", "mobile");
  1009   const APPLY_BATCH_SIZE = 4;
  1010   const NUMBER_OF_RECORDS = 14;
  1012   // Engine that fails the first 2 records.
  1013   let engine = makeRotaryEngine();
  1014   engine.mobileGUIDFetchBatchSize = engine.applyIncomingBatchSize = APPLY_BATCH_SIZE;
  1015   engine._store._applyIncomingBatch = engine._store.applyIncomingBatch;
  1016   engine._store.applyIncomingBatch = function (records) {
  1017     engine._store._applyIncomingBatch(records.slice(2));
  1018     return [records[0].id, records[1].id];
  1019   };
  1021   // Create a batch of server side records.
  1022   let collection = new ServerCollection();
  1023   for (var i = 0; i < NUMBER_OF_RECORDS; i++) {
  1024     let id = 'record-no-' + i;
  1025     let payload = encryptPayload({id: id, denomination: "Record No. " + i});
  1026     collection.insert(id, payload);
  1029   let server = sync_httpd_setup({
  1030       "/1.1/foo/storage/rotary": collection.handler()
  1031   });
  1033   let syncTesting = new SyncTestingInfrastructure(server);
  1035   let meta_global = Service.recordManager.set(engine.metaURL,
  1036                                               new WBORecord(engine.metaURL));
  1037   meta_global.payload.engines = {rotary: {version: engine.version,
  1038                                          syncID: engine.syncID}};
  1039   try {
  1040     // Confirm initial environment.
  1041     do_check_eq(engine.lastSync, 0);
  1042     do_check_eq(engine.toFetch.length, 0);
  1043     do_check_eq(engine.previousFailed.length, 0);
  1044     do_check_empty(engine._store.items);
  1046     // Initial failed items in previousFailed to be reset.
  1047     let previousFailed = [Utils.makeGUID(), Utils.makeGUID(), Utils.makeGUID()];
  1048     engine.previousFailed = previousFailed;
  1049     do_check_eq(engine.previousFailed, previousFailed);
  1051     // Do sync.
  1052     engine._syncStartup();
  1053     engine._processIncoming();
  1055     // Expected result: 4 sync batches with 2 failures each => 8 failures
  1056     do_check_attribute_count(engine._store.items, 6);
  1057     do_check_eq(engine.previousFailed.length, 8);
  1058     do_check_eq(engine.previousFailed[0], "record-no-0");
  1059     do_check_eq(engine.previousFailed[1], "record-no-1");
  1060     do_check_eq(engine.previousFailed[2], "record-no-4");
  1061     do_check_eq(engine.previousFailed[3], "record-no-5");
  1062     do_check_eq(engine.previousFailed[4], "record-no-8");
  1063     do_check_eq(engine.previousFailed[5], "record-no-9");
  1064     do_check_eq(engine.previousFailed[6], "record-no-12");
  1065     do_check_eq(engine.previousFailed[7], "record-no-13");
  1067     // Sync again with the same failed items (records 0, 1, 8, 9).
  1068     engine._processIncoming();
  1070     // A second sync with the same failed items should not add the same items again.
  1071     // Items that did not fail a second time should no longer be in previousFailed.
  1072     do_check_attribute_count(engine._store.items, 10);
  1073     do_check_eq(engine.previousFailed.length, 4);
  1074     do_check_eq(engine.previousFailed[0], "record-no-0");
  1075     do_check_eq(engine.previousFailed[1], "record-no-1");
  1076     do_check_eq(engine.previousFailed[2], "record-no-8");
  1077     do_check_eq(engine.previousFailed[3], "record-no-9");
  1079     // Refetched items that didn't fail the second time are in engine._store.items.
  1080     do_check_eq(engine._store.items['record-no-4'], "Record No. 4");
  1081     do_check_eq(engine._store.items['record-no-5'], "Record No. 5");
  1082     do_check_eq(engine._store.items['record-no-12'], "Record No. 12");
  1083     do_check_eq(engine._store.items['record-no-13'], "Record No. 13");
  1084   } finally {
  1085     cleanAndGo(server);
  1087 });
  1090 add_test(function test_processIncoming_failed_records() {
  1091   _("Ensure that failed records from _reconcile and applyIncomingBatch are refetched.");
  1092   Service.identity.username = "foo";
  1094   // Let's create three and a bit batches worth of server side records.
  1095   let collection = new ServerCollection();
  1096   const NUMBER_OF_RECORDS = MOBILE_BATCH_SIZE * 3 + 5;
  1097   for (let i = 0; i < NUMBER_OF_RECORDS; i++) {
  1098     let id = 'record-no-' + i;
  1099     let payload = encryptPayload({id: id, denomination: "Record No. " + id});
  1100     let wbo = new ServerWBO(id, payload);
  1101     wbo.modified = Date.now()/1000 + 60 * (i - MOBILE_BATCH_SIZE * 3);
  1102     collection.insertWBO(wbo);
  1105   // Engine that batches but likes to throw on a couple of records,
  1106   // two in each batch: the even ones fail in reconcile, the odd ones
  1107   // in applyIncoming.
  1108   const BOGUS_RECORDS = ["record-no-" + 42,
  1109                          "record-no-" + 23,
  1110                          "record-no-" + (42 + MOBILE_BATCH_SIZE),
  1111                          "record-no-" + (23 + MOBILE_BATCH_SIZE),
  1112                          "record-no-" + (42 + MOBILE_BATCH_SIZE * 2),
  1113                          "record-no-" + (23 + MOBILE_BATCH_SIZE * 2),
  1114                          "record-no-" + (2 + MOBILE_BATCH_SIZE * 3),
  1115                          "record-no-" + (1 + MOBILE_BATCH_SIZE * 3)];
  1116   let engine = makeRotaryEngine();
  1117   engine.applyIncomingBatchSize = MOBILE_BATCH_SIZE;
  1119   engine.__reconcile = engine._reconcile;
  1120   engine._reconcile = function _reconcile(record) {
  1121     if (BOGUS_RECORDS.indexOf(record.id) % 2 == 0) {
  1122       throw "I don't like this record! Baaaaaah!";
  1124     return this.__reconcile.apply(this, arguments);
  1125   };
  1126   engine._store._applyIncoming = engine._store.applyIncoming;
  1127   engine._store.applyIncoming = function (record) {
  1128     if (BOGUS_RECORDS.indexOf(record.id) % 2 == 1) {
  1129       throw "I don't like this record! Baaaaaah!";
  1131     return this._applyIncoming.apply(this, arguments);
  1132   };
  1134   // Keep track of requests made of a collection.
  1135   let count = 0;
  1136   let uris  = [];
  1137   function recording_handler(collection) {
  1138     let h = collection.handler();
  1139     return function(req, res) {
  1140       ++count;
  1141       uris.push(req.path + "?" + req.queryString);
  1142       return h(req, res);
  1143     };
  1145   let server = sync_httpd_setup({
  1146       "/1.1/foo/storage/rotary": recording_handler(collection)
  1147   });
  1149   let syncTesting = new SyncTestingInfrastructure(server);
  1151   let meta_global = Service.recordManager.set(engine.metaURL,
  1152                                               new WBORecord(engine.metaURL));
  1153   meta_global.payload.engines = {rotary: {version: engine.version,
  1154                                          syncID: engine.syncID}};
  1156   try {
  1158     // Confirm initial environment
  1159     do_check_eq(engine.lastSync, 0);
  1160     do_check_eq(engine.toFetch.length, 0);
  1161     do_check_eq(engine.previousFailed.length, 0);
  1162     do_check_empty(engine._store.items);
  1164     let observerSubject;
  1165     let observerData;
  1166     Svc.Obs.add("weave:engine:sync:applied", function onApplied(subject, data) {
  1167       Svc.Obs.remove("weave:engine:sync:applied", onApplied);
  1168       observerSubject = subject;
  1169       observerData = data;
  1170     });
  1172     engine._syncStartup();
  1173     engine._processIncoming();
  1175     // Ensure that all records but the bogus 4 have been applied.
  1176     do_check_attribute_count(engine._store.items,
  1177                              NUMBER_OF_RECORDS - BOGUS_RECORDS.length);
  1179     // Ensure that the bogus records will be fetched again on the next sync.
  1180     do_check_eq(engine.previousFailed.length, BOGUS_RECORDS.length);
  1181     engine.previousFailed.sort();
  1182     BOGUS_RECORDS.sort();
  1183     for (let i = 0; i < engine.previousFailed.length; i++) {
  1184       do_check_eq(engine.previousFailed[i], BOGUS_RECORDS[i]);
  1187     // Ensure the observer was notified
  1188     do_check_eq(observerData, engine.name);
  1189     do_check_eq(observerSubject.failed, BOGUS_RECORDS.length);
  1190     do_check_eq(observerSubject.newFailed, BOGUS_RECORDS.length);
  1192     // Testing batching of failed item fetches.
  1193     // Try to sync again. Ensure that we split the request into chunks to avoid
  1194     // URI length limitations.
  1195     function batchDownload(batchSize) {
  1196       count = 0;
  1197       uris  = [];
  1198       engine.guidFetchBatchSize = batchSize;
  1199       engine._processIncoming();
  1200       _("Tried again. Requests: " + count + "; URIs: " + JSON.stringify(uris));
  1201       return count;
  1204     // There are 8 bad records, so this needs 3 fetches.
  1205     _("Test batching with ID batch size 3, normal mobile batch size.");
  1206     do_check_eq(batchDownload(3), 3);
  1208     // Now see with a more realistic limit.
  1209     _("Test batching with sufficient ID batch size.");
  1210     do_check_eq(batchDownload(BOGUS_RECORDS.length), 1);
  1212     // If we're on mobile, that limit is used by default.
  1213     _("Test batching with tiny mobile batch size.");
  1214     Svc.Prefs.set("client.type", "mobile");
  1215     engine.mobileGUIDFetchBatchSize = 2;
  1216     do_check_eq(batchDownload(BOGUS_RECORDS.length), 4);
  1218   } finally {
  1219     cleanAndGo(server);
  1221 });
  1224 add_test(function test_processIncoming_decrypt_failed() {
  1225   _("Ensure that records failing to decrypt are either replaced or refetched.");
  1227   Service.identity.username = "foo";
  1229   // Some good and some bogus records. One doesn't contain valid JSON,
  1230   // the other will throw during decrypt.
  1231   let collection = new ServerCollection();
  1232   collection._wbos.flying = new ServerWBO(
  1233       'flying', encryptPayload({id: 'flying',
  1234                                 denomination: "LNER Class A3 4472"}));
  1235   collection._wbos.nojson = new ServerWBO("nojson", "This is invalid JSON");
  1236   collection._wbos.nojson2 = new ServerWBO("nojson2", "This is invalid JSON");
  1237   collection._wbos.scotsman = new ServerWBO(
  1238       'scotsman', encryptPayload({id: 'scotsman',
  1239                                   denomination: "Flying Scotsman"}));
  1240   collection._wbos.nodecrypt = new ServerWBO("nodecrypt", "Decrypt this!");
  1241   collection._wbos.nodecrypt2 = new ServerWBO("nodecrypt2", "Decrypt this!");
  1243   // Patch the fake crypto service to throw on the record above.
  1244   Svc.Crypto._decrypt = Svc.Crypto.decrypt;
  1245   Svc.Crypto.decrypt = function (ciphertext) {
  1246     if (ciphertext == "Decrypt this!") {
  1247       throw "Derp! Cipher finalized failed. Im ur crypto destroyin ur recordz.";
  1249     return this._decrypt.apply(this, arguments);
  1250   };
  1252   // Some broken records also exist locally.
  1253   let engine = makeRotaryEngine();
  1254   engine.enabled = true;
  1255   engine._store.items = {nojson: "Valid JSON",
  1256                          nodecrypt: "Valid ciphertext"};
  1258   let server = sync_httpd_setup({
  1259       "/1.1/foo/storage/rotary": collection.handler()
  1260   });
  1262   let syncTesting = new SyncTestingInfrastructure(server);
  1264   let meta_global = Service.recordManager.set(engine.metaURL,
  1265                                               new WBORecord(engine.metaURL));
  1266   meta_global.payload.engines = {rotary: {version: engine.version,
  1267                                          syncID: engine.syncID}};
  1268   try {
  1270     // Confirm initial state
  1271     do_check_eq(engine.toFetch.length, 0);
  1272     do_check_eq(engine.previousFailed.length, 0);
  1274     let observerSubject;
  1275     let observerData;
  1276     Svc.Obs.add("weave:engine:sync:applied", function onApplied(subject, data) {
  1277       Svc.Obs.remove("weave:engine:sync:applied", onApplied);
  1278       observerSubject = subject;
  1279       observerData = data;
  1280     });
  1282     engine.lastSync = collection.wbo("nojson").modified - 1;
  1283     engine.sync();
  1285     do_check_eq(engine.previousFailed.length, 4);
  1286     do_check_eq(engine.previousFailed[0], "nojson");
  1287     do_check_eq(engine.previousFailed[1], "nojson2");
  1288     do_check_eq(engine.previousFailed[2], "nodecrypt");
  1289     do_check_eq(engine.previousFailed[3], "nodecrypt2");
  1291     // Ensure the observer was notified
  1292     do_check_eq(observerData, engine.name);
  1293     do_check_eq(observerSubject.applied, 2);
  1294     do_check_eq(observerSubject.failed, 4);
  1296   } finally {
  1297     cleanAndGo(server);
  1299 });
  1302 add_test(function test_uploadOutgoing_toEmptyServer() {
  1303   _("SyncEngine._uploadOutgoing uploads new records to server");
  1305   Service.identity.username = "foo";
  1306   let collection = new ServerCollection();
  1307   collection._wbos.flying = new ServerWBO('flying');
  1308   collection._wbos.scotsman = new ServerWBO('scotsman');
  1310   let server = sync_httpd_setup({
  1311       "/1.1/foo/storage/rotary": collection.handler(),
  1312       "/1.1/foo/storage/rotary/flying": collection.wbo("flying").handler(),
  1313       "/1.1/foo/storage/rotary/scotsman": collection.wbo("scotsman").handler()
  1314   });
  1316   let syncTesting = new SyncTestingInfrastructure(server);
  1317   generateNewKeys(Service.collectionKeys);
  1319   let engine = makeRotaryEngine();
  1320   engine.lastSync = 123; // needs to be non-zero so that tracker is queried
  1321   engine._store.items = {flying: "LNER Class A3 4472",
  1322                          scotsman: "Flying Scotsman"};
  1323   // Mark one of these records as changed
  1324   engine._tracker.addChangedID('scotsman', 0);
  1326   let meta_global = Service.recordManager.set(engine.metaURL,
  1327                                               new WBORecord(engine.metaURL));
  1328   meta_global.payload.engines = {rotary: {version: engine.version,
  1329                                          syncID: engine.syncID}};
  1331   try {
  1333     // Confirm initial environment
  1334     do_check_eq(engine.lastSyncLocal, 0);
  1335     do_check_eq(collection.payload("flying"), undefined);
  1336     do_check_eq(collection.payload("scotsman"), undefined);
  1338     engine._syncStartup();
  1339     engine._uploadOutgoing();
  1341     // Local timestamp has been set.
  1342     do_check_true(engine.lastSyncLocal > 0);
  1344     // Ensure the marked record ('scotsman') has been uploaded and is
  1345     // no longer marked.
  1346     do_check_eq(collection.payload("flying"), undefined);
  1347     do_check_true(!!collection.payload("scotsman"));
  1348     do_check_eq(JSON.parse(collection.wbo("scotsman").data.ciphertext).id,
  1349                 "scotsman");
  1350     do_check_eq(engine._tracker.changedIDs["scotsman"], undefined);
  1352     // The 'flying' record wasn't marked so it wasn't uploaded
  1353     do_check_eq(collection.payload("flying"), undefined);
  1355   } finally {
  1356     cleanAndGo(server);
  1358 });
  1361 add_test(function test_uploadOutgoing_failed() {
  1362   _("SyncEngine._uploadOutgoing doesn't clear the tracker of objects that failed to upload.");
  1364   Service.identity.username = "foo";
  1365   let collection = new ServerCollection();
  1366   // We only define the "flying" WBO on the server, not the "scotsman"
  1367   // and "peppercorn" ones.
  1368   collection._wbos.flying = new ServerWBO('flying');
  1370   let server = sync_httpd_setup({
  1371       "/1.1/foo/storage/rotary": collection.handler()
  1372   });
  1374   let syncTesting = new SyncTestingInfrastructure(server);
  1376   let engine = makeRotaryEngine();
  1377   engine.lastSync = 123; // needs to be non-zero so that tracker is queried
  1378   engine._store.items = {flying: "LNER Class A3 4472",
  1379                          scotsman: "Flying Scotsman",
  1380                          peppercorn: "Peppercorn Class"};
  1381   // Mark these records as changed
  1382   const FLYING_CHANGED = 12345;
  1383   const SCOTSMAN_CHANGED = 23456;
  1384   const PEPPERCORN_CHANGED = 34567;
  1385   engine._tracker.addChangedID('flying', FLYING_CHANGED);
  1386   engine._tracker.addChangedID('scotsman', SCOTSMAN_CHANGED);
  1387   engine._tracker.addChangedID('peppercorn', PEPPERCORN_CHANGED);
  1389   let meta_global = Service.recordManager.set(engine.metaURL,
  1390                                               new WBORecord(engine.metaURL));
  1391   meta_global.payload.engines = {rotary: {version: engine.version,
  1392                                          syncID: engine.syncID}};
  1394   try {
  1396     // Confirm initial environment
  1397     do_check_eq(engine.lastSyncLocal, 0);
  1398     do_check_eq(collection.payload("flying"), undefined);
  1399     do_check_eq(engine._tracker.changedIDs['flying'], FLYING_CHANGED);
  1400     do_check_eq(engine._tracker.changedIDs['scotsman'], SCOTSMAN_CHANGED);
  1401     do_check_eq(engine._tracker.changedIDs['peppercorn'], PEPPERCORN_CHANGED);
  1403     engine.enabled = true;
  1404     engine.sync();
  1406     // Local timestamp has been set.
  1407     do_check_true(engine.lastSyncLocal > 0);
  1409     // Ensure the 'flying' record has been uploaded and is no longer marked.
  1410     do_check_true(!!collection.payload("flying"));
  1411     do_check_eq(engine._tracker.changedIDs['flying'], undefined);
  1413     // The 'scotsman' and 'peppercorn' records couldn't be uploaded so
  1414     // they weren't cleared from the tracker.
  1415     do_check_eq(engine._tracker.changedIDs['scotsman'], SCOTSMAN_CHANGED);
  1416     do_check_eq(engine._tracker.changedIDs['peppercorn'], PEPPERCORN_CHANGED);
  1418   } finally {
  1419     cleanAndGo(server);
  1421 });
  1424 add_test(function test_uploadOutgoing_MAX_UPLOAD_RECORDS() {
  1425   _("SyncEngine._uploadOutgoing uploads in batches of MAX_UPLOAD_RECORDS");
  1427   Service.identity.username = "foo";
  1428   let collection = new ServerCollection();
  1430   // Let's count how many times the client posts to the server
  1431   var noOfUploads = 0;
  1432   collection.post = (function(orig) {
  1433     return function() {
  1434       noOfUploads++;
  1435       return orig.apply(this, arguments);
  1436     };
  1437   }(collection.post));
  1439   // Create a bunch of records (and server side handlers)
  1440   let engine = makeRotaryEngine();
  1441   for (var i = 0; i < 234; i++) {
  1442     let id = 'record-no-' + i;
  1443     engine._store.items[id] = "Record No. " + i;
  1444     engine._tracker.addChangedID(id, 0);
  1445     collection.insert(id);
  1448   let meta_global = Service.recordManager.set(engine.metaURL,
  1449                                               new WBORecord(engine.metaURL));
  1450   meta_global.payload.engines = {rotary: {version: engine.version,
  1451                                          syncID: engine.syncID}};
  1453   let server = sync_httpd_setup({
  1454       "/1.1/foo/storage/rotary": collection.handler()
  1455   });
  1457   let syncTesting = new SyncTestingInfrastructure(server);
  1459   try {
  1461     // Confirm initial environment.
  1462     do_check_eq(noOfUploads, 0);
  1464     engine._syncStartup();
  1465     engine._uploadOutgoing();
  1467     // Ensure all records have been uploaded.
  1468     for (i = 0; i < 234; i++) {
  1469       do_check_true(!!collection.payload('record-no-' + i));
  1472     // Ensure that the uploads were performed in batches of MAX_UPLOAD_RECORDS.
  1473     do_check_eq(noOfUploads, Math.ceil(234/MAX_UPLOAD_RECORDS));
  1475   } finally {
  1476     cleanAndGo(server);
  1478 });
  1481 add_test(function test_syncFinish_noDelete() {
  1482   _("SyncEngine._syncFinish resets tracker's score");
  1484   let server = httpd_setup({});
  1486   let syncTesting = new SyncTestingInfrastructure(server);
  1487   let engine = makeRotaryEngine();
  1488   engine._delete = {}; // Nothing to delete
  1489   engine._tracker.score = 100;
  1491   // _syncFinish() will reset the engine's score.
  1492   engine._syncFinish();
  1493   do_check_eq(engine.score, 0);
  1494   server.stop(run_next_test);
  1495 });
  1498 add_test(function test_syncFinish_deleteByIds() {
  1499   _("SyncEngine._syncFinish deletes server records slated for deletion (list of record IDs).");
  1501   Service.identity.username = "foo";
  1502   let collection = new ServerCollection();
  1503   collection._wbos.flying = new ServerWBO(
  1504       'flying', encryptPayload({id: 'flying',
  1505                                 denomination: "LNER Class A3 4472"}));
  1506   collection._wbos.scotsman = new ServerWBO(
  1507       'scotsman', encryptPayload({id: 'scotsman',
  1508                                   denomination: "Flying Scotsman"}));
  1509   collection._wbos.rekolok = new ServerWBO(
  1510       'rekolok', encryptPayload({id: 'rekolok',
  1511                                 denomination: "Rekonstruktionslokomotive"}));
  1513   let server = httpd_setup({
  1514       "/1.1/foo/storage/rotary": collection.handler()
  1515   });
  1516   let syncTesting = new SyncTestingInfrastructure(server);
  1518   let engine = makeRotaryEngine();
  1519   try {
  1520     engine._delete = {ids: ['flying', 'rekolok']};
  1521     engine._syncFinish();
  1523     // The 'flying' and 'rekolok' records were deleted while the
  1524     // 'scotsman' one wasn't.
  1525     do_check_eq(collection.payload("flying"), undefined);
  1526     do_check_true(!!collection.payload("scotsman"));
  1527     do_check_eq(collection.payload("rekolok"), undefined);
  1529     // The deletion todo list has been reset.
  1530     do_check_eq(engine._delete.ids, undefined);
  1532   } finally {
  1533     cleanAndGo(server);
  1535 });
  1538 add_test(function test_syncFinish_deleteLotsInBatches() {
  1539   _("SyncEngine._syncFinish deletes server records in batches of 100 (list of record IDs).");
  1541   Service.identity.username = "foo";
  1542   let collection = new ServerCollection();
  1544   // Let's count how many times the client does a DELETE request to the server
  1545   var noOfUploads = 0;
  1546   collection.delete = (function(orig) {
  1547     return function() {
  1548       noOfUploads++;
  1549       return orig.apply(this, arguments);
  1550     };
  1551   }(collection.delete));
  1553   // Create a bunch of records on the server
  1554   let now = Date.now();
  1555   for (var i = 0; i < 234; i++) {
  1556     let id = 'record-no-' + i;
  1557     let payload = encryptPayload({id: id, denomination: "Record No. " + i});
  1558     let wbo = new ServerWBO(id, payload);
  1559     wbo.modified = now / 1000 - 60 * (i + 110);
  1560     collection.insertWBO(wbo);
  1563   let server = httpd_setup({
  1564       "/1.1/foo/storage/rotary": collection.handler()
  1565   });
  1567   let syncTesting = new SyncTestingInfrastructure(server);
  1569   let engine = makeRotaryEngine();
  1570   try {
  1572     // Confirm initial environment
  1573     do_check_eq(noOfUploads, 0);
  1575     // Declare what we want to have deleted: all records no. 100 and
  1576     // up and all records that are less than 200 mins old (which are
  1577     // records 0 thru 90).
  1578     engine._delete = {ids: [],
  1579                       newer: now / 1000 - 60 * 200.5};
  1580     for (i = 100; i < 234; i++) {
  1581       engine._delete.ids.push('record-no-' + i);
  1584     engine._syncFinish();
  1586     // Ensure that the appropriate server data has been wiped while
  1587     // preserving records 90 thru 200.
  1588     for (i = 0; i < 234; i++) {
  1589       let id = 'record-no-' + i;
  1590       if (i <= 90 || i >= 100) {
  1591         do_check_eq(collection.payload(id), undefined);
  1592       } else {
  1593         do_check_true(!!collection.payload(id));
  1597     // The deletion was done in batches
  1598     do_check_eq(noOfUploads, 2 + 1);
  1600     // The deletion todo list has been reset.
  1601     do_check_eq(engine._delete.ids, undefined);
  1603   } finally {
  1604     cleanAndGo(server);
  1606 });
  1609 add_test(function test_sync_partialUpload() {
  1610   _("SyncEngine.sync() keeps changedIDs that couldn't be uploaded.");
  1612   Service.identity.username = "foo";
  1614   let collection = new ServerCollection();
  1615   let server = sync_httpd_setup({
  1616       "/1.1/foo/storage/rotary": collection.handler()
  1617   });
  1618   let syncTesting = new SyncTestingInfrastructure(server);
  1619   generateNewKeys(Service.collectionKeys);
  1621   let engine = makeRotaryEngine();
  1622   engine.lastSync = 123; // needs to be non-zero so that tracker is queried
  1623   engine.lastSyncLocal = 456;
  1625   // Let the third upload fail completely
  1626   var noOfUploads = 0;
  1627   collection.post = (function(orig) {
  1628     return function() {
  1629       if (noOfUploads == 2)
  1630         throw "FAIL!";
  1631       noOfUploads++;
  1632       return orig.apply(this, arguments);
  1633     };
  1634   }(collection.post));
  1636   // Create a bunch of records (and server side handlers)
  1637   for (let i = 0; i < 234; i++) {
  1638     let id = 'record-no-' + i;
  1639     engine._store.items[id] = "Record No. " + i;
  1640     engine._tracker.addChangedID(id, i);
  1641     // Let two items in the first upload batch fail.
  1642     if ((i != 23) && (i != 42)) {
  1643       collection.insert(id);
  1647   let meta_global = Service.recordManager.set(engine.metaURL,
  1648                                               new WBORecord(engine.metaURL));
  1649   meta_global.payload.engines = {rotary: {version: engine.version,
  1650                                          syncID: engine.syncID}};
  1652   try {
  1654     engine.enabled = true;
  1655     let error;
  1656     try {
  1657       engine.sync();
  1658     } catch (ex) {
  1659       error = ex;
  1661     do_check_true(!!error);
  1663     // The timestamp has been updated.
  1664     do_check_true(engine.lastSyncLocal > 456);
  1666     for (let i = 0; i < 234; i++) {
  1667       let id = 'record-no-' + i;
  1668       // Ensure failed records are back in the tracker:
  1669       // * records no. 23 and 42 were rejected by the server,
  1670       // * records no. 200 and higher couldn't be uploaded because we failed
  1671       //   hard on the 3rd upload.
  1672       if ((i == 23) || (i == 42) || (i >= 200))
  1673         do_check_eq(engine._tracker.changedIDs[id], i);
  1674       else
  1675         do_check_false(id in engine._tracker.changedIDs);
  1678   } finally {
  1679     cleanAndGo(server);
  1681 });
  1683 add_test(function test_canDecrypt_noCryptoKeys() {
  1684   _("SyncEngine.canDecrypt returns false if the engine fails to decrypt items on the server, e.g. due to a missing crypto key collection.");
  1685   Service.identity.username = "foo";
  1687   // Wipe collection keys so we can test the desired scenario.
  1688   Service.collectionKeys.clear();
  1690   let collection = new ServerCollection();
  1691   collection._wbos.flying = new ServerWBO(
  1692       'flying', encryptPayload({id: 'flying',
  1693                                 denomination: "LNER Class A3 4472"}));
  1695   let server = sync_httpd_setup({
  1696       "/1.1/foo/storage/rotary": collection.handler()
  1697   });
  1699   let syncTesting = new SyncTestingInfrastructure(server);
  1700   let engine = makeRotaryEngine();
  1701   try {
  1703     do_check_false(engine.canDecrypt());
  1705   } finally {
  1706     cleanAndGo(server);
  1708 });
  1710 add_test(function test_canDecrypt_true() {
  1711   _("SyncEngine.canDecrypt returns true if the engine can decrypt the items on the server.");
  1712   Service.identity.username = "foo";
  1714   generateNewKeys(Service.collectionKeys);
  1716   let collection = new ServerCollection();
  1717   collection._wbos.flying = new ServerWBO(
  1718       'flying', encryptPayload({id: 'flying',
  1719                                 denomination: "LNER Class A3 4472"}));
  1721   let server = sync_httpd_setup({
  1722       "/1.1/foo/storage/rotary": collection.handler()
  1723   });
  1725   let syncTesting = new SyncTestingInfrastructure(server);
  1726   let engine = makeRotaryEngine();
  1727   try {
  1729     do_check_true(engine.canDecrypt());
  1731   } finally {
  1732     cleanAndGo(server);
  1735 });
  1737 add_test(function test_syncapplied_observer() {
  1738   Service.identity.username = "foo";
  1740   const NUMBER_OF_RECORDS = 10;
  1742   let engine = makeRotaryEngine();
  1744   // Create a batch of server side records.
  1745   let collection = new ServerCollection();
  1746   for (var i = 0; i < NUMBER_OF_RECORDS; i++) {
  1747     let id = 'record-no-' + i;
  1748     let payload = encryptPayload({id: id, denomination: "Record No. " + id});
  1749     collection.insert(id, payload);
  1752   let server = httpd_setup({
  1753     "/1.1/foo/storage/rotary": collection.handler()
  1754   });
  1756   let syncTesting = new SyncTestingInfrastructure(server);
  1758   let meta_global = Service.recordManager.set(engine.metaURL,
  1759                                               new WBORecord(engine.metaURL));
  1760   meta_global.payload.engines = {rotary: {version: engine.version,
  1761                                          syncID: engine.syncID}};
  1763   let numApplyCalls = 0;
  1764   let engine_name;
  1765   let count;
  1766   function onApplied(subject, data) {
  1767     numApplyCalls++;
  1768     engine_name = data;
  1769     count = subject;
  1772   Svc.Obs.add("weave:engine:sync:applied", onApplied);
  1774   try {
  1775     Service.scheduler.hasIncomingItems = false;
  1777     // Do sync.
  1778     engine._syncStartup();
  1779     engine._processIncoming();
  1781     do_check_attribute_count(engine._store.items, 10);
  1783     do_check_eq(numApplyCalls, 1);
  1784     do_check_eq(engine_name, "rotary");
  1785     do_check_eq(count.applied, 10);
  1787     do_check_true(Service.scheduler.hasIncomingItems);
  1788   } finally {
  1789     cleanAndGo(server);
  1790     Service.scheduler.hasIncomingItems = false;
  1791     Svc.Obs.remove("weave:engine:sync:applied", onApplied);
  1793 });

mercurial