1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/services/sync/tests/unit/test_syncengine_sync.js Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,1793 @@ 1.4 +/* Any copyright is dedicated to the Public Domain. 1.5 + * http://creativecommons.org/publicdomain/zero/1.0/ */ 1.6 + 1.7 +Cu.import("resource://services-sync/constants.js"); 1.8 +Cu.import("resource://services-sync/engines.js"); 1.9 +Cu.import("resource://services-sync/policies.js"); 1.10 +Cu.import("resource://services-sync/record.js"); 1.11 +Cu.import("resource://services-sync/resource.js"); 1.12 +Cu.import("resource://services-sync/service.js"); 1.13 +Cu.import("resource://services-sync/util.js"); 1.14 +Cu.import("resource://testing-common/services/sync/rotaryengine.js"); 1.15 +Cu.import("resource://testing-common/services/sync/utils.js"); 1.16 + 1.17 +function makeRotaryEngine() { 1.18 + return new RotaryEngine(Service); 1.19 +} 1.20 + 1.21 +function cleanAndGo(server) { 1.22 + Svc.Prefs.resetBranch(""); 1.23 + Svc.Prefs.set("log.logger.engine.rotary", "Trace"); 1.24 + Service.recordManager.clearCache(); 1.25 + server.stop(run_next_test); 1.26 +} 1.27 + 1.28 +function configureService(server, username, password) { 1.29 + Service.clusterURL = server.baseURI; 1.30 + 1.31 + Service.identity.account = username || "foo"; 1.32 + Service.identity.basicPassword = password || "password"; 1.33 +} 1.34 + 1.35 +function createServerAndConfigureClient() { 1.36 + let engine = new RotaryEngine(Service); 1.37 + 1.38 + let contents = { 1.39 + meta: {global: {engines: {rotary: {version: engine.version, 1.40 + syncID: engine.syncID}}}}, 1.41 + crypto: {}, 1.42 + rotary: {} 1.43 + }; 1.44 + 1.45 + const USER = "foo"; 1.46 + let server = new SyncServer(); 1.47 + server.registerUser(USER, "password"); 1.48 + server.createContents(USER, contents); 1.49 + server.start(); 1.50 + 1.51 + Service.serverURL = server.baseURI; 1.52 + Service.clusterURL = server.baseURI; 1.53 + Service.identity.username = USER; 1.54 + Service._updateCachedURLs(); 1.55 + 1.56 + return [engine, server, USER]; 1.57 +} 1.58 + 1.59 +function run_test() { 1.60 + generateNewKeys(Service.collectionKeys); 1.61 + Svc.Prefs.set("log.logger.engine.rotary", "Trace"); 1.62 + run_next_test(); 1.63 +} 1.64 + 1.65 +/* 1.66 + * Tests 1.67 + * 1.68 + * SyncEngine._sync() is divided into four rather independent steps: 1.69 + * 1.70 + * - _syncStartup() 1.71 + * - _processIncoming() 1.72 + * - _uploadOutgoing() 1.73 + * - _syncFinish() 1.74 + * 1.75 + * In the spirit of unit testing, these are tested individually for 1.76 + * different scenarios below. 1.77 + */ 1.78 + 1.79 +add_test(function test_syncStartup_emptyOrOutdatedGlobalsResetsSync() { 1.80 + _("SyncEngine._syncStartup resets sync and wipes server data if there's no or an outdated global record"); 1.81 + 1.82 + // Some server side data that's going to be wiped 1.83 + let collection = new ServerCollection(); 1.84 + collection.insert('flying', 1.85 + encryptPayload({id: 'flying', 1.86 + denomination: "LNER Class A3 4472"})); 1.87 + collection.insert('scotsman', 1.88 + encryptPayload({id: 'scotsman', 1.89 + denomination: "Flying Scotsman"})); 1.90 + 1.91 + let server = sync_httpd_setup({ 1.92 + "/1.1/foo/storage/rotary": collection.handler() 1.93 + }); 1.94 + 1.95 + let syncTesting = new SyncTestingInfrastructure(server); 1.96 + Service.identity.username = "foo"; 1.97 + 1.98 + let engine = makeRotaryEngine(); 1.99 + engine._store.items = {rekolok: "Rekonstruktionslokomotive"}; 1.100 + try { 1.101 + 1.102 + // Confirm initial environment 1.103 + do_check_eq(engine._tracker.changedIDs["rekolok"], undefined); 1.104 + let metaGlobal = Service.recordManager.get(engine.metaURL); 1.105 + do_check_eq(metaGlobal.payload.engines, undefined); 1.106 + do_check_true(!!collection.payload("flying")); 1.107 + do_check_true(!!collection.payload("scotsman")); 1.108 + 1.109 + engine.lastSync = Date.now() / 1000; 1.110 + engine.lastSyncLocal = Date.now(); 1.111 + 1.112 + // Trying to prompt a wipe -- we no longer track CryptoMeta per engine, 1.113 + // so it has nothing to check. 1.114 + engine._syncStartup(); 1.115 + 1.116 + // The meta/global WBO has been filled with data about the engine 1.117 + let engineData = metaGlobal.payload.engines["rotary"]; 1.118 + do_check_eq(engineData.version, engine.version); 1.119 + do_check_eq(engineData.syncID, engine.syncID); 1.120 + 1.121 + // Sync was reset and server data was wiped 1.122 + do_check_eq(engine.lastSync, 0); 1.123 + do_check_eq(collection.payload("flying"), undefined); 1.124 + do_check_eq(collection.payload("scotsman"), undefined); 1.125 + 1.126 + } finally { 1.127 + cleanAndGo(server); 1.128 + } 1.129 +}); 1.130 + 1.131 +add_test(function test_syncStartup_serverHasNewerVersion() { 1.132 + _("SyncEngine._syncStartup "); 1.133 + 1.134 + let global = new ServerWBO('global', {engines: {rotary: {version: 23456}}}); 1.135 + let server = httpd_setup({ 1.136 + "/1.1/foo/storage/meta/global": global.handler() 1.137 + }); 1.138 + 1.139 + let syncTesting = new SyncTestingInfrastructure(server); 1.140 + Service.identity.username = "foo"; 1.141 + 1.142 + let engine = makeRotaryEngine(); 1.143 + try { 1.144 + 1.145 + // The server has a newer version of the data and our engine can 1.146 + // handle. That should give us an exception. 1.147 + let error; 1.148 + try { 1.149 + engine._syncStartup(); 1.150 + } catch (ex) { 1.151 + error = ex; 1.152 + } 1.153 + do_check_eq(error.failureCode, VERSION_OUT_OF_DATE); 1.154 + 1.155 + } finally { 1.156 + cleanAndGo(server); 1.157 + } 1.158 +}); 1.159 + 1.160 + 1.161 +add_test(function test_syncStartup_syncIDMismatchResetsClient() { 1.162 + _("SyncEngine._syncStartup resets sync if syncIDs don't match"); 1.163 + 1.164 + let server = sync_httpd_setup({}); 1.165 + let syncTesting = new SyncTestingInfrastructure(server); 1.166 + Service.identity.username = "foo"; 1.167 + 1.168 + // global record with a different syncID than our engine has 1.169 + let engine = makeRotaryEngine(); 1.170 + let global = new ServerWBO('global', 1.171 + {engines: {rotary: {version: engine.version, 1.172 + syncID: 'foobar'}}}); 1.173 + server.registerPathHandler("/1.1/foo/storage/meta/global", global.handler()); 1.174 + 1.175 + try { 1.176 + 1.177 + // Confirm initial environment 1.178 + do_check_eq(engine.syncID, 'fake-guid-0'); 1.179 + do_check_eq(engine._tracker.changedIDs["rekolok"], undefined); 1.180 + 1.181 + engine.lastSync = Date.now() / 1000; 1.182 + engine.lastSyncLocal = Date.now(); 1.183 + engine._syncStartup(); 1.184 + 1.185 + // The engine has assumed the server's syncID 1.186 + do_check_eq(engine.syncID, 'foobar'); 1.187 + 1.188 + // Sync was reset 1.189 + do_check_eq(engine.lastSync, 0); 1.190 + 1.191 + } finally { 1.192 + cleanAndGo(server); 1.193 + } 1.194 +}); 1.195 + 1.196 + 1.197 +add_test(function test_processIncoming_emptyServer() { 1.198 + _("SyncEngine._processIncoming working with an empty server backend"); 1.199 + 1.200 + let collection = new ServerCollection(); 1.201 + let server = sync_httpd_setup({ 1.202 + "/1.1/foo/storage/rotary": collection.handler() 1.203 + }); 1.204 + 1.205 + let syncTesting = new SyncTestingInfrastructure(server); 1.206 + Service.identity.username = "foo"; 1.207 + 1.208 + let engine = makeRotaryEngine(); 1.209 + try { 1.210 + 1.211 + // Merely ensure that this code path is run without any errors 1.212 + engine._processIncoming(); 1.213 + do_check_eq(engine.lastSync, 0); 1.214 + 1.215 + } finally { 1.216 + cleanAndGo(server); 1.217 + } 1.218 +}); 1.219 + 1.220 + 1.221 +add_test(function test_processIncoming_createFromServer() { 1.222 + _("SyncEngine._processIncoming creates new records from server data"); 1.223 + 1.224 + // Some server records that will be downloaded 1.225 + let collection = new ServerCollection(); 1.226 + collection.insert('flying', 1.227 + encryptPayload({id: 'flying', 1.228 + denomination: "LNER Class A3 4472"})); 1.229 + collection.insert('scotsman', 1.230 + encryptPayload({id: 'scotsman', 1.231 + denomination: "Flying Scotsman"})); 1.232 + 1.233 + // Two pathological cases involving relative URIs gone wrong. 1.234 + let pathologicalPayload = encryptPayload({id: '../pathological', 1.235 + denomination: "Pathological Case"}); 1.236 + collection.insert('../pathological', pathologicalPayload); 1.237 + 1.238 + let server = sync_httpd_setup({ 1.239 + "/1.1/foo/storage/rotary": collection.handler(), 1.240 + "/1.1/foo/storage/rotary/flying": collection.wbo("flying").handler(), 1.241 + "/1.1/foo/storage/rotary/scotsman": collection.wbo("scotsman").handler() 1.242 + }); 1.243 + 1.244 + let syncTesting = new SyncTestingInfrastructure(server); 1.245 + Service.identity.username = "foo"; 1.246 + 1.247 + generateNewKeys(Service.collectionKeys); 1.248 + 1.249 + let engine = makeRotaryEngine(); 1.250 + let meta_global = Service.recordManager.set(engine.metaURL, 1.251 + new WBORecord(engine.metaURL)); 1.252 + meta_global.payload.engines = {rotary: {version: engine.version, 1.253 + syncID: engine.syncID}}; 1.254 + 1.255 + try { 1.256 + 1.257 + // Confirm initial environment 1.258 + do_check_eq(engine.lastSync, 0); 1.259 + do_check_eq(engine.lastModified, null); 1.260 + do_check_eq(engine._store.items.flying, undefined); 1.261 + do_check_eq(engine._store.items.scotsman, undefined); 1.262 + do_check_eq(engine._store.items['../pathological'], undefined); 1.263 + 1.264 + engine._syncStartup(); 1.265 + engine._processIncoming(); 1.266 + 1.267 + // Timestamps of last sync and last server modification are set. 1.268 + do_check_true(engine.lastSync > 0); 1.269 + do_check_true(engine.lastModified > 0); 1.270 + 1.271 + // Local records have been created from the server data. 1.272 + do_check_eq(engine._store.items.flying, "LNER Class A3 4472"); 1.273 + do_check_eq(engine._store.items.scotsman, "Flying Scotsman"); 1.274 + do_check_eq(engine._store.items['../pathological'], "Pathological Case"); 1.275 + 1.276 + } finally { 1.277 + cleanAndGo(server); 1.278 + } 1.279 +}); 1.280 + 1.281 + 1.282 +add_test(function test_processIncoming_reconcile() { 1.283 + _("SyncEngine._processIncoming updates local records"); 1.284 + 1.285 + let collection = new ServerCollection(); 1.286 + 1.287 + // This server record is newer than the corresponding client one, 1.288 + // so it'll update its data. 1.289 + collection.insert('newrecord', 1.290 + encryptPayload({id: 'newrecord', 1.291 + denomination: "New stuff..."})); 1.292 + 1.293 + // This server record is newer than the corresponding client one, 1.294 + // so it'll update its data. 1.295 + collection.insert('newerserver', 1.296 + encryptPayload({id: 'newerserver', 1.297 + denomination: "New data!"})); 1.298 + 1.299 + // This server record is 2 mins older than the client counterpart 1.300 + // but identical to it, so we're expecting the client record's 1.301 + // changedID to be reset. 1.302 + collection.insert('olderidentical', 1.303 + encryptPayload({id: 'olderidentical', 1.304 + denomination: "Older but identical"})); 1.305 + collection._wbos.olderidentical.modified -= 120; 1.306 + 1.307 + // This item simply has different data than the corresponding client 1.308 + // record (which is unmodified), so it will update the client as well 1.309 + collection.insert('updateclient', 1.310 + encryptPayload({id: 'updateclient', 1.311 + denomination: "Get this!"})); 1.312 + 1.313 + // This is a dupe of 'original'. 1.314 + collection.insert('duplication', 1.315 + encryptPayload({id: 'duplication', 1.316 + denomination: "Original Entry"})); 1.317 + 1.318 + // This record is marked as deleted, so we're expecting the client 1.319 + // record to be removed. 1.320 + collection.insert('nukeme', 1.321 + encryptPayload({id: 'nukeme', 1.322 + denomination: "Nuke me!", 1.323 + deleted: true})); 1.324 + 1.325 + let server = sync_httpd_setup({ 1.326 + "/1.1/foo/storage/rotary": collection.handler() 1.327 + }); 1.328 + 1.329 + let syncTesting = new SyncTestingInfrastructure(server); 1.330 + Service.identity.username = "foo"; 1.331 + 1.332 + let engine = makeRotaryEngine(); 1.333 + engine._store.items = {newerserver: "New data, but not as new as server!", 1.334 + olderidentical: "Older but identical", 1.335 + updateclient: "Got data?", 1.336 + original: "Original Entry", 1.337 + long_original: "Long Original Entry", 1.338 + nukeme: "Nuke me!"}; 1.339 + // Make this record 1 min old, thus older than the one on the server 1.340 + engine._tracker.addChangedID('newerserver', Date.now()/1000 - 60); 1.341 + // This record has been changed 2 mins later than the one on the server 1.342 + engine._tracker.addChangedID('olderidentical', Date.now()/1000); 1.343 + 1.344 + let meta_global = Service.recordManager.set(engine.metaURL, 1.345 + new WBORecord(engine.metaURL)); 1.346 + meta_global.payload.engines = {rotary: {version: engine.version, 1.347 + syncID: engine.syncID}}; 1.348 + 1.349 + try { 1.350 + 1.351 + // Confirm initial environment 1.352 + do_check_eq(engine._store.items.newrecord, undefined); 1.353 + do_check_eq(engine._store.items.newerserver, "New data, but not as new as server!"); 1.354 + do_check_eq(engine._store.items.olderidentical, "Older but identical"); 1.355 + do_check_eq(engine._store.items.updateclient, "Got data?"); 1.356 + do_check_eq(engine._store.items.nukeme, "Nuke me!"); 1.357 + do_check_true(engine._tracker.changedIDs['olderidentical'] > 0); 1.358 + 1.359 + engine._syncStartup(); 1.360 + engine._processIncoming(); 1.361 + 1.362 + // Timestamps of last sync and last server modification are set. 1.363 + do_check_true(engine.lastSync > 0); 1.364 + do_check_true(engine.lastModified > 0); 1.365 + 1.366 + // The new record is created. 1.367 + do_check_eq(engine._store.items.newrecord, "New stuff..."); 1.368 + 1.369 + // The 'newerserver' record is updated since the server data is newer. 1.370 + do_check_eq(engine._store.items.newerserver, "New data!"); 1.371 + 1.372 + // The data for 'olderidentical' is identical on the server, so 1.373 + // it's no longer marked as changed anymore. 1.374 + do_check_eq(engine._store.items.olderidentical, "Older but identical"); 1.375 + do_check_eq(engine._tracker.changedIDs['olderidentical'], undefined); 1.376 + 1.377 + // Updated with server data. 1.378 + do_check_eq(engine._store.items.updateclient, "Get this!"); 1.379 + 1.380 + // The incoming ID is preferred. 1.381 + do_check_eq(engine._store.items.original, undefined); 1.382 + do_check_eq(engine._store.items.duplication, "Original Entry"); 1.383 + do_check_neq(engine._delete.ids.indexOf("original"), -1); 1.384 + 1.385 + // The 'nukeme' record marked as deleted is removed. 1.386 + do_check_eq(engine._store.items.nukeme, undefined); 1.387 + } finally { 1.388 + cleanAndGo(server); 1.389 + } 1.390 +}); 1.391 + 1.392 +add_test(function test_processIncoming_reconcile_local_deleted() { 1.393 + _("Ensure local, duplicate ID is deleted on server."); 1.394 + 1.395 + // When a duplicate is resolved, the local ID (which is never taken) should 1.396 + // be deleted on the server. 1.397 + let [engine, server, user] = createServerAndConfigureClient(); 1.398 + 1.399 + let now = Date.now() / 1000 - 10; 1.400 + engine.lastSync = now; 1.401 + engine.lastModified = now + 1; 1.402 + 1.403 + let record = encryptPayload({id: "DUPE_INCOMING", denomination: "incoming"}); 1.404 + let wbo = new ServerWBO("DUPE_INCOMING", record, now + 2); 1.405 + server.insertWBO(user, "rotary", wbo); 1.406 + 1.407 + let record = encryptPayload({id: "DUPE_LOCAL", denomination: "local"}); 1.408 + let wbo = new ServerWBO("DUPE_LOCAL", record, now - 1); 1.409 + server.insertWBO(user, "rotary", wbo); 1.410 + 1.411 + engine._store.create({id: "DUPE_LOCAL", denomination: "local"}); 1.412 + do_check_true(engine._store.itemExists("DUPE_LOCAL")); 1.413 + do_check_eq("DUPE_LOCAL", engine._findDupe({id: "DUPE_INCOMING"})); 1.414 + 1.415 + engine._sync(); 1.416 + 1.417 + do_check_attribute_count(engine._store.items, 1); 1.418 + do_check_true("DUPE_INCOMING" in engine._store.items); 1.419 + 1.420 + let collection = server.getCollection(user, "rotary"); 1.421 + do_check_eq(1, collection.count()); 1.422 + do_check_neq(undefined, collection.wbo("DUPE_INCOMING")); 1.423 + 1.424 + cleanAndGo(server); 1.425 +}); 1.426 + 1.427 +add_test(function test_processIncoming_reconcile_equivalent() { 1.428 + _("Ensure proper handling of incoming records that match local."); 1.429 + 1.430 + let [engine, server, user] = createServerAndConfigureClient(); 1.431 + 1.432 + let now = Date.now() / 1000 - 10; 1.433 + engine.lastSync = now; 1.434 + engine.lastModified = now + 1; 1.435 + 1.436 + let record = encryptPayload({id: "entry", denomination: "denomination"}); 1.437 + let wbo = new ServerWBO("entry", record, now + 2); 1.438 + server.insertWBO(user, "rotary", wbo); 1.439 + 1.440 + engine._store.items = {entry: "denomination"}; 1.441 + do_check_true(engine._store.itemExists("entry")); 1.442 + 1.443 + engine._sync(); 1.444 + 1.445 + do_check_attribute_count(engine._store.items, 1); 1.446 + 1.447 + cleanAndGo(server); 1.448 +}); 1.449 + 1.450 +add_test(function test_processIncoming_reconcile_locally_deleted_dupe_new() { 1.451 + _("Ensure locally deleted duplicate record newer than incoming is handled."); 1.452 + 1.453 + // This is a somewhat complicated test. It ensures that if a client receives 1.454 + // a modified record for an item that is deleted locally but with a different 1.455 + // ID that the incoming record is ignored. This is a corner case for record 1.456 + // handling, but it needs to be supported. 1.457 + let [engine, server, user] = createServerAndConfigureClient(); 1.458 + 1.459 + let now = Date.now() / 1000 - 10; 1.460 + engine.lastSync = now; 1.461 + engine.lastModified = now + 1; 1.462 + 1.463 + let record = encryptPayload({id: "DUPE_INCOMING", denomination: "incoming"}); 1.464 + let wbo = new ServerWBO("DUPE_INCOMING", record, now + 2); 1.465 + server.insertWBO(user, "rotary", wbo); 1.466 + 1.467 + // Simulate a locally-deleted item. 1.468 + engine._store.items = {}; 1.469 + engine._tracker.addChangedID("DUPE_LOCAL", now + 3); 1.470 + do_check_false(engine._store.itemExists("DUPE_LOCAL")); 1.471 + do_check_false(engine._store.itemExists("DUPE_INCOMING")); 1.472 + do_check_eq("DUPE_LOCAL", engine._findDupe({id: "DUPE_INCOMING"})); 1.473 + 1.474 + engine._sync(); 1.475 + 1.476 + // After the sync, the server's payload for the original ID should be marked 1.477 + // as deleted. 1.478 + do_check_empty(engine._store.items); 1.479 + let collection = server.getCollection(user, "rotary"); 1.480 + do_check_eq(1, collection.count()); 1.481 + let wbo = collection.wbo("DUPE_INCOMING"); 1.482 + do_check_neq(null, wbo); 1.483 + let payload = JSON.parse(JSON.parse(wbo.payload).ciphertext); 1.484 + do_check_true(payload.deleted); 1.485 + 1.486 + cleanAndGo(server); 1.487 +}); 1.488 + 1.489 +add_test(function test_processIncoming_reconcile_locally_deleted_dupe_old() { 1.490 + _("Ensure locally deleted duplicate record older than incoming is restored."); 1.491 + 1.492 + // This is similar to the above test except it tests the condition where the 1.493 + // incoming record is newer than the local deletion, therefore overriding it. 1.494 + 1.495 + let [engine, server, user] = createServerAndConfigureClient(); 1.496 + 1.497 + let now = Date.now() / 1000 - 10; 1.498 + engine.lastSync = now; 1.499 + engine.lastModified = now + 1; 1.500 + 1.501 + let record = encryptPayload({id: "DUPE_INCOMING", denomination: "incoming"}); 1.502 + let wbo = new ServerWBO("DUPE_INCOMING", record, now + 2); 1.503 + server.insertWBO(user, "rotary", wbo); 1.504 + 1.505 + // Simulate a locally-deleted item. 1.506 + engine._store.items = {}; 1.507 + engine._tracker.addChangedID("DUPE_LOCAL", now + 1); 1.508 + do_check_false(engine._store.itemExists("DUPE_LOCAL")); 1.509 + do_check_false(engine._store.itemExists("DUPE_INCOMING")); 1.510 + do_check_eq("DUPE_LOCAL", engine._findDupe({id: "DUPE_INCOMING"})); 1.511 + 1.512 + engine._sync(); 1.513 + 1.514 + // Since the remote change is newer, the incoming item should exist locally. 1.515 + do_check_attribute_count(engine._store.items, 1); 1.516 + do_check_true("DUPE_INCOMING" in engine._store.items); 1.517 + do_check_eq("incoming", engine._store.items.DUPE_INCOMING); 1.518 + 1.519 + let collection = server.getCollection(user, "rotary"); 1.520 + do_check_eq(1, collection.count()); 1.521 + let wbo = collection.wbo("DUPE_INCOMING"); 1.522 + let payload = JSON.parse(JSON.parse(wbo.payload).ciphertext); 1.523 + do_check_eq("incoming", payload.denomination); 1.524 + 1.525 + cleanAndGo(server); 1.526 +}); 1.527 + 1.528 +add_test(function test_processIncoming_reconcile_changed_dupe() { 1.529 + _("Ensure that locally changed duplicate record is handled properly."); 1.530 + 1.531 + let [engine, server, user] = createServerAndConfigureClient(); 1.532 + 1.533 + let now = Date.now() / 1000 - 10; 1.534 + engine.lastSync = now; 1.535 + engine.lastModified = now + 1; 1.536 + 1.537 + // The local record is newer than the incoming one, so it should be retained. 1.538 + let record = encryptPayload({id: "DUPE_INCOMING", denomination: "incoming"}); 1.539 + let wbo = new ServerWBO("DUPE_INCOMING", record, now + 2); 1.540 + server.insertWBO(user, "rotary", wbo); 1.541 + 1.542 + engine._store.create({id: "DUPE_LOCAL", denomination: "local"}); 1.543 + engine._tracker.addChangedID("DUPE_LOCAL", now + 3); 1.544 + do_check_true(engine._store.itemExists("DUPE_LOCAL")); 1.545 + do_check_eq("DUPE_LOCAL", engine._findDupe({id: "DUPE_INCOMING"})); 1.546 + 1.547 + engine._sync(); 1.548 + 1.549 + // The ID should have been changed to incoming. 1.550 + do_check_attribute_count(engine._store.items, 1); 1.551 + do_check_true("DUPE_INCOMING" in engine._store.items); 1.552 + 1.553 + // On the server, the local ID should be deleted and the incoming ID should 1.554 + // have its payload set to what was in the local record. 1.555 + let collection = server.getCollection(user, "rotary"); 1.556 + do_check_eq(1, collection.count()); 1.557 + let wbo = collection.wbo("DUPE_INCOMING"); 1.558 + do_check_neq(undefined, wbo); 1.559 + let payload = JSON.parse(JSON.parse(wbo.payload).ciphertext); 1.560 + do_check_eq("local", payload.denomination); 1.561 + 1.562 + cleanAndGo(server); 1.563 +}); 1.564 + 1.565 +add_test(function test_processIncoming_reconcile_changed_dupe_new() { 1.566 + _("Ensure locally changed duplicate record older than incoming is ignored."); 1.567 + 1.568 + // This test is similar to the above except the incoming record is younger 1.569 + // than the local record. The incoming record should be authoritative. 1.570 + let [engine, server, user] = createServerAndConfigureClient(); 1.571 + 1.572 + let now = Date.now() / 1000 - 10; 1.573 + engine.lastSync = now; 1.574 + engine.lastModified = now + 1; 1.575 + 1.576 + let record = encryptPayload({id: "DUPE_INCOMING", denomination: "incoming"}); 1.577 + let wbo = new ServerWBO("DUPE_INCOMING", record, now + 2); 1.578 + server.insertWBO(user, "rotary", wbo); 1.579 + 1.580 + engine._store.create({id: "DUPE_LOCAL", denomination: "local"}); 1.581 + engine._tracker.addChangedID("DUPE_LOCAL", now + 1); 1.582 + do_check_true(engine._store.itemExists("DUPE_LOCAL")); 1.583 + do_check_eq("DUPE_LOCAL", engine._findDupe({id: "DUPE_INCOMING"})); 1.584 + 1.585 + engine._sync(); 1.586 + 1.587 + // The ID should have been changed to incoming. 1.588 + do_check_attribute_count(engine._store.items, 1); 1.589 + do_check_true("DUPE_INCOMING" in engine._store.items); 1.590 + 1.591 + // On the server, the local ID should be deleted and the incoming ID should 1.592 + // have its payload retained. 1.593 + let collection = server.getCollection(user, "rotary"); 1.594 + do_check_eq(1, collection.count()); 1.595 + let wbo = collection.wbo("DUPE_INCOMING"); 1.596 + do_check_neq(undefined, wbo); 1.597 + let payload = JSON.parse(JSON.parse(wbo.payload).ciphertext); 1.598 + do_check_eq("incoming", payload.denomination); 1.599 + cleanAndGo(server); 1.600 +}); 1.601 + 1.602 +add_test(function test_processIncoming_mobile_batchSize() { 1.603 + _("SyncEngine._processIncoming doesn't fetch everything at once on mobile clients"); 1.604 + 1.605 + Svc.Prefs.set("client.type", "mobile"); 1.606 + Service.identity.username = "foo"; 1.607 + 1.608 + // A collection that logs each GET 1.609 + let collection = new ServerCollection(); 1.610 + collection.get_log = []; 1.611 + collection._get = collection.get; 1.612 + collection.get = function (options) { 1.613 + this.get_log.push(options); 1.614 + return this._get(options); 1.615 + }; 1.616 + 1.617 + // Let's create some 234 server side records. They're all at least 1.618 + // 10 minutes old. 1.619 + for (let i = 0; i < 234; i++) { 1.620 + let id = 'record-no-' + i; 1.621 + let payload = encryptPayload({id: id, denomination: "Record No. " + i}); 1.622 + let wbo = new ServerWBO(id, payload); 1.623 + wbo.modified = Date.now()/1000 - 60*(i+10); 1.624 + collection.insertWBO(wbo); 1.625 + } 1.626 + 1.627 + let server = sync_httpd_setup({ 1.628 + "/1.1/foo/storage/rotary": collection.handler() 1.629 + }); 1.630 + 1.631 + let syncTesting = new SyncTestingInfrastructure(server); 1.632 + 1.633 + let engine = makeRotaryEngine(); 1.634 + let meta_global = Service.recordManager.set(engine.metaURL, 1.635 + new WBORecord(engine.metaURL)); 1.636 + meta_global.payload.engines = {rotary: {version: engine.version, 1.637 + syncID: engine.syncID}}; 1.638 + 1.639 + try { 1.640 + 1.641 + _("On a mobile client, we get new records from the server in batches of 50."); 1.642 + engine._syncStartup(); 1.643 + engine._processIncoming(); 1.644 + do_check_attribute_count(engine._store.items, 234); 1.645 + do_check_true('record-no-0' in engine._store.items); 1.646 + do_check_true('record-no-49' in engine._store.items); 1.647 + do_check_true('record-no-50' in engine._store.items); 1.648 + do_check_true('record-no-233' in engine._store.items); 1.649 + 1.650 + // Verify that the right number of GET requests with the right 1.651 + // kind of parameters were made. 1.652 + do_check_eq(collection.get_log.length, 1.653 + Math.ceil(234 / MOBILE_BATCH_SIZE) + 1); 1.654 + do_check_eq(collection.get_log[0].full, 1); 1.655 + do_check_eq(collection.get_log[0].limit, MOBILE_BATCH_SIZE); 1.656 + do_check_eq(collection.get_log[1].full, undefined); 1.657 + do_check_eq(collection.get_log[1].limit, undefined); 1.658 + for (let i = 1; i <= Math.floor(234 / MOBILE_BATCH_SIZE); i++) { 1.659 + do_check_eq(collection.get_log[i+1].full, 1); 1.660 + do_check_eq(collection.get_log[i+1].limit, undefined); 1.661 + if (i < Math.floor(234 / MOBILE_BATCH_SIZE)) 1.662 + do_check_eq(collection.get_log[i+1].ids.length, MOBILE_BATCH_SIZE); 1.663 + else 1.664 + do_check_eq(collection.get_log[i+1].ids.length, 234 % MOBILE_BATCH_SIZE); 1.665 + } 1.666 + 1.667 + } finally { 1.668 + cleanAndGo(server); 1.669 + } 1.670 +}); 1.671 + 1.672 + 1.673 +add_test(function test_processIncoming_store_toFetch() { 1.674 + _("If processIncoming fails in the middle of a batch on mobile, state is saved in toFetch and lastSync."); 1.675 + Service.identity.username = "foo"; 1.676 + Svc.Prefs.set("client.type", "mobile"); 1.677 + 1.678 + // A collection that throws at the fourth get. 1.679 + let collection = new ServerCollection(); 1.680 + collection._get_calls = 0; 1.681 + collection._get = collection.get; 1.682 + collection.get = function() { 1.683 + this._get_calls += 1; 1.684 + if (this._get_calls > 3) { 1.685 + throw "Abort on fourth call!"; 1.686 + } 1.687 + return this._get.apply(this, arguments); 1.688 + }; 1.689 + 1.690 + // Let's create three batches worth of server side records. 1.691 + for (var i = 0; i < MOBILE_BATCH_SIZE * 3; i++) { 1.692 + let id = 'record-no-' + i; 1.693 + let payload = encryptPayload({id: id, denomination: "Record No. " + id}); 1.694 + let wbo = new ServerWBO(id, payload); 1.695 + wbo.modified = Date.now()/1000 + 60 * (i - MOBILE_BATCH_SIZE * 3); 1.696 + collection.insertWBO(wbo); 1.697 + } 1.698 + 1.699 + let engine = makeRotaryEngine(); 1.700 + engine.enabled = true; 1.701 + 1.702 + let server = sync_httpd_setup({ 1.703 + "/1.1/foo/storage/rotary": collection.handler() 1.704 + }); 1.705 + 1.706 + let syncTesting = new SyncTestingInfrastructure(server); 1.707 + 1.708 + let meta_global = Service.recordManager.set(engine.metaURL, 1.709 + new WBORecord(engine.metaURL)); 1.710 + meta_global.payload.engines = {rotary: {version: engine.version, 1.711 + syncID: engine.syncID}}; 1.712 + try { 1.713 + 1.714 + // Confirm initial environment 1.715 + do_check_eq(engine.lastSync, 0); 1.716 + do_check_empty(engine._store.items); 1.717 + 1.718 + let error; 1.719 + try { 1.720 + engine.sync(); 1.721 + } catch (ex) { 1.722 + error = ex; 1.723 + } 1.724 + do_check_true(!!error); 1.725 + 1.726 + // Only the first two batches have been applied. 1.727 + do_check_eq(Object.keys(engine._store.items).length, 1.728 + MOBILE_BATCH_SIZE * 2); 1.729 + 1.730 + // The third batch is stuck in toFetch. lastSync has been moved forward to 1.731 + // the last successful item's timestamp. 1.732 + do_check_eq(engine.toFetch.length, MOBILE_BATCH_SIZE); 1.733 + do_check_eq(engine.lastSync, collection.wbo("record-no-99").modified); 1.734 + 1.735 + } finally { 1.736 + cleanAndGo(server); 1.737 + } 1.738 +}); 1.739 + 1.740 + 1.741 +add_test(function test_processIncoming_resume_toFetch() { 1.742 + _("toFetch and previousFailed items left over from previous syncs are fetched on the next sync, along with new items."); 1.743 + Service.identity.username = "foo"; 1.744 + 1.745 + const LASTSYNC = Date.now() / 1000; 1.746 + 1.747 + // Server records that will be downloaded 1.748 + let collection = new ServerCollection(); 1.749 + collection.insert('flying', 1.750 + encryptPayload({id: 'flying', 1.751 + denomination: "LNER Class A3 4472"})); 1.752 + collection.insert('scotsman', 1.753 + encryptPayload({id: 'scotsman', 1.754 + denomination: "Flying Scotsman"})); 1.755 + collection.insert('rekolok', 1.756 + encryptPayload({id: 'rekolok', 1.757 + denomination: "Rekonstruktionslokomotive"})); 1.758 + for (let i = 0; i < 3; i++) { 1.759 + let id = 'failed' + i; 1.760 + let payload = encryptPayload({id: id, denomination: "Record No. " + i}); 1.761 + let wbo = new ServerWBO(id, payload); 1.762 + wbo.modified = LASTSYNC - 10; 1.763 + collection.insertWBO(wbo); 1.764 + } 1.765 + 1.766 + collection.wbo("flying").modified = 1.767 + collection.wbo("scotsman").modified = LASTSYNC - 10; 1.768 + collection._wbos.rekolok.modified = LASTSYNC + 10; 1.769 + 1.770 + // Time travel 10 seconds into the future but still download the above WBOs. 1.771 + let engine = makeRotaryEngine(); 1.772 + engine.lastSync = LASTSYNC; 1.773 + engine.toFetch = ["flying", "scotsman"]; 1.774 + engine.previousFailed = ["failed0", "failed1", "failed2"]; 1.775 + 1.776 + let server = sync_httpd_setup({ 1.777 + "/1.1/foo/storage/rotary": collection.handler() 1.778 + }); 1.779 + 1.780 + let syncTesting = new SyncTestingInfrastructure(server); 1.781 + 1.782 + let meta_global = Service.recordManager.set(engine.metaURL, 1.783 + new WBORecord(engine.metaURL)); 1.784 + meta_global.payload.engines = {rotary: {version: engine.version, 1.785 + syncID: engine.syncID}}; 1.786 + try { 1.787 + 1.788 + // Confirm initial environment 1.789 + do_check_eq(engine._store.items.flying, undefined); 1.790 + do_check_eq(engine._store.items.scotsman, undefined); 1.791 + do_check_eq(engine._store.items.rekolok, undefined); 1.792 + 1.793 + engine._syncStartup(); 1.794 + engine._processIncoming(); 1.795 + 1.796 + // Local records have been created from the server data. 1.797 + do_check_eq(engine._store.items.flying, "LNER Class A3 4472"); 1.798 + do_check_eq(engine._store.items.scotsman, "Flying Scotsman"); 1.799 + do_check_eq(engine._store.items.rekolok, "Rekonstruktionslokomotive"); 1.800 + do_check_eq(engine._store.items.failed0, "Record No. 0"); 1.801 + do_check_eq(engine._store.items.failed1, "Record No. 1"); 1.802 + do_check_eq(engine._store.items.failed2, "Record No. 2"); 1.803 + do_check_eq(engine.previousFailed.length, 0); 1.804 + } finally { 1.805 + cleanAndGo(server); 1.806 + } 1.807 +}); 1.808 + 1.809 + 1.810 +add_test(function test_processIncoming_applyIncomingBatchSize_smaller() { 1.811 + _("Ensure that a number of incoming items less than applyIncomingBatchSize is still applied."); 1.812 + Service.identity.username = "foo"; 1.813 + 1.814 + // Engine that doesn't like the first and last record it's given. 1.815 + const APPLY_BATCH_SIZE = 10; 1.816 + let engine = makeRotaryEngine(); 1.817 + engine.applyIncomingBatchSize = APPLY_BATCH_SIZE; 1.818 + engine._store._applyIncomingBatch = engine._store.applyIncomingBatch; 1.819 + engine._store.applyIncomingBatch = function (records) { 1.820 + let failed1 = records.shift(); 1.821 + let failed2 = records.pop(); 1.822 + this._applyIncomingBatch(records); 1.823 + return [failed1.id, failed2.id]; 1.824 + }; 1.825 + 1.826 + // Let's create less than a batch worth of server side records. 1.827 + let collection = new ServerCollection(); 1.828 + for (let i = 0; i < APPLY_BATCH_SIZE - 1; i++) { 1.829 + let id = 'record-no-' + i; 1.830 + let payload = encryptPayload({id: id, denomination: "Record No. " + id}); 1.831 + collection.insert(id, payload); 1.832 + } 1.833 + 1.834 + let server = sync_httpd_setup({ 1.835 + "/1.1/foo/storage/rotary": collection.handler() 1.836 + }); 1.837 + 1.838 + let syncTesting = new SyncTestingInfrastructure(server); 1.839 + 1.840 + let meta_global = Service.recordManager.set(engine.metaURL, 1.841 + new WBORecord(engine.metaURL)); 1.842 + meta_global.payload.engines = {rotary: {version: engine.version, 1.843 + syncID: engine.syncID}}; 1.844 + try { 1.845 + 1.846 + // Confirm initial environment 1.847 + do_check_empty(engine._store.items); 1.848 + 1.849 + engine._syncStartup(); 1.850 + engine._processIncoming(); 1.851 + 1.852 + // Records have been applied and the expected failures have failed. 1.853 + do_check_attribute_count(engine._store.items, APPLY_BATCH_SIZE - 1 - 2); 1.854 + do_check_eq(engine.toFetch.length, 0); 1.855 + do_check_eq(engine.previousFailed.length, 2); 1.856 + do_check_eq(engine.previousFailed[0], "record-no-0"); 1.857 + do_check_eq(engine.previousFailed[1], "record-no-8"); 1.858 + 1.859 + } finally { 1.860 + cleanAndGo(server); 1.861 + } 1.862 +}); 1.863 + 1.864 + 1.865 +add_test(function test_processIncoming_applyIncomingBatchSize_multiple() { 1.866 + _("Ensure that incoming items are applied according to applyIncomingBatchSize."); 1.867 + Service.identity.username = "foo"; 1.868 + 1.869 + const APPLY_BATCH_SIZE = 10; 1.870 + 1.871 + // Engine that applies records in batches. 1.872 + let engine = makeRotaryEngine(); 1.873 + engine.applyIncomingBatchSize = APPLY_BATCH_SIZE; 1.874 + let batchCalls = 0; 1.875 + engine._store._applyIncomingBatch = engine._store.applyIncomingBatch; 1.876 + engine._store.applyIncomingBatch = function (records) { 1.877 + batchCalls += 1; 1.878 + do_check_eq(records.length, APPLY_BATCH_SIZE); 1.879 + this._applyIncomingBatch.apply(this, arguments); 1.880 + }; 1.881 + 1.882 + // Let's create three batches worth of server side records. 1.883 + let collection = new ServerCollection(); 1.884 + for (let i = 0; i < APPLY_BATCH_SIZE * 3; i++) { 1.885 + let id = 'record-no-' + i; 1.886 + let payload = encryptPayload({id: id, denomination: "Record No. " + id}); 1.887 + collection.insert(id, payload); 1.888 + } 1.889 + 1.890 + let server = sync_httpd_setup({ 1.891 + "/1.1/foo/storage/rotary": collection.handler() 1.892 + }); 1.893 + 1.894 + let syncTesting = new SyncTestingInfrastructure(server); 1.895 + 1.896 + let meta_global = Service.recordManager.set(engine.metaURL, 1.897 + new WBORecord(engine.metaURL)); 1.898 + meta_global.payload.engines = {rotary: {version: engine.version, 1.899 + syncID: engine.syncID}}; 1.900 + try { 1.901 + 1.902 + // Confirm initial environment 1.903 + do_check_empty(engine._store.items); 1.904 + 1.905 + engine._syncStartup(); 1.906 + engine._processIncoming(); 1.907 + 1.908 + // Records have been applied in 3 batches. 1.909 + do_check_eq(batchCalls, 3); 1.910 + do_check_attribute_count(engine._store.items, APPLY_BATCH_SIZE * 3); 1.911 + 1.912 + } finally { 1.913 + cleanAndGo(server); 1.914 + } 1.915 +}); 1.916 + 1.917 + 1.918 +add_test(function test_processIncoming_notify_count() { 1.919 + _("Ensure that failed records are reported only once."); 1.920 + Service.identity.username = "foo"; 1.921 + 1.922 + const APPLY_BATCH_SIZE = 5; 1.923 + const NUMBER_OF_RECORDS = 15; 1.924 + 1.925 + // Engine that fails the first record. 1.926 + let engine = makeRotaryEngine(); 1.927 + engine.applyIncomingBatchSize = APPLY_BATCH_SIZE; 1.928 + engine._store._applyIncomingBatch = engine._store.applyIncomingBatch; 1.929 + engine._store.applyIncomingBatch = function (records) { 1.930 + engine._store._applyIncomingBatch(records.slice(1)); 1.931 + return [records[0].id]; 1.932 + }; 1.933 + 1.934 + // Create a batch of server side records. 1.935 + let collection = new ServerCollection(); 1.936 + for (var i = 0; i < NUMBER_OF_RECORDS; i++) { 1.937 + let id = 'record-no-' + i; 1.938 + let payload = encryptPayload({id: id, denomination: "Record No. " + id}); 1.939 + collection.insert(id, payload); 1.940 + } 1.941 + 1.942 + let server = sync_httpd_setup({ 1.943 + "/1.1/foo/storage/rotary": collection.handler() 1.944 + }); 1.945 + 1.946 + let syncTesting = new SyncTestingInfrastructure(server); 1.947 + 1.948 + let meta_global = Service.recordManager.set(engine.metaURL, 1.949 + new WBORecord(engine.metaURL)); 1.950 + meta_global.payload.engines = {rotary: {version: engine.version, 1.951 + syncID: engine.syncID}}; 1.952 + try { 1.953 + // Confirm initial environment. 1.954 + do_check_eq(engine.lastSync, 0); 1.955 + do_check_eq(engine.toFetch.length, 0); 1.956 + do_check_eq(engine.previousFailed.length, 0); 1.957 + do_check_empty(engine._store.items); 1.958 + 1.959 + let called = 0; 1.960 + let counts; 1.961 + function onApplied(count) { 1.962 + _("Called with " + JSON.stringify(counts)); 1.963 + counts = count; 1.964 + called++; 1.965 + } 1.966 + Svc.Obs.add("weave:engine:sync:applied", onApplied); 1.967 + 1.968 + // Do sync. 1.969 + engine._syncStartup(); 1.970 + engine._processIncoming(); 1.971 + 1.972 + // Confirm failures. 1.973 + do_check_attribute_count(engine._store.items, 12); 1.974 + do_check_eq(engine.previousFailed.length, 3); 1.975 + do_check_eq(engine.previousFailed[0], "record-no-0"); 1.976 + do_check_eq(engine.previousFailed[1], "record-no-5"); 1.977 + do_check_eq(engine.previousFailed[2], "record-no-10"); 1.978 + 1.979 + // There are newly failed records and they are reported. 1.980 + do_check_eq(called, 1); 1.981 + do_check_eq(counts.failed, 3); 1.982 + do_check_eq(counts.applied, 15); 1.983 + do_check_eq(counts.newFailed, 3); 1.984 + do_check_eq(counts.succeeded, 12); 1.985 + 1.986 + // Sync again, 1 of the failed items are the same, the rest didn't fail. 1.987 + engine._processIncoming(); 1.988 + 1.989 + // Confirming removed failures. 1.990 + do_check_attribute_count(engine._store.items, 14); 1.991 + do_check_eq(engine.previousFailed.length, 1); 1.992 + do_check_eq(engine.previousFailed[0], "record-no-0"); 1.993 + 1.994 + do_check_eq(called, 2); 1.995 + do_check_eq(counts.failed, 1); 1.996 + do_check_eq(counts.applied, 3); 1.997 + do_check_eq(counts.newFailed, 0); 1.998 + do_check_eq(counts.succeeded, 2); 1.999 + 1.1000 + Svc.Obs.remove("weave:engine:sync:applied", onApplied); 1.1001 + } finally { 1.1002 + cleanAndGo(server); 1.1003 + } 1.1004 +}); 1.1005 + 1.1006 + 1.1007 +add_test(function test_processIncoming_previousFailed() { 1.1008 + _("Ensure that failed records are retried."); 1.1009 + Service.identity.username = "foo"; 1.1010 + Svc.Prefs.set("client.type", "mobile"); 1.1011 + 1.1012 + const APPLY_BATCH_SIZE = 4; 1.1013 + const NUMBER_OF_RECORDS = 14; 1.1014 + 1.1015 + // Engine that fails the first 2 records. 1.1016 + let engine = makeRotaryEngine(); 1.1017 + engine.mobileGUIDFetchBatchSize = engine.applyIncomingBatchSize = APPLY_BATCH_SIZE; 1.1018 + engine._store._applyIncomingBatch = engine._store.applyIncomingBatch; 1.1019 + engine._store.applyIncomingBatch = function (records) { 1.1020 + engine._store._applyIncomingBatch(records.slice(2)); 1.1021 + return [records[0].id, records[1].id]; 1.1022 + }; 1.1023 + 1.1024 + // Create a batch of server side records. 1.1025 + let collection = new ServerCollection(); 1.1026 + for (var i = 0; i < NUMBER_OF_RECORDS; i++) { 1.1027 + let id = 'record-no-' + i; 1.1028 + let payload = encryptPayload({id: id, denomination: "Record No. " + i}); 1.1029 + collection.insert(id, payload); 1.1030 + } 1.1031 + 1.1032 + let server = sync_httpd_setup({ 1.1033 + "/1.1/foo/storage/rotary": collection.handler() 1.1034 + }); 1.1035 + 1.1036 + let syncTesting = new SyncTestingInfrastructure(server); 1.1037 + 1.1038 + let meta_global = Service.recordManager.set(engine.metaURL, 1.1039 + new WBORecord(engine.metaURL)); 1.1040 + meta_global.payload.engines = {rotary: {version: engine.version, 1.1041 + syncID: engine.syncID}}; 1.1042 + try { 1.1043 + // Confirm initial environment. 1.1044 + do_check_eq(engine.lastSync, 0); 1.1045 + do_check_eq(engine.toFetch.length, 0); 1.1046 + do_check_eq(engine.previousFailed.length, 0); 1.1047 + do_check_empty(engine._store.items); 1.1048 + 1.1049 + // Initial failed items in previousFailed to be reset. 1.1050 + let previousFailed = [Utils.makeGUID(), Utils.makeGUID(), Utils.makeGUID()]; 1.1051 + engine.previousFailed = previousFailed; 1.1052 + do_check_eq(engine.previousFailed, previousFailed); 1.1053 + 1.1054 + // Do sync. 1.1055 + engine._syncStartup(); 1.1056 + engine._processIncoming(); 1.1057 + 1.1058 + // Expected result: 4 sync batches with 2 failures each => 8 failures 1.1059 + do_check_attribute_count(engine._store.items, 6); 1.1060 + do_check_eq(engine.previousFailed.length, 8); 1.1061 + do_check_eq(engine.previousFailed[0], "record-no-0"); 1.1062 + do_check_eq(engine.previousFailed[1], "record-no-1"); 1.1063 + do_check_eq(engine.previousFailed[2], "record-no-4"); 1.1064 + do_check_eq(engine.previousFailed[3], "record-no-5"); 1.1065 + do_check_eq(engine.previousFailed[4], "record-no-8"); 1.1066 + do_check_eq(engine.previousFailed[5], "record-no-9"); 1.1067 + do_check_eq(engine.previousFailed[6], "record-no-12"); 1.1068 + do_check_eq(engine.previousFailed[7], "record-no-13"); 1.1069 + 1.1070 + // Sync again with the same failed items (records 0, 1, 8, 9). 1.1071 + engine._processIncoming(); 1.1072 + 1.1073 + // A second sync with the same failed items should not add the same items again. 1.1074 + // Items that did not fail a second time should no longer be in previousFailed. 1.1075 + do_check_attribute_count(engine._store.items, 10); 1.1076 + do_check_eq(engine.previousFailed.length, 4); 1.1077 + do_check_eq(engine.previousFailed[0], "record-no-0"); 1.1078 + do_check_eq(engine.previousFailed[1], "record-no-1"); 1.1079 + do_check_eq(engine.previousFailed[2], "record-no-8"); 1.1080 + do_check_eq(engine.previousFailed[3], "record-no-9"); 1.1081 + 1.1082 + // Refetched items that didn't fail the second time are in engine._store.items. 1.1083 + do_check_eq(engine._store.items['record-no-4'], "Record No. 4"); 1.1084 + do_check_eq(engine._store.items['record-no-5'], "Record No. 5"); 1.1085 + do_check_eq(engine._store.items['record-no-12'], "Record No. 12"); 1.1086 + do_check_eq(engine._store.items['record-no-13'], "Record No. 13"); 1.1087 + } finally { 1.1088 + cleanAndGo(server); 1.1089 + } 1.1090 +}); 1.1091 + 1.1092 + 1.1093 +add_test(function test_processIncoming_failed_records() { 1.1094 + _("Ensure that failed records from _reconcile and applyIncomingBatch are refetched."); 1.1095 + Service.identity.username = "foo"; 1.1096 + 1.1097 + // Let's create three and a bit batches worth of server side records. 1.1098 + let collection = new ServerCollection(); 1.1099 + const NUMBER_OF_RECORDS = MOBILE_BATCH_SIZE * 3 + 5; 1.1100 + for (let i = 0; i < NUMBER_OF_RECORDS; i++) { 1.1101 + let id = 'record-no-' + i; 1.1102 + let payload = encryptPayload({id: id, denomination: "Record No. " + id}); 1.1103 + let wbo = new ServerWBO(id, payload); 1.1104 + wbo.modified = Date.now()/1000 + 60 * (i - MOBILE_BATCH_SIZE * 3); 1.1105 + collection.insertWBO(wbo); 1.1106 + } 1.1107 + 1.1108 + // Engine that batches but likes to throw on a couple of records, 1.1109 + // two in each batch: the even ones fail in reconcile, the odd ones 1.1110 + // in applyIncoming. 1.1111 + const BOGUS_RECORDS = ["record-no-" + 42, 1.1112 + "record-no-" + 23, 1.1113 + "record-no-" + (42 + MOBILE_BATCH_SIZE), 1.1114 + "record-no-" + (23 + MOBILE_BATCH_SIZE), 1.1115 + "record-no-" + (42 + MOBILE_BATCH_SIZE * 2), 1.1116 + "record-no-" + (23 + MOBILE_BATCH_SIZE * 2), 1.1117 + "record-no-" + (2 + MOBILE_BATCH_SIZE * 3), 1.1118 + "record-no-" + (1 + MOBILE_BATCH_SIZE * 3)]; 1.1119 + let engine = makeRotaryEngine(); 1.1120 + engine.applyIncomingBatchSize = MOBILE_BATCH_SIZE; 1.1121 + 1.1122 + engine.__reconcile = engine._reconcile; 1.1123 + engine._reconcile = function _reconcile(record) { 1.1124 + if (BOGUS_RECORDS.indexOf(record.id) % 2 == 0) { 1.1125 + throw "I don't like this record! Baaaaaah!"; 1.1126 + } 1.1127 + return this.__reconcile.apply(this, arguments); 1.1128 + }; 1.1129 + engine._store._applyIncoming = engine._store.applyIncoming; 1.1130 + engine._store.applyIncoming = function (record) { 1.1131 + if (BOGUS_RECORDS.indexOf(record.id) % 2 == 1) { 1.1132 + throw "I don't like this record! Baaaaaah!"; 1.1133 + } 1.1134 + return this._applyIncoming.apply(this, arguments); 1.1135 + }; 1.1136 + 1.1137 + // Keep track of requests made of a collection. 1.1138 + let count = 0; 1.1139 + let uris = []; 1.1140 + function recording_handler(collection) { 1.1141 + let h = collection.handler(); 1.1142 + return function(req, res) { 1.1143 + ++count; 1.1144 + uris.push(req.path + "?" + req.queryString); 1.1145 + return h(req, res); 1.1146 + }; 1.1147 + } 1.1148 + let server = sync_httpd_setup({ 1.1149 + "/1.1/foo/storage/rotary": recording_handler(collection) 1.1150 + }); 1.1151 + 1.1152 + let syncTesting = new SyncTestingInfrastructure(server); 1.1153 + 1.1154 + let meta_global = Service.recordManager.set(engine.metaURL, 1.1155 + new WBORecord(engine.metaURL)); 1.1156 + meta_global.payload.engines = {rotary: {version: engine.version, 1.1157 + syncID: engine.syncID}}; 1.1158 + 1.1159 + try { 1.1160 + 1.1161 + // Confirm initial environment 1.1162 + do_check_eq(engine.lastSync, 0); 1.1163 + do_check_eq(engine.toFetch.length, 0); 1.1164 + do_check_eq(engine.previousFailed.length, 0); 1.1165 + do_check_empty(engine._store.items); 1.1166 + 1.1167 + let observerSubject; 1.1168 + let observerData; 1.1169 + Svc.Obs.add("weave:engine:sync:applied", function onApplied(subject, data) { 1.1170 + Svc.Obs.remove("weave:engine:sync:applied", onApplied); 1.1171 + observerSubject = subject; 1.1172 + observerData = data; 1.1173 + }); 1.1174 + 1.1175 + engine._syncStartup(); 1.1176 + engine._processIncoming(); 1.1177 + 1.1178 + // Ensure that all records but the bogus 4 have been applied. 1.1179 + do_check_attribute_count(engine._store.items, 1.1180 + NUMBER_OF_RECORDS - BOGUS_RECORDS.length); 1.1181 + 1.1182 + // Ensure that the bogus records will be fetched again on the next sync. 1.1183 + do_check_eq(engine.previousFailed.length, BOGUS_RECORDS.length); 1.1184 + engine.previousFailed.sort(); 1.1185 + BOGUS_RECORDS.sort(); 1.1186 + for (let i = 0; i < engine.previousFailed.length; i++) { 1.1187 + do_check_eq(engine.previousFailed[i], BOGUS_RECORDS[i]); 1.1188 + } 1.1189 + 1.1190 + // Ensure the observer was notified 1.1191 + do_check_eq(observerData, engine.name); 1.1192 + do_check_eq(observerSubject.failed, BOGUS_RECORDS.length); 1.1193 + do_check_eq(observerSubject.newFailed, BOGUS_RECORDS.length); 1.1194 + 1.1195 + // Testing batching of failed item fetches. 1.1196 + // Try to sync again. Ensure that we split the request into chunks to avoid 1.1197 + // URI length limitations. 1.1198 + function batchDownload(batchSize) { 1.1199 + count = 0; 1.1200 + uris = []; 1.1201 + engine.guidFetchBatchSize = batchSize; 1.1202 + engine._processIncoming(); 1.1203 + _("Tried again. Requests: " + count + "; URIs: " + JSON.stringify(uris)); 1.1204 + return count; 1.1205 + } 1.1206 + 1.1207 + // There are 8 bad records, so this needs 3 fetches. 1.1208 + _("Test batching with ID batch size 3, normal mobile batch size."); 1.1209 + do_check_eq(batchDownload(3), 3); 1.1210 + 1.1211 + // Now see with a more realistic limit. 1.1212 + _("Test batching with sufficient ID batch size."); 1.1213 + do_check_eq(batchDownload(BOGUS_RECORDS.length), 1); 1.1214 + 1.1215 + // If we're on mobile, that limit is used by default. 1.1216 + _("Test batching with tiny mobile batch size."); 1.1217 + Svc.Prefs.set("client.type", "mobile"); 1.1218 + engine.mobileGUIDFetchBatchSize = 2; 1.1219 + do_check_eq(batchDownload(BOGUS_RECORDS.length), 4); 1.1220 + 1.1221 + } finally { 1.1222 + cleanAndGo(server); 1.1223 + } 1.1224 +}); 1.1225 + 1.1226 + 1.1227 +add_test(function test_processIncoming_decrypt_failed() { 1.1228 + _("Ensure that records failing to decrypt are either replaced or refetched."); 1.1229 + 1.1230 + Service.identity.username = "foo"; 1.1231 + 1.1232 + // Some good and some bogus records. One doesn't contain valid JSON, 1.1233 + // the other will throw during decrypt. 1.1234 + let collection = new ServerCollection(); 1.1235 + collection._wbos.flying = new ServerWBO( 1.1236 + 'flying', encryptPayload({id: 'flying', 1.1237 + denomination: "LNER Class A3 4472"})); 1.1238 + collection._wbos.nojson = new ServerWBO("nojson", "This is invalid JSON"); 1.1239 + collection._wbos.nojson2 = new ServerWBO("nojson2", "This is invalid JSON"); 1.1240 + collection._wbos.scotsman = new ServerWBO( 1.1241 + 'scotsman', encryptPayload({id: 'scotsman', 1.1242 + denomination: "Flying Scotsman"})); 1.1243 + collection._wbos.nodecrypt = new ServerWBO("nodecrypt", "Decrypt this!"); 1.1244 + collection._wbos.nodecrypt2 = new ServerWBO("nodecrypt2", "Decrypt this!"); 1.1245 + 1.1246 + // Patch the fake crypto service to throw on the record above. 1.1247 + Svc.Crypto._decrypt = Svc.Crypto.decrypt; 1.1248 + Svc.Crypto.decrypt = function (ciphertext) { 1.1249 + if (ciphertext == "Decrypt this!") { 1.1250 + throw "Derp! Cipher finalized failed. Im ur crypto destroyin ur recordz."; 1.1251 + } 1.1252 + return this._decrypt.apply(this, arguments); 1.1253 + }; 1.1254 + 1.1255 + // Some broken records also exist locally. 1.1256 + let engine = makeRotaryEngine(); 1.1257 + engine.enabled = true; 1.1258 + engine._store.items = {nojson: "Valid JSON", 1.1259 + nodecrypt: "Valid ciphertext"}; 1.1260 + 1.1261 + let server = sync_httpd_setup({ 1.1262 + "/1.1/foo/storage/rotary": collection.handler() 1.1263 + }); 1.1264 + 1.1265 + let syncTesting = new SyncTestingInfrastructure(server); 1.1266 + 1.1267 + let meta_global = Service.recordManager.set(engine.metaURL, 1.1268 + new WBORecord(engine.metaURL)); 1.1269 + meta_global.payload.engines = {rotary: {version: engine.version, 1.1270 + syncID: engine.syncID}}; 1.1271 + try { 1.1272 + 1.1273 + // Confirm initial state 1.1274 + do_check_eq(engine.toFetch.length, 0); 1.1275 + do_check_eq(engine.previousFailed.length, 0); 1.1276 + 1.1277 + let observerSubject; 1.1278 + let observerData; 1.1279 + Svc.Obs.add("weave:engine:sync:applied", function onApplied(subject, data) { 1.1280 + Svc.Obs.remove("weave:engine:sync:applied", onApplied); 1.1281 + observerSubject = subject; 1.1282 + observerData = data; 1.1283 + }); 1.1284 + 1.1285 + engine.lastSync = collection.wbo("nojson").modified - 1; 1.1286 + engine.sync(); 1.1287 + 1.1288 + do_check_eq(engine.previousFailed.length, 4); 1.1289 + do_check_eq(engine.previousFailed[0], "nojson"); 1.1290 + do_check_eq(engine.previousFailed[1], "nojson2"); 1.1291 + do_check_eq(engine.previousFailed[2], "nodecrypt"); 1.1292 + do_check_eq(engine.previousFailed[3], "nodecrypt2"); 1.1293 + 1.1294 + // Ensure the observer was notified 1.1295 + do_check_eq(observerData, engine.name); 1.1296 + do_check_eq(observerSubject.applied, 2); 1.1297 + do_check_eq(observerSubject.failed, 4); 1.1298 + 1.1299 + } finally { 1.1300 + cleanAndGo(server); 1.1301 + } 1.1302 +}); 1.1303 + 1.1304 + 1.1305 +add_test(function test_uploadOutgoing_toEmptyServer() { 1.1306 + _("SyncEngine._uploadOutgoing uploads new records to server"); 1.1307 + 1.1308 + Service.identity.username = "foo"; 1.1309 + let collection = new ServerCollection(); 1.1310 + collection._wbos.flying = new ServerWBO('flying'); 1.1311 + collection._wbos.scotsman = new ServerWBO('scotsman'); 1.1312 + 1.1313 + let server = sync_httpd_setup({ 1.1314 + "/1.1/foo/storage/rotary": collection.handler(), 1.1315 + "/1.1/foo/storage/rotary/flying": collection.wbo("flying").handler(), 1.1316 + "/1.1/foo/storage/rotary/scotsman": collection.wbo("scotsman").handler() 1.1317 + }); 1.1318 + 1.1319 + let syncTesting = new SyncTestingInfrastructure(server); 1.1320 + generateNewKeys(Service.collectionKeys); 1.1321 + 1.1322 + let engine = makeRotaryEngine(); 1.1323 + engine.lastSync = 123; // needs to be non-zero so that tracker is queried 1.1324 + engine._store.items = {flying: "LNER Class A3 4472", 1.1325 + scotsman: "Flying Scotsman"}; 1.1326 + // Mark one of these records as changed 1.1327 + engine._tracker.addChangedID('scotsman', 0); 1.1328 + 1.1329 + let meta_global = Service.recordManager.set(engine.metaURL, 1.1330 + new WBORecord(engine.metaURL)); 1.1331 + meta_global.payload.engines = {rotary: {version: engine.version, 1.1332 + syncID: engine.syncID}}; 1.1333 + 1.1334 + try { 1.1335 + 1.1336 + // Confirm initial environment 1.1337 + do_check_eq(engine.lastSyncLocal, 0); 1.1338 + do_check_eq(collection.payload("flying"), undefined); 1.1339 + do_check_eq(collection.payload("scotsman"), undefined); 1.1340 + 1.1341 + engine._syncStartup(); 1.1342 + engine._uploadOutgoing(); 1.1343 + 1.1344 + // Local timestamp has been set. 1.1345 + do_check_true(engine.lastSyncLocal > 0); 1.1346 + 1.1347 + // Ensure the marked record ('scotsman') has been uploaded and is 1.1348 + // no longer marked. 1.1349 + do_check_eq(collection.payload("flying"), undefined); 1.1350 + do_check_true(!!collection.payload("scotsman")); 1.1351 + do_check_eq(JSON.parse(collection.wbo("scotsman").data.ciphertext).id, 1.1352 + "scotsman"); 1.1353 + do_check_eq(engine._tracker.changedIDs["scotsman"], undefined); 1.1354 + 1.1355 + // The 'flying' record wasn't marked so it wasn't uploaded 1.1356 + do_check_eq(collection.payload("flying"), undefined); 1.1357 + 1.1358 + } finally { 1.1359 + cleanAndGo(server); 1.1360 + } 1.1361 +}); 1.1362 + 1.1363 + 1.1364 +add_test(function test_uploadOutgoing_failed() { 1.1365 + _("SyncEngine._uploadOutgoing doesn't clear the tracker of objects that failed to upload."); 1.1366 + 1.1367 + Service.identity.username = "foo"; 1.1368 + let collection = new ServerCollection(); 1.1369 + // We only define the "flying" WBO on the server, not the "scotsman" 1.1370 + // and "peppercorn" ones. 1.1371 + collection._wbos.flying = new ServerWBO('flying'); 1.1372 + 1.1373 + let server = sync_httpd_setup({ 1.1374 + "/1.1/foo/storage/rotary": collection.handler() 1.1375 + }); 1.1376 + 1.1377 + let syncTesting = new SyncTestingInfrastructure(server); 1.1378 + 1.1379 + let engine = makeRotaryEngine(); 1.1380 + engine.lastSync = 123; // needs to be non-zero so that tracker is queried 1.1381 + engine._store.items = {flying: "LNER Class A3 4472", 1.1382 + scotsman: "Flying Scotsman", 1.1383 + peppercorn: "Peppercorn Class"}; 1.1384 + // Mark these records as changed 1.1385 + const FLYING_CHANGED = 12345; 1.1386 + const SCOTSMAN_CHANGED = 23456; 1.1387 + const PEPPERCORN_CHANGED = 34567; 1.1388 + engine._tracker.addChangedID('flying', FLYING_CHANGED); 1.1389 + engine._tracker.addChangedID('scotsman', SCOTSMAN_CHANGED); 1.1390 + engine._tracker.addChangedID('peppercorn', PEPPERCORN_CHANGED); 1.1391 + 1.1392 + let meta_global = Service.recordManager.set(engine.metaURL, 1.1393 + new WBORecord(engine.metaURL)); 1.1394 + meta_global.payload.engines = {rotary: {version: engine.version, 1.1395 + syncID: engine.syncID}}; 1.1396 + 1.1397 + try { 1.1398 + 1.1399 + // Confirm initial environment 1.1400 + do_check_eq(engine.lastSyncLocal, 0); 1.1401 + do_check_eq(collection.payload("flying"), undefined); 1.1402 + do_check_eq(engine._tracker.changedIDs['flying'], FLYING_CHANGED); 1.1403 + do_check_eq(engine._tracker.changedIDs['scotsman'], SCOTSMAN_CHANGED); 1.1404 + do_check_eq(engine._tracker.changedIDs['peppercorn'], PEPPERCORN_CHANGED); 1.1405 + 1.1406 + engine.enabled = true; 1.1407 + engine.sync(); 1.1408 + 1.1409 + // Local timestamp has been set. 1.1410 + do_check_true(engine.lastSyncLocal > 0); 1.1411 + 1.1412 + // Ensure the 'flying' record has been uploaded and is no longer marked. 1.1413 + do_check_true(!!collection.payload("flying")); 1.1414 + do_check_eq(engine._tracker.changedIDs['flying'], undefined); 1.1415 + 1.1416 + // The 'scotsman' and 'peppercorn' records couldn't be uploaded so 1.1417 + // they weren't cleared from the tracker. 1.1418 + do_check_eq(engine._tracker.changedIDs['scotsman'], SCOTSMAN_CHANGED); 1.1419 + do_check_eq(engine._tracker.changedIDs['peppercorn'], PEPPERCORN_CHANGED); 1.1420 + 1.1421 + } finally { 1.1422 + cleanAndGo(server); 1.1423 + } 1.1424 +}); 1.1425 + 1.1426 + 1.1427 +add_test(function test_uploadOutgoing_MAX_UPLOAD_RECORDS() { 1.1428 + _("SyncEngine._uploadOutgoing uploads in batches of MAX_UPLOAD_RECORDS"); 1.1429 + 1.1430 + Service.identity.username = "foo"; 1.1431 + let collection = new ServerCollection(); 1.1432 + 1.1433 + // Let's count how many times the client posts to the server 1.1434 + var noOfUploads = 0; 1.1435 + collection.post = (function(orig) { 1.1436 + return function() { 1.1437 + noOfUploads++; 1.1438 + return orig.apply(this, arguments); 1.1439 + }; 1.1440 + }(collection.post)); 1.1441 + 1.1442 + // Create a bunch of records (and server side handlers) 1.1443 + let engine = makeRotaryEngine(); 1.1444 + for (var i = 0; i < 234; i++) { 1.1445 + let id = 'record-no-' + i; 1.1446 + engine._store.items[id] = "Record No. " + i; 1.1447 + engine._tracker.addChangedID(id, 0); 1.1448 + collection.insert(id); 1.1449 + } 1.1450 + 1.1451 + let meta_global = Service.recordManager.set(engine.metaURL, 1.1452 + new WBORecord(engine.metaURL)); 1.1453 + meta_global.payload.engines = {rotary: {version: engine.version, 1.1454 + syncID: engine.syncID}}; 1.1455 + 1.1456 + let server = sync_httpd_setup({ 1.1457 + "/1.1/foo/storage/rotary": collection.handler() 1.1458 + }); 1.1459 + 1.1460 + let syncTesting = new SyncTestingInfrastructure(server); 1.1461 + 1.1462 + try { 1.1463 + 1.1464 + // Confirm initial environment. 1.1465 + do_check_eq(noOfUploads, 0); 1.1466 + 1.1467 + engine._syncStartup(); 1.1468 + engine._uploadOutgoing(); 1.1469 + 1.1470 + // Ensure all records have been uploaded. 1.1471 + for (i = 0; i < 234; i++) { 1.1472 + do_check_true(!!collection.payload('record-no-' + i)); 1.1473 + } 1.1474 + 1.1475 + // Ensure that the uploads were performed in batches of MAX_UPLOAD_RECORDS. 1.1476 + do_check_eq(noOfUploads, Math.ceil(234/MAX_UPLOAD_RECORDS)); 1.1477 + 1.1478 + } finally { 1.1479 + cleanAndGo(server); 1.1480 + } 1.1481 +}); 1.1482 + 1.1483 + 1.1484 +add_test(function test_syncFinish_noDelete() { 1.1485 + _("SyncEngine._syncFinish resets tracker's score"); 1.1486 + 1.1487 + let server = httpd_setup({}); 1.1488 + 1.1489 + let syncTesting = new SyncTestingInfrastructure(server); 1.1490 + let engine = makeRotaryEngine(); 1.1491 + engine._delete = {}; // Nothing to delete 1.1492 + engine._tracker.score = 100; 1.1493 + 1.1494 + // _syncFinish() will reset the engine's score. 1.1495 + engine._syncFinish(); 1.1496 + do_check_eq(engine.score, 0); 1.1497 + server.stop(run_next_test); 1.1498 +}); 1.1499 + 1.1500 + 1.1501 +add_test(function test_syncFinish_deleteByIds() { 1.1502 + _("SyncEngine._syncFinish deletes server records slated for deletion (list of record IDs)."); 1.1503 + 1.1504 + Service.identity.username = "foo"; 1.1505 + let collection = new ServerCollection(); 1.1506 + collection._wbos.flying = new ServerWBO( 1.1507 + 'flying', encryptPayload({id: 'flying', 1.1508 + denomination: "LNER Class A3 4472"})); 1.1509 + collection._wbos.scotsman = new ServerWBO( 1.1510 + 'scotsman', encryptPayload({id: 'scotsman', 1.1511 + denomination: "Flying Scotsman"})); 1.1512 + collection._wbos.rekolok = new ServerWBO( 1.1513 + 'rekolok', encryptPayload({id: 'rekolok', 1.1514 + denomination: "Rekonstruktionslokomotive"})); 1.1515 + 1.1516 + let server = httpd_setup({ 1.1517 + "/1.1/foo/storage/rotary": collection.handler() 1.1518 + }); 1.1519 + let syncTesting = new SyncTestingInfrastructure(server); 1.1520 + 1.1521 + let engine = makeRotaryEngine(); 1.1522 + try { 1.1523 + engine._delete = {ids: ['flying', 'rekolok']}; 1.1524 + engine._syncFinish(); 1.1525 + 1.1526 + // The 'flying' and 'rekolok' records were deleted while the 1.1527 + // 'scotsman' one wasn't. 1.1528 + do_check_eq(collection.payload("flying"), undefined); 1.1529 + do_check_true(!!collection.payload("scotsman")); 1.1530 + do_check_eq(collection.payload("rekolok"), undefined); 1.1531 + 1.1532 + // The deletion todo list has been reset. 1.1533 + do_check_eq(engine._delete.ids, undefined); 1.1534 + 1.1535 + } finally { 1.1536 + cleanAndGo(server); 1.1537 + } 1.1538 +}); 1.1539 + 1.1540 + 1.1541 +add_test(function test_syncFinish_deleteLotsInBatches() { 1.1542 + _("SyncEngine._syncFinish deletes server records in batches of 100 (list of record IDs)."); 1.1543 + 1.1544 + Service.identity.username = "foo"; 1.1545 + let collection = new ServerCollection(); 1.1546 + 1.1547 + // Let's count how many times the client does a DELETE request to the server 1.1548 + var noOfUploads = 0; 1.1549 + collection.delete = (function(orig) { 1.1550 + return function() { 1.1551 + noOfUploads++; 1.1552 + return orig.apply(this, arguments); 1.1553 + }; 1.1554 + }(collection.delete)); 1.1555 + 1.1556 + // Create a bunch of records on the server 1.1557 + let now = Date.now(); 1.1558 + for (var i = 0; i < 234; i++) { 1.1559 + let id = 'record-no-' + i; 1.1560 + let payload = encryptPayload({id: id, denomination: "Record No. " + i}); 1.1561 + let wbo = new ServerWBO(id, payload); 1.1562 + wbo.modified = now / 1000 - 60 * (i + 110); 1.1563 + collection.insertWBO(wbo); 1.1564 + } 1.1565 + 1.1566 + let server = httpd_setup({ 1.1567 + "/1.1/foo/storage/rotary": collection.handler() 1.1568 + }); 1.1569 + 1.1570 + let syncTesting = new SyncTestingInfrastructure(server); 1.1571 + 1.1572 + let engine = makeRotaryEngine(); 1.1573 + try { 1.1574 + 1.1575 + // Confirm initial environment 1.1576 + do_check_eq(noOfUploads, 0); 1.1577 + 1.1578 + // Declare what we want to have deleted: all records no. 100 and 1.1579 + // up and all records that are less than 200 mins old (which are 1.1580 + // records 0 thru 90). 1.1581 + engine._delete = {ids: [], 1.1582 + newer: now / 1000 - 60 * 200.5}; 1.1583 + for (i = 100; i < 234; i++) { 1.1584 + engine._delete.ids.push('record-no-' + i); 1.1585 + } 1.1586 + 1.1587 + engine._syncFinish(); 1.1588 + 1.1589 + // Ensure that the appropriate server data has been wiped while 1.1590 + // preserving records 90 thru 200. 1.1591 + for (i = 0; i < 234; i++) { 1.1592 + let id = 'record-no-' + i; 1.1593 + if (i <= 90 || i >= 100) { 1.1594 + do_check_eq(collection.payload(id), undefined); 1.1595 + } else { 1.1596 + do_check_true(!!collection.payload(id)); 1.1597 + } 1.1598 + } 1.1599 + 1.1600 + // The deletion was done in batches 1.1601 + do_check_eq(noOfUploads, 2 + 1); 1.1602 + 1.1603 + // The deletion todo list has been reset. 1.1604 + do_check_eq(engine._delete.ids, undefined); 1.1605 + 1.1606 + } finally { 1.1607 + cleanAndGo(server); 1.1608 + } 1.1609 +}); 1.1610 + 1.1611 + 1.1612 +add_test(function test_sync_partialUpload() { 1.1613 + _("SyncEngine.sync() keeps changedIDs that couldn't be uploaded."); 1.1614 + 1.1615 + Service.identity.username = "foo"; 1.1616 + 1.1617 + let collection = new ServerCollection(); 1.1618 + let server = sync_httpd_setup({ 1.1619 + "/1.1/foo/storage/rotary": collection.handler() 1.1620 + }); 1.1621 + let syncTesting = new SyncTestingInfrastructure(server); 1.1622 + generateNewKeys(Service.collectionKeys); 1.1623 + 1.1624 + let engine = makeRotaryEngine(); 1.1625 + engine.lastSync = 123; // needs to be non-zero so that tracker is queried 1.1626 + engine.lastSyncLocal = 456; 1.1627 + 1.1628 + // Let the third upload fail completely 1.1629 + var noOfUploads = 0; 1.1630 + collection.post = (function(orig) { 1.1631 + return function() { 1.1632 + if (noOfUploads == 2) 1.1633 + throw "FAIL!"; 1.1634 + noOfUploads++; 1.1635 + return orig.apply(this, arguments); 1.1636 + }; 1.1637 + }(collection.post)); 1.1638 + 1.1639 + // Create a bunch of records (and server side handlers) 1.1640 + for (let i = 0; i < 234; i++) { 1.1641 + let id = 'record-no-' + i; 1.1642 + engine._store.items[id] = "Record No. " + i; 1.1643 + engine._tracker.addChangedID(id, i); 1.1644 + // Let two items in the first upload batch fail. 1.1645 + if ((i != 23) && (i != 42)) { 1.1646 + collection.insert(id); 1.1647 + } 1.1648 + } 1.1649 + 1.1650 + let meta_global = Service.recordManager.set(engine.metaURL, 1.1651 + new WBORecord(engine.metaURL)); 1.1652 + meta_global.payload.engines = {rotary: {version: engine.version, 1.1653 + syncID: engine.syncID}}; 1.1654 + 1.1655 + try { 1.1656 + 1.1657 + engine.enabled = true; 1.1658 + let error; 1.1659 + try { 1.1660 + engine.sync(); 1.1661 + } catch (ex) { 1.1662 + error = ex; 1.1663 + } 1.1664 + do_check_true(!!error); 1.1665 + 1.1666 + // The timestamp has been updated. 1.1667 + do_check_true(engine.lastSyncLocal > 456); 1.1668 + 1.1669 + for (let i = 0; i < 234; i++) { 1.1670 + let id = 'record-no-' + i; 1.1671 + // Ensure failed records are back in the tracker: 1.1672 + // * records no. 23 and 42 were rejected by the server, 1.1673 + // * records no. 200 and higher couldn't be uploaded because we failed 1.1674 + // hard on the 3rd upload. 1.1675 + if ((i == 23) || (i == 42) || (i >= 200)) 1.1676 + do_check_eq(engine._tracker.changedIDs[id], i); 1.1677 + else 1.1678 + do_check_false(id in engine._tracker.changedIDs); 1.1679 + } 1.1680 + 1.1681 + } finally { 1.1682 + cleanAndGo(server); 1.1683 + } 1.1684 +}); 1.1685 + 1.1686 +add_test(function test_canDecrypt_noCryptoKeys() { 1.1687 + _("SyncEngine.canDecrypt returns false if the engine fails to decrypt items on the server, e.g. due to a missing crypto key collection."); 1.1688 + Service.identity.username = "foo"; 1.1689 + 1.1690 + // Wipe collection keys so we can test the desired scenario. 1.1691 + Service.collectionKeys.clear(); 1.1692 + 1.1693 + let collection = new ServerCollection(); 1.1694 + collection._wbos.flying = new ServerWBO( 1.1695 + 'flying', encryptPayload({id: 'flying', 1.1696 + denomination: "LNER Class A3 4472"})); 1.1697 + 1.1698 + let server = sync_httpd_setup({ 1.1699 + "/1.1/foo/storage/rotary": collection.handler() 1.1700 + }); 1.1701 + 1.1702 + let syncTesting = new SyncTestingInfrastructure(server); 1.1703 + let engine = makeRotaryEngine(); 1.1704 + try { 1.1705 + 1.1706 + do_check_false(engine.canDecrypt()); 1.1707 + 1.1708 + } finally { 1.1709 + cleanAndGo(server); 1.1710 + } 1.1711 +}); 1.1712 + 1.1713 +add_test(function test_canDecrypt_true() { 1.1714 + _("SyncEngine.canDecrypt returns true if the engine can decrypt the items on the server."); 1.1715 + Service.identity.username = "foo"; 1.1716 + 1.1717 + generateNewKeys(Service.collectionKeys); 1.1718 + 1.1719 + let collection = new ServerCollection(); 1.1720 + collection._wbos.flying = new ServerWBO( 1.1721 + 'flying', encryptPayload({id: 'flying', 1.1722 + denomination: "LNER Class A3 4472"})); 1.1723 + 1.1724 + let server = sync_httpd_setup({ 1.1725 + "/1.1/foo/storage/rotary": collection.handler() 1.1726 + }); 1.1727 + 1.1728 + let syncTesting = new SyncTestingInfrastructure(server); 1.1729 + let engine = makeRotaryEngine(); 1.1730 + try { 1.1731 + 1.1732 + do_check_true(engine.canDecrypt()); 1.1733 + 1.1734 + } finally { 1.1735 + cleanAndGo(server); 1.1736 + } 1.1737 + 1.1738 +}); 1.1739 + 1.1740 +add_test(function test_syncapplied_observer() { 1.1741 + Service.identity.username = "foo"; 1.1742 + 1.1743 + const NUMBER_OF_RECORDS = 10; 1.1744 + 1.1745 + let engine = makeRotaryEngine(); 1.1746 + 1.1747 + // Create a batch of server side records. 1.1748 + let collection = new ServerCollection(); 1.1749 + for (var i = 0; i < NUMBER_OF_RECORDS; i++) { 1.1750 + let id = 'record-no-' + i; 1.1751 + let payload = encryptPayload({id: id, denomination: "Record No. " + id}); 1.1752 + collection.insert(id, payload); 1.1753 + } 1.1754 + 1.1755 + let server = httpd_setup({ 1.1756 + "/1.1/foo/storage/rotary": collection.handler() 1.1757 + }); 1.1758 + 1.1759 + let syncTesting = new SyncTestingInfrastructure(server); 1.1760 + 1.1761 + let meta_global = Service.recordManager.set(engine.metaURL, 1.1762 + new WBORecord(engine.metaURL)); 1.1763 + meta_global.payload.engines = {rotary: {version: engine.version, 1.1764 + syncID: engine.syncID}}; 1.1765 + 1.1766 + let numApplyCalls = 0; 1.1767 + let engine_name; 1.1768 + let count; 1.1769 + function onApplied(subject, data) { 1.1770 + numApplyCalls++; 1.1771 + engine_name = data; 1.1772 + count = subject; 1.1773 + } 1.1774 + 1.1775 + Svc.Obs.add("weave:engine:sync:applied", onApplied); 1.1776 + 1.1777 + try { 1.1778 + Service.scheduler.hasIncomingItems = false; 1.1779 + 1.1780 + // Do sync. 1.1781 + engine._syncStartup(); 1.1782 + engine._processIncoming(); 1.1783 + 1.1784 + do_check_attribute_count(engine._store.items, 10); 1.1785 + 1.1786 + do_check_eq(numApplyCalls, 1); 1.1787 + do_check_eq(engine_name, "rotary"); 1.1788 + do_check_eq(count.applied, 10); 1.1789 + 1.1790 + do_check_true(Service.scheduler.hasIncomingItems); 1.1791 + } finally { 1.1792 + cleanAndGo(server); 1.1793 + Service.scheduler.hasIncomingItems = false; 1.1794 + Svc.Obs.remove("weave:engine:sync:applied", onApplied); 1.1795 + } 1.1796 +});