Merge the last green changeset of mozilla-inbound to mozilla-central
authorEhsan Akhgari <ehsan@mozilla.com>
Fri, 09 Sep 2011 10:05:52 -0400
changeset 76825 694520af9b1847cc9c70e5deac135d419407eb53
parent 76824 7a8399ef753513fd59f0753d08fc2ef5d1405b1c (current diff)
parent 76771 164976bffd31fdf47f61923b5d25aa48e245da5b (diff)
child 76826 d078623f7875c1b77e0d05c7d541cc23a4b239a7
child 76837 898b0317fc01d5c1849ce3326c7c4bb09cf4c89a
push id3
push userfelipc@gmail.com
push dateFri, 30 Sep 2011 20:09:13 +0000
milestone9.0a1
Merge the last green changeset of mozilla-inbound to mozilla-central
--- a/browser/base/content/syncGenericChange.js
+++ b/browser/base/content/syncGenericChange.js
@@ -192,16 +192,17 @@ let Change = {
 
   doChangePassphrase: function Change_doChangePassphrase() {
     let pp = Weave.Utils.normalizePassphrase(this._passphraseBox.value);
     if (this._updatingPassphrase) {
       Weave.Service.passphrase = pp;
       if (Weave.Service.login()) {
         this._updateStatus("change.recoverykey.success", "success");
         Weave.Service.persistLogin();
+        Weave.SyncScheduler.delayedAutoConnect(0);
       }
       else {
         this._updateStatus("new.passphrase.status.incorrect", "error");
       }
     }
     else {
       this._updateStatus("change.recoverykey.label", "active");
 
--- a/browser/base/content/syncSetup.js
+++ b/browser/base/content/syncSetup.js
@@ -108,16 +108,22 @@ var gSyncSetup = {
           Weave.Svc.Obs.add(topic, self[func], self);
         else
           Weave.Svc.Obs.remove(topic, self[func], self);
       });
     };
     addRem(true);
     window.addEventListener("unload", function() addRem(false), false);
 
+    window.setTimeout(function () {
+      // Force Service to be loaded so that engines are registered.
+      // See Bug 670082.
+      Weave.Service;
+    }, 0);
+
     this.captchaBrowser = document.getElementById("captcha");
     this.wizard = document.getElementById("accountSetup");
 
     if (window.arguments && window.arguments[0] == true) {
       // we're resetting sync
       this._resettingSync = true;
       this.wizard.pageIndex = OPTIONS_PAGE;
     }
--- a/browser/branding/official/pref/firefox-branding.js
+++ b/browser/branding/official/pref/firefox-branding.js
@@ -1,9 +1,10 @@
-pref("startup.homepage_override_url","http://www.mozilla.com/%LOCALE%/%APP%/%VERSION%/whatsnew/");
+// Fight update fatigue by suppressing whatsnew tab opening after update (bug 685727)
+pref("startup.homepage_override_url","");
 pref("startup.homepage_welcome_url","http://www.mozilla.com/%LOCALE%/%APP%/%VERSION%/firstrun/");
 // Interval: Time between checks for a new version (in seconds)
 // nightly=6 hours, official=24 hours
 pref("app.update.interval", 86400);
 // The time interval between the downloading of mar file chunks in the
 // background (in seconds)
 pref("app.update.download.backgroundInterval", 600);
 // URL user can browse to manually if for some reason all update installation
new file mode 100644
--- /dev/null
+++ b/content/html/content/crashtests/682058.xhtml
@@ -0,0 +1,11 @@
+<?xml version="1.0"?>
+<html xmlns="http://www.w3.org/1999/xhtml">
+  <body onload="test()">
+    <script>
+      function test() {
+        document.body.innerHTML = "Foobar";
+      }
+      document.addEventListener("DOMNodeRemoved", function() {});
+    </script>
+  </body>
+</html>
--- a/content/html/content/crashtests/crashtests.list
+++ b/content/html/content/crashtests/crashtests.list
@@ -24,9 +24,10 @@ load 604807.html
 load 605264.html
 load 606430-1.html
 load 602117.html
 load 613027.html
 load 614279.html
 load 614988-1.html
 load 620078-1.html
 load 620078-2.html
+load 682058.xhtml
 load 682460.html
--- a/content/html/content/src/nsGenericHTMLElement.cpp
+++ b/content/html/content/src/nsGenericHTMLElement.cpp
@@ -780,16 +780,21 @@ nsGenericHTMLElement::SetInnerHTML(const
     // HTML5 parser has notified, but not fired mutation events.
     FireMutationEventsForDirectParsing(doc, this, oldChildCount);
   } else {
     rv = nsContentUtils::CreateContextualFragment(this, aInnerHTML,
                                                   PR_TRUE,
                                                   getter_AddRefs(df));
     nsCOMPtr<nsINode> fragment = do_QueryInterface(df);
     if (NS_SUCCEEDED(rv)) {
+      // Suppress assertion about node removal mutation events that can't have
+      // listeners anyway, because no one has had the chance to register mutation
+      // listeners on the fragment that comes from the parser.
+      nsAutoScriptBlockerSuppressNodeRemoved scriptBlocker;
+
       static_cast<nsINode*>(this)->AppendChild(fragment, &rv);
     }
   }
 
   return rv;
 }
 
 enum nsAdjacentPosition {
--- a/services/sync/modules/policies.js
+++ b/services/sync/modules/policies.js
@@ -102,18 +102,20 @@ let SyncScheduler = {
       Svc.Idle.addIdleObserver(this, Svc.Prefs.get("scheduler.idleTime"));
     }
 
   },
 
   observe: function observe(subject, topic, data) {
     switch(topic) {
       case "weave:engine:score:updated":
-        Utils.namedTimer(this.calculateScore, SCORE_UPDATE_DELAY, this,
-                         "_scoreTimer");
+        if (Status.login == LOGIN_SUCCEEDED) {
+          Utils.namedTimer(this.calculateScore, SCORE_UPDATE_DELAY, this,
+                           "_scoreTimer");
+        }
         break;
       case "network:offline-status-changed":
         // Whether online or offline, we'll reschedule syncs
         this._log.trace("Network offline status change: " + data);
         this.checkSyncStatus();
         break;
       case "weave:service:sync:start":
         // Clear out any potentially pending syncs now that we're syncing
@@ -488,59 +490,63 @@ let ErrorHandler = {
 
         Status.engines = [engine_name, exception.failureCode || ENGINE_UNKNOWN_FAIL];
         this._log.debug(engine_name + " failed: " + Utils.exceptionStr(exception));
         break;
       case "weave:service:login:error":
         if (this.shouldReportError()) {
           this.resetFileLog(Svc.Prefs.get("log.appender.file.logOnError"),
                             LOG_PREFIX_ERROR);
-          Svc.Obs.notify("weave:ui:login:error");
+          this.notifyOnNextTick("weave:ui:login:error");
         } else {
-          Svc.Obs.notify("weave:ui:clear-error");
+          this.notifyOnNextTick("weave:ui:clear-error");
         }
 
         this.dontIgnoreErrors = false;
         break;
       case "weave:service:sync:error":
         if (Status.sync == CREDENTIALS_CHANGED) {
           Weave.Service.logout();
         }
 
         if (this.shouldReportError()) {
           this.resetFileLog(Svc.Prefs.get("log.appender.file.logOnError"),
                             LOG_PREFIX_ERROR);
-          Svc.Obs.notify("weave:ui:sync:error");
+          this.notifyOnNextTick("weave:ui:sync:error");
         } else {
-          Svc.Obs.notify("weave:ui:sync:finish");
+          this.notifyOnNextTick("weave:ui:sync:finish");
         }
 
         this.dontIgnoreErrors = false;
         break;
       case "weave:service:sync:finish":
         if (Status.service == SYNC_FAILED_PARTIAL) {
           this._log.debug("Some engines did not sync correctly.");
           this.resetFileLog(Svc.Prefs.get("log.appender.file.logOnError"),
                             LOG_PREFIX_ERROR);
 
           if (this.shouldReportError()) {
             this.dontIgnoreErrors = false;
-            Svc.Obs.notify("weave:ui:sync:error");
+            this.notifyOnNextTick("weave:ui:sync:error");
             break;
           }
         } else {
           this.resetFileLog(Svc.Prefs.get("log.appender.file.logOnSuccess"),
                             LOG_PREFIX_SUCCESS);
         }
         this.dontIgnoreErrors = false;
-        Svc.Obs.notify("weave:ui:sync:finish");
+        this.notifyOnNextTick("weave:ui:sync:finish");
         break;
     }
   },
 
+  notifyOnNextTick: function notifyOnNextTick(topic) {
+    Utils.nextTick(function() Svc.Obs.notify(topic));
+  },
+
   /**
    * Trigger a sync and don't muffle any errors, particularly network errors.
    */
   syncAndReportErrors: function syncAndReportErrors() {
     this._log.debug("Beginning user-triggered sync.");
 
     this.dontIgnoreErrors = true;
     Utils.nextTick(Weave.Service.sync, Weave.Service);
@@ -618,17 +624,17 @@ let ErrorHandler = {
 
     let lastSync = Svc.Prefs.get("lastSync");
     if (lastSync && ((Date.now() - Date.parse(lastSync)) >
         Svc.Prefs.get("errorhandler.networkFailureReportTimeout") * 1000)) {
       Status.sync = PROLONGED_SYNC_FAILURE;
       return true;
     }
 
-    return (Status.sync != SERVER_MAINTENANCE &&
+    return ([Status.login, Status.sync].indexOf(SERVER_MAINTENANCE) == -1 &&
             [Status.login, Status.sync].indexOf(LOGIN_FAILED_NETWORK_ERROR) == -1);
   },
 
   /**
    * Handle HTTP response results or exceptions and set the appropriate
    * Status.* bits.
    */
   checkServerError: function checkServerError(resp) {
@@ -645,29 +651,37 @@ let ErrorHandler = {
         break;
 
       case 500:
       case 502:
       case 503:
       case 504:
         Status.enforceBackoff = true;
         if (resp.status == 503 && resp.headers["retry-after"]) {
-          Status.sync = SERVER_MAINTENANCE;
+          if (Weave.Service.isLoggedIn) {
+            Status.sync = SERVER_MAINTENANCE;
+          } else {
+            Status.login = SERVER_MAINTENANCE;
+          }
           Svc.Obs.notify("weave:service:backoff:interval",
                          parseInt(resp.headers["retry-after"], 10));
         }
         break;
     }
 
     switch (resp.result) {
       case Cr.NS_ERROR_UNKNOWN_HOST:
       case Cr.NS_ERROR_CONNECTION_REFUSED:
       case Cr.NS_ERROR_NET_TIMEOUT:
       case Cr.NS_ERROR_NET_RESET:
       case Cr.NS_ERROR_NET_INTERRUPT:
       case Cr.NS_ERROR_PROXY_CONNECTION_REFUSED:
         // The constant says it's about login, but in fact it just
         // indicates general network error.
-        Status.sync = LOGIN_FAILED_NETWORK_ERROR;
+        if (Weave.Service.isLoggedIn) {
+          Status.sync = LOGIN_FAILED_NETWORK_ERROR;
+        } else {
+          Status.login = LOGIN_FAILED_NETWORK_ERROR;
+        }
         break;
     }
   },
 };
--- a/services/sync/modules/service.js
+++ b/services/sync/modules/service.js
@@ -522,22 +522,24 @@ WeaveSvc.prototype = {
           this._log.debug("Using serverURL as data cluster (multi-cluster support disabled)");
           return this.serverURL;
         case 0:
         case 200:
           if (node == "null")
             node = null;
           return node;
         default:
+          ErrorHandler.checkServerError(node);
           fail = "Unexpected response code: " + node.status;
           break;
       }
     } catch (e) {
       this._log.debug("Network error on findCluster");
       Status.login = LOGIN_FAILED_NETWORK_ERROR;
+      ErrorHandler.checkServerError(e);
       fail = e;
     }
     throw fail;
   },
 
   // gets cluster from central LDAP server and sets this.clusterURL
   _setCluster: function _setCluster() {
     // Make sure we didn't get some unexpected response for the cluster
@@ -616,16 +618,17 @@ WeaveSvc.prototype = {
     try {
       if (!infoResponse)
         infoResponse = this._fetchInfo();    // Will throw an exception on failure.
 
       // This only applies when the server is already at version 4.
       if (infoResponse.status != 200) {
         this._log.warn("info/collections returned non-200 response. Failing key fetch.");
         Status.login = LOGIN_FAILED_SERVER_ERROR;
+        ErrorHandler.checkServerError(infoResponse);
         return false;
       }
 
       let infoCollections = infoResponse.obj;
 
       this._log.info("Testing info/collections: " + JSON.stringify(infoCollections));
 
       if (CollectionKeys.updateNeeded(infoCollections)) {
@@ -648,18 +651,19 @@ WeaveSvc.prototype = {
             else if (cryptoResp.status == 404) {
               // On failure, ask CollectionKeys to generate new keys and upload them.
               // Fall through to the behavior below.
               this._log.warn("Got 404 for crypto/keys, but 'crypto' in info/collections. Regenerating.");
               cryptoKeys = null;
             }
             else {
               // Some other problem.
+              Status.login = LOGIN_FAILED_SERVER_ERROR;
+              ErrorHandler.checkServerError(cryptoResp);
               this._log.warn("Got status " + cryptoResp.status + " fetching crypto keys.");
-              Status.login = LOGIN_FAILED_SERVER_ERROR;
               return false;
             }
           }
           catch (ex) {
             this._log.warn("Got exception \"" + ex + "\" fetching cryptoKeys.");
             // TODO: Um, what exceptions might we get here? Should we re-throw any?
 
             // One kind of exception: HMAC failure.
@@ -793,25 +797,26 @@ WeaveSvc.prototype = {
               return this.verifyLogin();
 
             // We must have the right cluster, but the server doesn't expect us
             Status.login = LOGIN_FAILED_LOGIN_REJECTED;
             return false;
 
           default:
             // Server didn't respond with something that we expected
+            Status.login = LOGIN_FAILED_SERVER_ERROR;
             ErrorHandler.checkServerError(test);
-            Status.login = LOGIN_FAILED_SERVER_ERROR;
             return false;
         }
       }
       catch (ex) {
         // Must have failed on some network issue
         this._log.debug("verifyLogin failed: " + Utils.exceptionStr(ex));
         Status.login = LOGIN_FAILED_NETWORK_ERROR;
+        ErrorHandler.checkServerError(ex);
         return false;
       }
     })(),
 
   generateNewSymmetricKeys:
   function WeaveSvc_generateNewSymmetricKeys() {
     this._log.info("Generating new keys WBO...");
     let wbo = CollectionKeys.generateNewKeysWBO();
@@ -1131,18 +1136,18 @@ WeaveSvc.prototype = {
     if (!meta || !meta.payload.storageVersion || !meta.payload.syncID ||
         STORAGE_VERSION > parseFloat(remoteVersion)) {
 
       this._log.info("One of: no meta, no meta storageVersion, or no meta syncID. Fresh start needed.");
 
       // abort the server wipe if the GET status was anything other than 404 or 200
       let status = Records.response.status;
       if (status != 200 && status != 404) {
+        Status.sync = METARECORD_DOWNLOAD_FAIL;
         ErrorHandler.checkServerError(Records.response);
-        Status.sync = METARECORD_DOWNLOAD_FAIL;
         this._log.warn("Unknown error while downloading metadata record. " +
                        "Aborting sync.");
         return false;
       }
 
       if (!meta)
         this._log.info("No metadata record, server wipe needed");
       if (meta && !meta.payload.syncID)
--- a/services/sync/tests/tps/mozmill_sanity.js
+++ b/services/sync/tests/tps/mozmill_sanity.js
@@ -30,31 +30,33 @@
  * use your version of this file under the terms of the MPL, indicate your
  * decision by deleting the provisions above and replace them with the notice
  * and other provisions required by the GPL or the LGPL. If you do not delete
  * the provisions above, a recipient may use your version of this file under
  * the terms of any one of the MPL, the GPL or the LGPL.
  *
  * **** END LICENSE BLOCK ***** */
 
-var jum = {}; Components.utils.import('resource://mozmill/modules/jum.js', jum);
+Components.utils.import('resource://tps/sync.jsm');
 
 var setupModule = function(module) {
   controller = mozmill.getBrowserController();
-  jum.assert(true, "SetupModule passes");
+  assert.ok(true, "SetupModule passes");
 }
 
 var setupTest = function(module) {
-  jum.assert(true, "SetupTest passes");
+  assert.ok(true, "SetupTest passes");
 }
 
 var testTestStep = function() {
-  jum.assert(true, "test Passes");
+  assert.ok(true, "test Passes");
   controller.open("http://www.mozilla.org");
+  TPS.SetupSyncAccount();
+  assert.equal(TPS.Sync(SYNC_WIPE_SERVER), 0, "sync succeeded");
 }
 
 var teardownTest = function () {
-  jum.assert(true, "teardownTest passes");
+  assert.ok(true, "teardownTest passes");
 }
 
 var teardownModule = function() {
-  jum.assert(true, "teardownModule passes");
+  assert.ok(true, "teardownModule passes");
 }
--- a/services/sync/tests/tps/test_mozmill_sanity.js
+++ b/services/sync/tests/tps/test_mozmill_sanity.js
@@ -11,15 +11,14 @@ var phases = { "phase1": "profile1",
                "phase2": "profile2" };
 
 /*
  * Test phases
  */
 
 Phase('phase1', [
   [RunMozmillTest, 'mozmill_sanity.js'],
-  [Sync, SYNC_WIPE_SERVER]
 ]);
 
 Phase('phase2', [
   [Sync],
   [RunMozmillTest, 'mozmill_sanity2.js'],
 ]);
--- a/services/sync/tests/unit/test_errorhandler.js
+++ b/services/sync/tests/unit/test_errorhandler.js
@@ -52,16 +52,23 @@ function generateCredentialsChangedFailu
   // the keys with a different Sync Key, without changing the local one.
   let newSyncKeyBundle = new SyncKeyBundle(PWDMGR_PASSPHRASE_REALM, Service.username);
   newSyncKeyBundle.keyStr = "23456234562345623456234562";
   let keys = CollectionKeys.asWBO();
   keys.encrypt(newSyncKeyBundle);
   keys.upload(Service.cryptoKeysURL);
 }
 
+function service_unavailable(request, response) {
+  let body = "Service Unavailable";
+  response.setStatusLine(request.httpVersion, 503, "Service Unavailable");
+  response.setHeader("Retry-After", "42");
+  response.bodyOutputStream.write(body, body.length);
+}
+
 function sync_httpd_setup() {
   let global = new ServerWBO("global", {
     syncID: Service.syncID,
     storageVersion: STORAGE_VERSION,
     engines: {clients: {version: Clients.version,
                         syncID: Clients.syncID}}
   });
   let clientsColl = new ServerCollection({}, true);
@@ -75,31 +82,46 @@ function sync_httpd_setup() {
     "/1.1/johndoe/storage/meta/global": upd("meta", global.handler()),
     "/1.1/johndoe/info/collections": collectionsHelper.handler,
     "/1.1/johndoe/storage/crypto/keys":
       upd("crypto", (new ServerWBO("keys")).handler()),
     "/1.1/johndoe/storage/clients": upd("clients", clientsColl.handler()),
 
     "/1.1/janedoe/storage/meta/global": handler_401,
     "/1.1/janedoe/info/collections": handler_401,
+
+    "/maintenance/1.1/johnsmith/info/collections": service_unavailable,
+
+    "/maintenance/1.1/janesmith/storage/meta/global": service_unavailable,
+    "/maintenance/1.1/janesmith/info/collections": collectionsHelper.handler,
+
+    "/maintenance/1.1/foo/storage/meta/global": upd("meta", global.handler()),
+    "/maintenance/1.1/foo/info/collections": collectionsHelper.handler,
+    "/maintenance/1.1/foo/storage/crypto/keys": service_unavailable,
   });
 }
 
 function setUp() {
   Service.username = "johndoe";
   Service.password = "ilovejane";
   Service.passphrase = "abcdeabcdeabcdeabcdeabcdea";
   Service.clusterURL = "http://localhost:8080/";
 
   generateNewKeys();
   let serverKeys = CollectionKeys.asWBO("crypto", "keys");
   serverKeys.encrypt(Service.syncKeyBundle);
   return serverKeys.upload(Service.cryptoKeysURL).success;
 }
 
+function clean() {
+  Service.startOver();
+  Status.resetSync();
+  Status.resetBackoff();
+}
+
 add_test(function test_401_logout() {
   let server = sync_httpd_setup();
   setUp();
 
   // By calling sync, we ensure we're logged in.
   Service.sync();
   do_check_eq(Status.sync, SYNC_SUCCEEDED);
   do_check_true(Service.isLoggedIn);
@@ -263,45 +285,74 @@ add_test(function test_shouldReportError
 
   // Test network, non-prolonged, sync error reported
   Status.resetSync();
   setLastSync(NON_PROLONGED_ERROR_DURATION);
   ErrorHandler.dontIgnoreErrors = false;
   Status.sync = LOGIN_FAILED_NETWORK_ERROR;
   do_check_false(ErrorHandler.shouldReportError());
 
-  // Test server maintenance errors are not reported
+  // Test server maintenance, sync errors are not reported
   Status.resetSync();
   setLastSync(NON_PROLONGED_ERROR_DURATION);
   ErrorHandler.dontIgnoreErrors = false;
   Status.sync = SERVER_MAINTENANCE;
   do_check_false(ErrorHandler.shouldReportError());
 
-  // Test prolonged server maintenance errors are reported
+  // Test server maintenance, login errors are not reported
+  Status.resetSync();
+  setLastSync(NON_PROLONGED_ERROR_DURATION);
+  ErrorHandler.dontIgnoreErrors = false;
+  Status.login = SERVER_MAINTENANCE;
+  do_check_false(ErrorHandler.shouldReportError());
+
+  // Test prolonged, server maintenance, sync errors are reported
   Status.resetSync();
   setLastSync(PROLONGED_ERROR_DURATION);
   ErrorHandler.dontIgnoreErrors = false;
   Status.sync = SERVER_MAINTENANCE;
   do_check_true(ErrorHandler.shouldReportError());
 
-  // Test dontIgnoreErrors, server maintenance errors are reported
+  // Test prolonged, server maintenance, login errors are reported
+  Status.resetSync();
+  setLastSync(PROLONGED_ERROR_DURATION);
+  ErrorHandler.dontIgnoreErrors = false;
+  Status.login = SERVER_MAINTENANCE;
+  do_check_true(ErrorHandler.shouldReportError());
+
+  // Test dontIgnoreErrors, server maintenance, sync errors are reported
   Status.resetSync();
   setLastSync(NON_PROLONGED_ERROR_DURATION);
   ErrorHandler.dontIgnoreErrors = true;
   Status.sync = SERVER_MAINTENANCE;
   do_check_true(ErrorHandler.shouldReportError());
 
-  // Test dontIgnoreErrors, prolonged, server maintenance
-  // errors are reported
+  // Test dontIgnoreErrors, server maintenance, login errors are reported
+  Status.resetSync();
+  setLastSync(NON_PROLONGED_ERROR_DURATION);
+  ErrorHandler.dontIgnoreErrors = true;
+  Status.login = SERVER_MAINTENANCE;
+  do_check_true(ErrorHandler.shouldReportError());
+
+  // Test dontIgnoreErrors, prolonged, server maintenance,
+  // sync errors are reported
   Status.resetSync();
   setLastSync(PROLONGED_ERROR_DURATION);
   ErrorHandler.dontIgnoreErrors = true;
   Status.sync = SERVER_MAINTENANCE;
   do_check_true(ErrorHandler.shouldReportError());
 
+  // Test dontIgnoreErrors, prolonged, server maintenance,
+  // login errors are reported
+  Status.resetSync();
+  setLastSync(PROLONGED_ERROR_DURATION);
+  ErrorHandler.dontIgnoreErrors = true;
+  Status.login = SERVER_MAINTENANCE;
+  do_check_true(ErrorHandler.shouldReportError());
+
   run_next_test();
 });
 
 add_test(function test_shouldReportError_master_password() {
   _("Test error ignored due to locked master password");
   let server = sync_httpd_setup();
   setUp();
 
@@ -314,32 +365,32 @@ add_test(function test_shouldReportError
   };
 
   setLastSync(NON_PROLONGED_ERROR_DURATION);
   Service.sync();
   do_check_false(ErrorHandler.shouldReportError());
 
   // Clean up.
   Service.verifyLogin = Service._verifyLogin;
-  Service.startOver();
+  clean();
   server.stop(run_next_test);
 });
 
 add_test(function test_login_syncAndReportErrors_non_network_error() {
   // Test non-network errors are reported
   // when calling syncAndReportErrors
   let server = sync_httpd_setup();
   setUp();
   Service.password = "";
 
   Svc.Obs.add("weave:ui:login:error", function onSyncError() {
     Svc.Obs.remove("weave:ui:login:error", onSyncError);
     do_check_eq(Status.login, LOGIN_FAILED_NO_PASSWORD);
 
-    Service.startOver();
+    clean();
     server.stop(run_next_test);
   });
 
   setLastSync(NON_PROLONGED_ERROR_DURATION);
   ErrorHandler.syncAndReportErrors();
 });
 
 add_test(function test_sync_syncAndReportErrors_non_network_error() {
@@ -354,17 +405,17 @@ add_test(function test_sync_syncAndRepor
   do_check_true(Service.isLoggedIn);
 
   generateCredentialsChangedFailure();
 
   Svc.Obs.add("weave:ui:sync:error", function onSyncError() {
     Svc.Obs.remove("weave:ui:sync:error", onSyncError);
     do_check_eq(Status.sync, CREDENTIALS_CHANGED);
 
-    Service.startOver();
+    clean();
     server.stop(run_next_test);
   });
 
   setLastSync(NON_PROLONGED_ERROR_DURATION);
   ErrorHandler.syncAndReportErrors();
 });
 
 add_test(function test_login_syncAndReportErrors_prolonged_non_network_error() {
@@ -373,17 +424,17 @@ add_test(function test_login_syncAndRepo
   let server = sync_httpd_setup();
   setUp();
   Service.password = "";
 
   Svc.Obs.add("weave:ui:login:error", function onSyncError() {
     Svc.Obs.remove("weave:ui:login:error", onSyncError);
     do_check_eq(Status.login, LOGIN_FAILED_NO_PASSWORD);
 
-    Service.startOver();
+    clean();
     server.stop(run_next_test);
   });
 
   setLastSync(PROLONGED_ERROR_DURATION);
   ErrorHandler.syncAndReportErrors();
 });
 
 add_test(function test_sync_syncAndReportErrors_prolonged_non_network_error() {
@@ -398,17 +449,17 @@ add_test(function test_sync_syncAndRepor
   do_check_true(Service.isLoggedIn);
 
   generateCredentialsChangedFailure();
 
   Svc.Obs.add("weave:ui:sync:error", function onSyncError() {
     Svc.Obs.remove("weave:ui:sync:error", onSyncError);
     do_check_eq(Status.sync, CREDENTIALS_CHANGED);
 
-    Service.startOver();
+    clean();
     server.stop(run_next_test);
   });
 
   setLastSync(PROLONGED_ERROR_DURATION);
   ErrorHandler.syncAndReportErrors();
 });
 
 add_test(function test_login_syncAndReportErrors_network_error() {
@@ -417,17 +468,17 @@ add_test(function test_login_syncAndRepo
   Service.password = "ilovejane";
   Service.passphrase = "abcdeabcdeabcdeabcdeabcdea";
   Service.clusterURL = "http://localhost:8080/";
 
   Svc.Obs.add("weave:ui:login:error", function onSyncError() {
     Svc.Obs.remove("weave:ui:login:error", onSyncError);
     do_check_eq(Status.login, LOGIN_FAILED_NETWORK_ERROR);
 
-    Service.startOver();
+    clean();
     run_next_test();
   });
 
   setLastSync(NON_PROLONGED_ERROR_DURATION);
   ErrorHandler.syncAndReportErrors();
 });
 
 
@@ -435,17 +486,17 @@ add_test(function test_sync_syncAndRepor
   // Test network errors are reported when calling syncAndReportErrors.
   Services.io.offline = true;
 
   Svc.Obs.add("weave:ui:sync:error", function onSyncError() {
     Svc.Obs.remove("weave:ui:sync:error", onSyncError);
     do_check_eq(Status.sync, LOGIN_FAILED_NETWORK_ERROR);
 
     Services.io.offline = false;
-    Service.startOver();
+    clean();
     run_next_test();
   });
 
   setLastSync(NON_PROLONGED_ERROR_DURATION);
   ErrorHandler.syncAndReportErrors();
 });
 
 add_test(function test_login_syncAndReportErrors_prolonged_network_error() {
@@ -453,19 +504,19 @@ add_test(function test_login_syncAndRepo
   // when calling syncAndReportErrors.
   Service.username = "johndoe";
   Service.password = "ilovejane";
   Service.passphrase = "abcdeabcdeabcdeabcdeabcdea";
   Service.clusterURL = "http://localhost:8080/";
 
   Svc.Obs.add("weave:ui:login:error", function onSyncError() {
     Svc.Obs.remove("weave:ui:login:error", onSyncError);
-    do_check_eq(Status.sync, LOGIN_FAILED_NETWORK_ERROR);
+    do_check_eq(Status.login, LOGIN_FAILED_NETWORK_ERROR);
 
-    Service.startOver();
+    clean();
     run_next_test();
   });
 
   setLastSync(PROLONGED_ERROR_DURATION);
   ErrorHandler.syncAndReportErrors();
 });
 
 add_test(function test_sync_syncAndReportErrors_prolonged_network_error() {
@@ -473,17 +524,17 @@ add_test(function test_sync_syncAndRepor
   // when calling syncAndReportErrors.
   Services.io.offline = true;
 
   Svc.Obs.add("weave:ui:sync:error", function onSyncError() {
     Svc.Obs.remove("weave:ui:sync:error", onSyncError);
     do_check_eq(Status.sync, LOGIN_FAILED_NETWORK_ERROR);
 
     Services.io.offline = false;
-    Service.startOver();
+    clean();
     run_next_test();
   });
 
   setLastSync(PROLONGED_ERROR_DURATION);
   ErrorHandler.syncAndReportErrors();
 });
 
 add_test(function test_login_prolonged_non_network_error() {
@@ -491,17 +542,17 @@ add_test(function test_login_prolonged_n
   let server = sync_httpd_setup();
   setUp();
   Service.password = "";
 
   Svc.Obs.add("weave:ui:login:error", function onSyncError() {
     Svc.Obs.remove("weave:ui:login:error", onSyncError);
     do_check_eq(Status.sync, PROLONGED_SYNC_FAILURE);
 
-    Service.startOver();
+    clean();
     server.stop(run_next_test);
   });
 
   setLastSync(PROLONGED_ERROR_DURATION);
   Service.sync();
 });
 
 add_test(function test_sync_prolonged_non_network_error() {
@@ -515,17 +566,17 @@ add_test(function test_sync_prolonged_no
   do_check_true(Service.isLoggedIn);
 
   generateCredentialsChangedFailure();
 
   Svc.Obs.add("weave:ui:sync:error", function onSyncError() {
     Svc.Obs.remove("weave:ui:sync:error", onSyncError);
     do_check_eq(Status.sync, PROLONGED_SYNC_FAILURE);
 
-    Service.startOver();
+    clean();
     server.stop(run_next_test);
   });
 
   setLastSync(PROLONGED_ERROR_DURATION);
   Service.sync();
 });
 
 add_test(function test_login_prolonged_network_error() {
@@ -534,34 +585,34 @@ add_test(function test_login_prolonged_n
   Service.password = "ilovejane";
   Service.passphrase = "abcdeabcdeabcdeabcdeabcdea";
   Service.clusterURL = "http://localhost:8080/";
 
   Svc.Obs.add("weave:ui:login:error", function onSyncError() {
     Svc.Obs.remove("weave:ui:login:error", onSyncError);
     do_check_eq(Status.sync, PROLONGED_SYNC_FAILURE);
 
-    Service.startOver();
+    clean();
     run_next_test();
   });
 
   setLastSync(PROLONGED_ERROR_DURATION);
   Service.sync();
 });
 
 add_test(function test_sync_prolonged_network_error() {
   // Test prolonged, network errors are reported
   Services.io.offline = true;
 
   Svc.Obs.add("weave:ui:sync:error", function onSyncError() {
     Svc.Obs.remove("weave:ui:sync:error", onSyncError);
     do_check_eq(Status.sync, PROLONGED_SYNC_FAILURE);
 
     Services.io.offline = false;
-    Service.startOver();
+    clean();
     run_next_test();
   });
 
   setLastSync(PROLONGED_ERROR_DURATION);
   Service.sync();
 });
 
 add_test(function test_login_non_network_error() {
@@ -569,42 +620,41 @@ add_test(function test_login_non_network
   let server = sync_httpd_setup();
   setUp();
   Service.password = "";
 
   Svc.Obs.add("weave:ui:login:error", function onSyncError() {
     Svc.Obs.remove("weave:ui:login:error", onSyncError);
     do_check_eq(Status.login, LOGIN_FAILED_NO_PASSWORD);
 
-    Service.startOver();
+    clean();
     server.stop(run_next_test);
   });
 
   setLastSync(NON_PROLONGED_ERROR_DURATION);
   Service.sync();
 });
 
-
 add_test(function test_sync_non_network_error() {
   // Test non-network errors are reported
   let server = sync_httpd_setup();
   setUp();
 
   // By calling sync, we ensure we're logged in.
   Service.sync();
   do_check_eq(Status.sync, SYNC_SUCCEEDED);
   do_check_true(Service.isLoggedIn);
 
   generateCredentialsChangedFailure();
 
   Svc.Obs.add("weave:ui:sync:error", function onSyncError() {
     Svc.Obs.remove("weave:ui:sync:error", onSyncError);
     do_check_eq(Status.sync, CREDENTIALS_CHANGED);
 
-    Service.startOver();
+    clean();
     server.stop(run_next_test);
   });
 
   setLastSync(NON_PROLONGED_ERROR_DURATION);
   Service.sync();
 });
 
 add_test(function test_login_network_error() {
@@ -614,37 +664,35 @@ add_test(function test_login_network_err
   Service.clusterURL = "http://localhost:8080/";
 
   // Test network errors are not reported.
   Svc.Obs.add("weave:ui:clear-error", function onClearError() {
     Svc.Obs.remove("weave:ui:clear-error", onClearError);
 
     do_check_eq(Status.login, LOGIN_FAILED_NETWORK_ERROR);
 
-    Service.startOver();
-    Status.resetSync();
     Services.io.offline = false;
+    clean();
     run_next_test();
   });
 
   setLastSync(NON_PROLONGED_ERROR_DURATION);
   Service.sync();
 });
 
 add_test(function test_sync_network_error() {
   // Test network errors are not reported.
   Services.io.offline = true;
 
   Svc.Obs.add("weave:ui:sync:finish", function onUIUpdate() {
     Svc.Obs.remove("weave:ui:sync:finish", onUIUpdate);
     do_check_eq(Status.sync, LOGIN_FAILED_NETWORK_ERROR);
 
-    Service.startOver();
     Services.io.offline = false;
-    Status.resetSync();
+    clean();
     run_next_test();
   });
 
   setLastSync(NON_PROLONGED_ERROR_DURATION);
   Service.sync();
 });
 
 add_test(function test_sync_server_maintenance_error() {
@@ -653,34 +701,155 @@ add_test(function test_sync_server_maint
   setUp();
 
   const BACKOFF = 42;
   let engine = Engines.get("catapult");
   engine.enabled = true;
   engine.exception = {status: 503,
                       headers: {"retry-after": BACKOFF}};
 
-  function onUIUpdate() {
+  function onSyncError() {
     do_throw("Shouldn't get here!");
   }
-  Svc.Obs.add("weave:ui:sync:error", onUIUpdate);
+  Svc.Obs.add("weave:ui:sync:error", onSyncError);
 
   do_check_eq(Status.service, STATUS_OK);
 
-  // Last sync was 2 days ago
-  Svc.Prefs.set("lastSync", (new Date(Date.now() - 172800000)).toString());
+  Svc.Obs.add("weave:ui:sync:finish", function onSyncFinish() {
+    Svc.Obs.remove("weave:ui:sync:finish", onSyncFinish);
+
+    do_check_eq(Status.service, SYNC_FAILED_PARTIAL);
+    do_check_eq(Status.sync, SERVER_MAINTENANCE);
+
+    Svc.Obs.remove("weave:ui:sync:error", onSyncError);
+    clean();
+    server.stop(run_next_test);
+  });
+
+  setLastSync(NON_PROLONGED_ERROR_DURATION);
   Service.sync();
+});
+
+add_test(function test_info_collections_login_server_maintenance_error() {
+  // Test info/collections server maintenance errors are not reported.
+  let server = sync_httpd_setup();
+  setUp();
+
+  Service.clusterURL = "http://localhost:8080/maintenance/";
+
+  Service.username = "johnsmith";
+  let backoffInterval;
+  Svc.Obs.add("weave:service:backoff:interval", function observe(subject, data) {
+    Svc.Obs.remove("weave:service:backoff:interval", observe);
+    backoffInterval = subject;
+  });
+
+  function onUIUpdate() {
+    do_throw("Shouldn't get here!");
+  }
+  Svc.Obs.add("weave:ui:login:error", onUIUpdate);
+
+  do_check_false(Status.enforceBackoff);
+  do_check_eq(Status.service, STATUS_OK);
+
+  Svc.Obs.add("weave:ui:clear-error", function onLoginFinish() {
+    Svc.Obs.remove("weave:ui:clear-error", onLoginFinish);
+
+    do_check_true(Status.enforceBackoff);
+    do_check_eq(backoffInterval, 42);
+    do_check_eq(Status.service, LOGIN_FAILED);
+    do_check_eq(Status.login, SERVER_MAINTENANCE);
+
+    Svc.Obs.remove("weave:ui:login:error", onUIUpdate);
+    clean();
+    server.stop(run_next_test);
+  });
+
+  setLastSync(NON_PROLONGED_ERROR_DURATION);
+  Service.sync();
+});
+
+add_test(function test_meta_global_login_server_maintenance_error() {
+  // Test meta/global server maintenance errors are not reported.
+  let server = sync_httpd_setup();
+  setUp();
+
+  Service.clusterURL = "http://localhost:8080/maintenance/";
 
-  do_check_eq(Status.service, SYNC_FAILED_PARTIAL);
-  do_check_eq(Status.sync, SERVER_MAINTENANCE);
+  Service.username = "janesmith";
+  let backoffInterval;
+  Svc.Obs.add("weave:service:backoff:interval", function observe(subject, data) {
+    Svc.Obs.remove("weave:service:backoff:interval", observe);
+    backoffInterval = subject;
+  });
+
+  function onUIUpdate() {
+    do_throw("Shouldn't get here!");
+  }
+  Svc.Obs.add("weave:ui:login:error", onUIUpdate);
+
+  do_check_false(Status.enforceBackoff);
+  do_check_eq(Status.service, STATUS_OK);
+
+  Svc.Obs.add("weave:ui:clear-error", function onLoginFinish() {
+    Svc.Obs.remove("weave:ui:clear-error", onLoginFinish);
+
+    do_check_true(Status.enforceBackoff);
+    do_check_eq(backoffInterval, 42);
+    do_check_eq(Status.service, LOGIN_FAILED);
+    do_check_eq(Status.login, SERVER_MAINTENANCE);
+
+    Svc.Obs.remove("weave:ui:login:error", onUIUpdate);
+    clean();
+    server.stop(run_next_test);
+  });
+
+  setLastSync(NON_PROLONGED_ERROR_DURATION);
+  Service.sync();
+});
 
-  Svc.Obs.remove("weave:ui:sync:error", onUIUpdate);
-  Service.startOver();
-  Status.resetSync();
-  server.stop(run_next_test);
+add_test(function test_crypto_keys_login_server_maintenance_error() {
+  // Test crypto/keys server maintenance errors are not reported.
+  let server = sync_httpd_setup();
+  setUp();
+
+  Service.clusterURL = "http://localhost:8080/maintenance/";
+  Service.username = "foo";
+  // Force re-download of keys
+  CollectionKeys.clear();
+
+  let backoffInterval;
+  Svc.Obs.add("weave:service:backoff:interval", function observe(subject, data) {
+    Svc.Obs.remove("weave:service:backoff:interval", observe);
+    backoffInterval = subject;
+  });
+
+  function onUIUpdate() {
+    do_throw("Shouldn't get here!");
+  }
+  Svc.Obs.add("weave:ui:login:error", onUIUpdate);
+
+  do_check_false(Status.enforceBackoff);
+  do_check_eq(Status.service, STATUS_OK);
+
+  Svc.Obs.add("weave:ui:clear-error", function onLoginFinish() {
+    Svc.Obs.remove("weave:ui:clear-error", onLoginFinish);
+
+    do_check_true(Status.enforceBackoff);
+    do_check_eq(backoffInterval, 42);
+    do_check_eq(Status.service, LOGIN_FAILED);
+    do_check_eq(Status.login, SERVER_MAINTENANCE);
+
+    Svc.Obs.remove("weave:ui:login:error", onUIUpdate);
+    clean();
+    server.stop(run_next_test);
+  });
+
+  setLastSync(NON_PROLONGED_ERROR_DURATION);
+  Service.sync();
 });
 
 add_test(function test_sync_prolonged_server_maintenance_error() {
   // Test prolonged server maintenance errors are reported.
   let server = sync_httpd_setup();
   setUp();
 
   const BACKOFF = 42;
@@ -689,77 +858,374 @@ add_test(function test_sync_prolonged_se
   engine.exception = {status: 503,
                       headers: {"retry-after": BACKOFF}};
 
   Svc.Obs.add("weave:ui:sync:error", function onUIUpdate() {
     Svc.Obs.remove("weave:ui:sync:error", onUIUpdate);
     do_check_eq(Status.service, SYNC_FAILED);
     do_check_eq(Status.sync, PROLONGED_SYNC_FAILURE);
 
-    Service.startOver();
-    Status.resetSync();
+    clean();
     server.stop(run_next_test);
   });
 
   do_check_eq(Status.service, STATUS_OK);
 
   setLastSync(PROLONGED_ERROR_DURATION);
   Service.sync();
 });
 
-add_test(function test_syncAndReportErrors_server_maintenance_error() {
+add_test(function test_info_collections_login_prolonged_server_maintenance_error(){
+  // Test info/collections prolonged server maintenance errors are reported.
+  let server = sync_httpd_setup();
+  setUp();
+
+  Service.clusterURL = "http://localhost:8080/maintenance/";
+
+  Service.username = "johnsmith";
+  let backoffInterval;
+  Svc.Obs.add("weave:service:backoff:interval", function observe(subject, data) {
+    Svc.Obs.remove("weave:service:backoff:interval", observe);
+    backoffInterval = subject;
+  });
+
+  Svc.Obs.add("weave:ui:login:error", function onUIUpdate() {
+    Svc.Obs.remove("weave:ui:login:error", onUIUpdate);
+    do_check_true(Status.enforceBackoff);
+    do_check_eq(backoffInterval, 42);
+    do_check_eq(Status.service, SYNC_FAILED);
+    do_check_eq(Status.sync, PROLONGED_SYNC_FAILURE);
+
+    clean();
+    server.stop(run_next_test);
+  });
+
+  do_check_false(Status.enforceBackoff);
+  do_check_eq(Status.service, STATUS_OK);
+
+  setLastSync(PROLONGED_ERROR_DURATION);
+  Service.sync();
+});
+
+add_test(function test_meta_global_login_prolonged_server_maintenance_error(){
+  // Test meta/global prolonged server maintenance errors are reported.
+  let server = sync_httpd_setup();
+  setUp();
+
+  Service.clusterURL = "http://localhost:8080/maintenance/";
+
+  Service.username = "janesmith";
+  let backoffInterval;
+  Svc.Obs.add("weave:service:backoff:interval", function observe(subject, data) {
+    Svc.Obs.remove("weave:service:backoff:interval", observe);
+    backoffInterval = subject;
+  });
+
+  Svc.Obs.add("weave:ui:login:error", function onUIUpdate() {
+    Svc.Obs.remove("weave:ui:login:error", onUIUpdate);
+    do_check_true(Status.enforceBackoff);
+    do_check_eq(backoffInterval, 42);
+    do_check_eq(Status.service, SYNC_FAILED);
+    do_check_eq(Status.sync, PROLONGED_SYNC_FAILURE);
+
+    clean();
+    server.stop(run_next_test);
+  });
+
+  do_check_false(Status.enforceBackoff);
+  do_check_eq(Status.service, STATUS_OK);
+
+  setLastSync(PROLONGED_ERROR_DURATION);
+  Service.sync();
+});
+
+add_test(function test_crypto_keys_login_prolonged_server_maintenance_error(){
+  // Test crypto/keys prolonged server maintenance errors are reported.
+  let server = sync_httpd_setup();
+  setUp();
+
+  Service.clusterURL = "http://localhost:8080/maintenance/";
+  Service.username = "foo";
+  // Force re-download of keys
+  CollectionKeys.clear();
+
+  let backoffInterval;
+  Svc.Obs.add("weave:service:backoff:interval", function observe(subject, data) {
+    Svc.Obs.remove("weave:service:backoff:interval", observe);
+    backoffInterval = subject;
+  });
+
+  Svc.Obs.add("weave:ui:login:error", function onUIUpdate() {
+    Svc.Obs.remove("weave:ui:login:error", onUIUpdate);
+    do_check_true(Status.enforceBackoff);
+    do_check_eq(backoffInterval, 42);
+    do_check_eq(Status.service, SYNC_FAILED);
+    do_check_eq(Status.sync, PROLONGED_SYNC_FAILURE);
+
+    clean();
+    server.stop(run_next_test);
+  });
+
+  do_check_false(Status.enforceBackoff);
+  do_check_eq(Status.service, STATUS_OK);
+
+  setLastSync(PROLONGED_ERROR_DURATION);
+  Service.sync();
+});
+
+add_test(function test_sync_syncAndReportErrors_server_maintenance_error() {
   // Test server maintenance errors are reported
   // when calling syncAndReportErrors.
   let server = sync_httpd_setup();
   setUp();
 
   const BACKOFF = 42;
   let engine = Engines.get("catapult");
   engine.enabled = true;
   engine.exception = {status: 503,
                       headers: {"retry-after": BACKOFF}};
 
   Svc.Obs.add("weave:ui:sync:error", function onUIUpdate() {
     Svc.Obs.remove("weave:ui:sync:error", onUIUpdate);
     do_check_eq(Status.service, SYNC_FAILED_PARTIAL);
     do_check_eq(Status.sync, SERVER_MAINTENANCE);
 
-    Service.startOver();
-    Status.resetSync();
+    clean();
     server.stop(run_next_test);
   });
 
   do_check_eq(Status.service, STATUS_OK);
 
   setLastSync(NON_PROLONGED_ERROR_DURATION);
   ErrorHandler.syncAndReportErrors();
 });
 
-add_test(function test_syncAndReportErrors_prolonged_server_maintenance_error() {
+add_test(function test_info_collections_login_syncAndReportErrors_server_maintenance_error() {
+  // Test info/collections server maintenance errors are reported
+  // when calling syncAndReportErrors.
+  let server = sync_httpd_setup();
+  setUp();
+
+  Service.clusterURL = "http://localhost:8080/maintenance/";
+
+  Service.username = "johnsmith";
+  let backoffInterval;
+  Svc.Obs.add("weave:service:backoff:interval", function observe(subject, data) {
+    Svc.Obs.remove("weave:service:backoff:interval", observe);
+    backoffInterval = subject;
+  });
+
+  Svc.Obs.add("weave:ui:login:error", function onUIUpdate() {
+    Svc.Obs.remove("weave:ui:login:error", onUIUpdate);
+    do_check_true(Status.enforceBackoff);
+    do_check_eq(backoffInterval, 42);
+    do_check_eq(Status.service, LOGIN_FAILED);
+    do_check_eq(Status.login, SERVER_MAINTENANCE);
+
+    clean();
+    server.stop(run_next_test);
+  });
+
+  do_check_false(Status.enforceBackoff);
+  do_check_eq(Status.service, STATUS_OK);
+
+  setLastSync(NON_PROLONGED_ERROR_DURATION);
+  ErrorHandler.syncAndReportErrors();
+});
+
+add_test(function test_meta_global_login_syncAndReportErrors_server_maintenance_error() {
+  // Test meta/global server maintenance errors are reported
+  // when calling syncAndReportErrors.
+  let server = sync_httpd_setup();
+  setUp();
+
+  Service.clusterURL = "http://localhost:8080/maintenance/";
+
+  Service.username = "janesmith";
+  let backoffInterval;
+  Svc.Obs.add("weave:service:backoff:interval", function observe(subject, data) {
+    Svc.Obs.remove("weave:service:backoff:interval", observe);
+    backoffInterval = subject;
+  });
+
+  Svc.Obs.add("weave:ui:login:error", function onUIUpdate() {
+    Svc.Obs.remove("weave:ui:login:error", onUIUpdate);
+    do_check_true(Status.enforceBackoff);
+    do_check_eq(backoffInterval, 42);
+    do_check_eq(Status.service, LOGIN_FAILED);
+    do_check_eq(Status.login, SERVER_MAINTENANCE);
+
+    clean();
+    server.stop(run_next_test);
+  });
+
+  do_check_false(Status.enforceBackoff);
+  do_check_eq(Status.service, STATUS_OK);
+
+  setLastSync(NON_PROLONGED_ERROR_DURATION);
+  ErrorHandler.syncAndReportErrors();
+});
+
+add_test(function test_crypto_keys_login_syncAndReportErrors_server_maintenance_error() {
+  // Test crypto/keys server maintenance errors are reported
+  // when calling syncAndReportErrors.
+  let server = sync_httpd_setup();
+  setUp();
+
+  Service.clusterURL = "http://localhost:8080/maintenance/";
+  Service.username = "foo";
+  // Force re-download of keys
+  CollectionKeys.clear();
+
+  let backoffInterval;
+  Svc.Obs.add("weave:service:backoff:interval", function observe(subject, data) {
+    Svc.Obs.remove("weave:service:backoff:interval", observe);
+    backoffInterval = subject;
+  });
+
+  Svc.Obs.add("weave:ui:login:error", function onUIUpdate() {
+    Svc.Obs.remove("weave:ui:login:error", onUIUpdate);
+    do_check_true(Status.enforceBackoff);
+    do_check_eq(backoffInterval, 42);
+    do_check_eq(Status.service, LOGIN_FAILED);
+    do_check_eq(Status.login, SERVER_MAINTENANCE);
+
+    clean();
+    server.stop(run_next_test);
+  });
+
+  do_check_false(Status.enforceBackoff);
+  do_check_eq(Status.service, STATUS_OK);
+
+  setLastSync(NON_PROLONGED_ERROR_DURATION);
+  ErrorHandler.syncAndReportErrors();
+});
+
+add_test(function test_sync_syncAndReportErrors_prolonged_server_maintenance_error() {
   // Test prolonged server maintenance errors are
   // reported when calling syncAndReportErrors.
   let server = sync_httpd_setup();
   setUp();
 
   const BACKOFF = 42;
   let engine = Engines.get("catapult");
   engine.enabled = true;
   engine.exception = {status: 503,
                       headers: {"retry-after": BACKOFF}};
 
   Svc.Obs.add("weave:ui:sync:error", function onUIUpdate() {
     Svc.Obs.remove("weave:ui:sync:error", onUIUpdate);
     do_check_eq(Status.service, SYNC_FAILED_PARTIAL);
     do_check_eq(Status.sync, SERVER_MAINTENANCE);
 
-    Service.startOver();
-    Status.resetSync();
+    clean();
+    server.stop(run_next_test);
+  });
+
+  do_check_eq(Status.service, STATUS_OK);
+
+  setLastSync(PROLONGED_ERROR_DURATION);
+  ErrorHandler.syncAndReportErrors();
+});
+
+add_test(function test_info_collections_login_syncAndReportErrors_prolonged_server_maintenance_error() {
+  // Test info/collections server maintenance errors are reported
+  // when calling syncAndReportErrors.
+  let server = sync_httpd_setup();
+  setUp();
+
+  Service.clusterURL = "http://localhost:8080/maintenance/";
+
+  Service.username = "johnsmith";
+  let backoffInterval;
+  Svc.Obs.add("weave:service:backoff:interval", function observe(subject, data) {
+    Svc.Obs.remove("weave:service:backoff:interval", observe);
+    backoffInterval = subject;
+  });
+
+  Svc.Obs.add("weave:ui:login:error", function onUIUpdate() {
+    Svc.Obs.remove("weave:ui:login:error", onUIUpdate);
+    do_check_true(Status.enforceBackoff);
+    do_check_eq(backoffInterval, 42);
+    do_check_eq(Status.service, LOGIN_FAILED);
+    do_check_eq(Status.login, SERVER_MAINTENANCE);
+
+    clean();
     server.stop(run_next_test);
   });
 
+  do_check_false(Status.enforceBackoff);
+  do_check_eq(Status.service, STATUS_OK);
+
+  setLastSync(PROLONGED_ERROR_DURATION);
+  ErrorHandler.syncAndReportErrors();
+});
+
+add_test(function test_meta_global_login_syncAndReportErrors_prolonged_server_maintenance_error() {
+  // Test meta/global server maintenance errors are reported
+  // when calling syncAndReportErrors.
+  let server = sync_httpd_setup();
+  setUp();
+
+  Service.clusterURL = "http://localhost:8080/maintenance/";
+
+  Service.username = "janesmith";
+  let backoffInterval;
+  Svc.Obs.add("weave:service:backoff:interval", function observe(subject, data) {
+    Svc.Obs.remove("weave:service:backoff:interval", observe);
+    backoffInterval = subject;
+  });
+
+  Svc.Obs.add("weave:ui:login:error", function onUIUpdate() {
+    Svc.Obs.remove("weave:ui:login:error", onUIUpdate);
+    do_check_true(Status.enforceBackoff);
+    do_check_eq(backoffInterval, 42);
+    do_check_eq(Status.service, LOGIN_FAILED);
+    do_check_eq(Status.login, SERVER_MAINTENANCE);
+
+    clean();
+    server.stop(run_next_test);
+  });
+
+  do_check_false(Status.enforceBackoff);
+  do_check_eq(Status.service, STATUS_OK);
+
+  setLastSync(PROLONGED_ERROR_DURATION);
+  ErrorHandler.syncAndReportErrors();
+});
+
+add_test(function test_crypto_keys_login_syncAndReportErrors_prolonged_server_maintenance_error() {
+  // Test crypto/keys server maintenance errors are reported
+  // when calling syncAndReportErrors.
+  let server = sync_httpd_setup();
+  setUp();
+
+  Service.clusterURL = "http://localhost:8080/maintenance/";
+  Service.username = "foo";
+  // Force re-download of keys
+  CollectionKeys.clear();
+
+  let backoffInterval;
+  Svc.Obs.add("weave:service:backoff:interval", function observe(subject, data) {
+    Svc.Obs.remove("weave:service:backoff:interval", observe);
+    backoffInterval = subject;
+  });
+
+  Svc.Obs.add("weave:ui:login:error", function onUIUpdate() {
+    Svc.Obs.remove("weave:ui:login:error", onUIUpdate);
+    do_check_true(Status.enforceBackoff);
+    do_check_eq(backoffInterval, 42);
+    do_check_eq(Status.service, LOGIN_FAILED);
+    do_check_eq(Status.login, SERVER_MAINTENANCE);
+
+    clean();
+    server.stop(run_next_test);
+  });
+
+  do_check_false(Status.enforceBackoff);
   do_check_eq(Status.service, STATUS_OK);
 
   setLastSync(PROLONGED_ERROR_DURATION);
   ErrorHandler.syncAndReportErrors();
 });
 
 add_test(function test_sync_engine_generic_fail() {
   let server = sync_httpd_setup();
@@ -784,26 +1250,22 @@ add_test(function test_sync_engine_gener
 
     // Test Error log was written on SYNC_FAILED_PARTIAL.
     let entries = logsdir.directoryEntries;
     do_check_true(entries.hasMoreElements());
     let logfile = entries.getNext().QueryInterface(Ci.nsILocalFile);
     do_check_eq(logfile.leafName.slice(0, LOG_PREFIX_ERROR.length),
                 LOG_PREFIX_ERROR);
 
-    Status.resetSync();
-    Service.startOver();
-
+    clean();
     server.stop(run_next_test);
   });
 
   do_check_eq(Status.engines["catapult"], undefined);
-
   do_check_true(setUp());
-
   Service.sync();
 });
 
 // This test should be the last one since it monkeypatches the engine object
 // and we should only have one engine object throughout the file (bug 629664).
 add_test(function test_engine_applyFailed() {
   let server = sync_httpd_setup();
 
@@ -825,20 +1287,16 @@ add_test(function test_engine_applyFaile
 
     // Test Error log was written on SYNC_FAILED_PARTIAL.
     let entries = logsdir.directoryEntries;
     do_check_true(entries.hasMoreElements());
     let logfile = entries.getNext().QueryInterface(Ci.nsILocalFile);
     do_check_eq(logfile.leafName.slice(0, LOG_PREFIX_ERROR.length),
                 LOG_PREFIX_ERROR);
 
-    Status.resetSync();
-    Service.startOver();
-
+    clean();
     server.stop(run_next_test);
   });
 
   do_check_eq(Status.engines["catapult"], undefined);
-
   do_check_true(setUp());
-
   Service.sync();
 });
--- a/services/sync/tests/unit/test_score_triggers.js
+++ b/services/sync/tests/unit/test_score_triggers.js
@@ -1,48 +1,49 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 Cu.import("resource://services-sync/engines.js");
 Cu.import("resource://services-sync/engines/clients.js");
 Cu.import("resource://services-sync/constants.js");
 Cu.import("resource://services-sync/policies.js");
+Cu.import("resource://services-sync/status.js");
 
 Svc.DefaultPrefs.set("registerEngines", "");
 Cu.import("resource://services-sync/service.js");
 
 Engines.register(RotaryEngine);
 let engine = Engines.get("rotary");
 let tracker = engine._tracker;
 engine.enabled = true;
 
 // Tracking info/collections.
 let collectionsHelper = track_collections_helper();
 let upd = collectionsHelper.with_updated_collection;
 
 function sync_httpd_setup() {
-  let handlers = {};  
+  let handlers = {};
 
-  handlers["/1.1/johndoe/storage/meta/global"] = 
+  handlers["/1.1/johndoe/storage/meta/global"] =
     new ServerWBO("global", {}).handler();
-  handlers["/1.1/johndoe/storage/steam"] = 
+  handlers["/1.1/johndoe/storage/steam"] =
     new ServerWBO("steam", {}).handler();
 
   handlers["/1.1/johndoe/info/collections"] = collectionsHelper.handler;
   delete collectionsHelper.collections.crypto;
   delete collectionsHelper.collections.meta;
-  
+
   let cr = new ServerWBO("keys");
   handlers["/1.1/johndoe/storage/crypto/keys"] =
     upd("crypto", cr.handler());
-  
+
   let cl = new ServerCollection();
   handlers["/1.1/johndoe/storage/clients"] =
     upd("clients", cl.handler());
-  
+
   return httpd_setup(handlers);
 }
 
 function setUp() {
   Service.username = "johndoe";
   Service.password = "ilovejane";
   Service.passphrase = "sekrit";
   Service.clusterURL = "http://localhost:8080/";
@@ -88,16 +89,17 @@ add_test(function test_sync_triggered() 
 
   SyncScheduler.syncThreshold = MULTI_DEVICE_THRESHOLD;
   Svc.Obs.add("weave:service:sync:finish", function onSyncFinish() {
     Svc.Obs.remove("weave:service:sync:finish", onSyncFinish);
     _("Sync completed!");
     server.stop(run_next_test);
   });
 
+  do_check_eq(Status.login, LOGIN_SUCCEEDED);
   tracker.score += SCORE_INCREMENT_XLARGE;
 });
 
 add_test(function test_clients_engine_sync_triggered() {
   _("Ensure that client engine score changes trigger a sync.");
 
   // The clients engine is not registered like other engines. Therefore,
   // it needs special treatment throughout the code. Here, we verify the
@@ -110,11 +112,41 @@ add_test(function test_clients_engine_sy
   const TOPIC = "weave:service:sync:finish";
   Svc.Obs.add(TOPIC, function onSyncFinish() {
     Svc.Obs.remove(TOPIC, onSyncFinish);
     _("Sync due to clients engine change completed.");
     server.stop(run_next_test);
   });
 
   SyncScheduler.syncThreshold = MULTI_DEVICE_THRESHOLD;
+  do_check_eq(Status.login, LOGIN_SUCCEEDED);
   Clients._tracker.score += SCORE_INCREMENT_XLARGE;
 });
 
+add_test(function test_incorrect_credentials_sync_not_triggered() {
+  _("Ensure that score changes don't trigger a sync if Status.login != LOGIN_SUCCEEDED.");
+  let server = sync_httpd_setup();
+  setUp();
+
+  // Ensure we don't actually try to sync.
+  function onSyncStart() {
+    do_throw("Should not get here!");
+  }
+  Svc.Obs.add("weave:service:sync:start", onSyncStart);
+
+  // First wait >100ms (nsITimers can take up to that much time to fire, so
+  // we can account for the timer in delayedAutoconnect) and then one event
+  // loop tick (to account for a possible call to weave:service:sync:start).
+  Utils.namedTimer(function() {
+    Utils.nextTick(function() {
+      Svc.Obs.remove("weave:service:sync:start", onSyncStart);
+
+      do_check_eq(Status.login, LOGIN_FAILED_LOGIN_REJECTED);
+
+      Service.startOver();
+      server.stop(run_next_test);
+    });
+  }, 150, {}, "timer");
+
+  // Faking incorrect credentials to prevent score update.
+  Status.login = LOGIN_FAILED_LOGIN_REJECTED;
+  tracker.score += SCORE_INCREMENT_XLARGE;
+});
--- a/services/sync/tests/unit/test_service_verifyLogin.js
+++ b/services/sync/tests/unit/test_service_verifyLogin.js
@@ -74,24 +74,25 @@ function run_test() {
     do_check_eq(Status.service, STATUS_OK);
     do_check_eq(Status.login, LOGIN_SUCCEEDED);
 
     _("If verifyLogin() encounters a server error, it flips on the backoff flag and notifies observers on a 503 with Retry-After.");
     Status.resetSync();
     Service.username = "janedoe";
     do_check_false(Status.enforceBackoff);
     let backoffInterval;    
-    Svc.Obs.add("weave:service:backoff:interval", function(subject, data) {
+    Svc.Obs.add("weave:service:backoff:interval", function observe(subject, data) {
+      Svc.Obs.remove("weave:service:backoff:interval", observe);
       backoffInterval = subject;
     });
     do_check_false(Service.verifyLogin());
     do_check_true(Status.enforceBackoff);
     do_check_eq(backoffInterval, 42);
     do_check_eq(Status.service, LOGIN_FAILED);
-    do_check_eq(Status.login, LOGIN_FAILED_SERVER_ERROR);
+    do_check_eq(Status.login, SERVER_MAINTENANCE);
 
     _("Ensure a network error when finding the cluster sets the right Status bits.");
     Status.resetSync();
     Service.serverURL = "http://localhost:12345/";
     do_check_false(Service.verifyLogin());
     do_check_eq(Status.service, LOGIN_FAILED);
     do_check_eq(Status.login, LOGIN_FAILED_NETWORK_ERROR);
 
--- a/services/sync/tps/extensions/tps/modules/bookmarks.jsm
+++ b/services/sync/tps/extensions/tps/modules/bookmarks.jsm
@@ -36,26 +36,46 @@
  * ***** END LICENSE BLOCK ***** */
 
  /* This is a JavaScript module (JSM) to be imported via 
   * Components.utils.import() and acts as a singleton. Only the following 
   * listed symbols will exposed on import, and only when and where imported.
   */
 
 var EXPORTED_SYMBOLS = ["PlacesItem", "Bookmark", "Separator", "Livemark",
-                        "BookmarkFolder"];
+                        "BookmarkFolder", "DumpBookmarks"];
 
 const CC = Components.classes;
 const CI = Components.interfaces;
 const CU = Components.utils;
 
 CU.import("resource://tps/logger.jsm");
 CU.import("resource://gre/modules/Services.jsm");
 CU.import("resource://gre/modules/PlacesUtils.jsm");
 
+
+var DumpBookmarks = function TPS_Bookmarks__DumpBookmarks() {
+  let writer = {
+    value: "",
+    write: function PlacesItem__dump__write(aStr, aLen) {
+      this.value += aStr;
+    }
+  };
+
+  let options = PlacesUtils.history.getNewQueryOptions();
+  options.queryType = options.QUERY_TYPE_BOOKMARKS;
+  let query = PlacesUtils.history.getNewQuery();
+  query.setFolders([PlacesUtils.placesRootId], 1);
+  let root = PlacesUtils.history.executeQuery(query, options).root;
+  root.containerOpen = true;
+  PlacesUtils.serializeNodeAsJSONToOutputStream(root, writer, true, false);
+  let value = JSON.parse(writer.value);
+  Logger.logInfo("dumping bookmarks\n\n" + JSON.stringify(value, null, ' ') + "\n\n");
+};
+
 /**
  * extend, causes a child object to inherit from a parent
  */
 function extend(child, supertype)
 {
    child.prototype.__proto__ = supertype.prototype;
 }
 
--- a/services/sync/tps/extensions/tps/modules/logger.jsm
+++ b/services/sync/tps/extensions/tps/modules/logger.jsm
@@ -52,16 +52,25 @@ var Logger =
   _potentialError: null,
 
   init: function (path) {
     if (this._converter != null) {
       // we're already open!
       return;
     }
 
+    let prefs = CC["@mozilla.org/preferences-service;1"]
+                .getService(CI.nsIPrefBranch);
+    if (path) {
+      prefs.setCharPref("tps.logfile", path);
+    }
+    else {
+      path = prefs.getCharPref("tps.logfile");
+    }
+
     this._file = CC["@mozilla.org/file/local;1"]
                  .createInstance(CI.nsILocalFile);
     this._file.initWithPath(path);
     var exists = this._file.exists();
 
     // Make a file output stream and converter to handle it.
     this._foStream = CC["@mozilla.org/network/file-output-stream;1"]
                      .createInstance(CI.nsIFileOutputStream);
new file mode 100644
--- /dev/null
+++ b/services/sync/tps/extensions/tps/modules/sync.jsm
@@ -0,0 +1,135 @@
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is TPS.
+ *
+ * The Initial Developer of the Original Code is Mozilla Foundation.
+ * Portions created by the Initial Developer are Copyright (C) 2011
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *   Jonathan Griffin <jgriffin@mozilla.com>
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+
+var EXPORTED_SYMBOLS = ["TPS", "SYNC_WIPE_SERVER", "SYNC_RESET_CLIENT",
+                        "SYNC_WIPE_CLIENT"];
+
+const CC = Components.classes;
+const CI = Components.interfaces;
+const CU = Components.utils;
+
+CU.import("resource://gre/modules/XPCOMUtils.jsm");
+CU.import("resource://gre/modules/Services.jsm");
+CU.import("resource://tps/logger.jsm");
+CU.import("resource://services-sync/service.js");
+CU.import("resource://services-sync/util.js");
+var utils = {}; CU.import('resource://mozmill/modules/utils.js', utils);
+
+const SYNC_WIPE_SERVER = "wipe-server";
+const SYNC_RESET_CLIENT = "reset-client";
+const SYNC_WIPE_CLIENT = "wipe-client";
+
+var prefs = CC["@mozilla.org/preferences-service;1"]
+            .getService(CI.nsIPrefBranch);
+
+var syncFinishedCallback = function() {
+  Logger.logInfo('syncFinishedCallback returned ' + !TPS._waitingForSync);
+  return !TPS._waitingForSync;
+};
+
+var TPS = {
+  _waitingForSync: false,
+  _syncErrors: 0,
+
+  QueryInterface: XPCOMUtils.generateQI([CI.nsIObserver,
+                                         CI.nsISupportsWeakReference]),
+
+  observe: function TPS__observe(subject, topic, data) {
+    Logger.logInfo('Mozmill observed: ' + topic);
+    switch(topic) {
+      case "weave:service:sync:error":
+        if (this._waitingForSync && this._syncErrors == 0) {
+          Logger.logInfo("sync error; retrying...");
+          this._syncErrors++;
+          Utils.namedTimer(function() {
+            Weave.service.sync();
+          }, 1000, this, "resync");
+        }
+        else if (this._waitingForSync) {
+          this._syncErrors = "sync error, see log";
+          this._waitingForSync = false;
+        }
+        break;
+      case "weave:service:sync:finish":
+        if (this._waitingForSync) {
+          this._syncErrors = 0;
+          this._waitingForSync = false;
+        }
+        break;
+    }
+  },
+
+  SetupSyncAccount: function TPS__SetupSyncAccount() {
+    Weave.Service.account = prefs.getCharPref('tps.account.username');
+    Weave.Service.password = prefs.getCharPref('tps.account.password');
+    Weave.Service.passphrase = prefs.getCharPref('tps.account.passphrase');
+    Weave.Svc.Obs.notify("weave:service:setup-complete");
+  },
+
+  Sync: function TPS__Sync(options) {
+    Logger.logInfo('Mozmill starting sync operation: ' + options);
+    switch(options) {
+      case SYNC_WIPE_SERVER:
+        Weave.Svc.Prefs.set("firstSync", "wipeRemote");
+        break;
+      case SYNC_WIPE_CLIENT:
+        Weave.Svc.Prefs.set("firstSync", "wipeClient");
+        break;
+      case SYNC_RESET_CLIENT:
+        Weave.Svc.Prefs.set("firstSync", "resetClient");
+        break;
+      default:
+        Weave.Svc.Prefs.reset("firstSync");
+    }
+
+    if (Weave.Status.service != Weave.STATUS_OK) {
+      return "Sync status not ok: " + Weave.Status.service;
+    }
+
+    this._waitingForSync = true;
+    this._syncErrors = 0;
+    Weave.Service.sync();
+    utils.waitFor(syncFinishedCallback, null, 20000, 500, TPS);
+    return this._syncErrors;
+  },
+};
+
+Services.obs.addObserver(TPS, "weave:service:sync:finish", true);
+Services.obs.addObserver(TPS, "weave:service:sync:error", true);
+Logger.init();
+
+
--- a/services/sync/tps/extensions/tps/modules/tps.jsm
+++ b/services/sync/tps/extensions/tps/modules/tps.jsm
@@ -138,17 +138,17 @@ var TPS =
           if (this._waitingForSync && this._syncErrors == 0) {
             // if this is the first sync error, retry...
             Logger.logInfo("sync error; retrying...");
             this._syncErrors++;
             this._waitingForSync = false;
             Weave.Service.logout();
             Utils.nextTick(this.RunNextTestAction, this);
           }
-          else {
+          else if (this._waitingForSync) {
             // ...otherwise abort the test
             this.DumpError("sync error; aborting test");
             return;
           }
           break;
         case "weave:service:sync:finish":
           if (this._waitingForSync) {
             this._syncErrors = 0;
@@ -337,74 +337,80 @@ var TPS =
           Logger.AssertTrue(false, "invalid action: " + action);
       } 
     }
     Logger.logPass("executing action " + action.toUpperCase() + 
                    " on passwords");
   },
 
   HandleBookmarks: function (bookmarks, action) {
-    let items = [];
-    for (folder in bookmarks) {
-      let last_item_pos = -1;
-      for each (bookmark in bookmarks[folder]) {
-        Logger.clearPotentialError();
-        let placesItem;
-        bookmark['location'] = folder;
-        if (last_item_pos != -1)
-          bookmark['last_item_pos'] = last_item_pos;
-        let item_id = -1;
-        if (action != ACTION_MODIFY && action != ACTION_DELETE)
-          Logger.logInfo("executing action " + action.toUpperCase() + 
-                         " on bookmark " + JSON.stringify(bookmark));
-        if ("uri" in bookmark)
-          placesItem = new Bookmark(bookmark);
-        else if ("folder" in bookmark)
-          placesItem = new BookmarkFolder(bookmark);
-        else if ("livemark" in bookmark)
-          placesItem = new Livemark(bookmark);
-        else if ("separator" in bookmark)
-          placesItem = new Separator(bookmark);
-        if (action == ACTION_ADD) {
-          item_id = placesItem.Create();
-        }
-        else {
-          item_id = placesItem.Find();
-          if (action == ACTION_VERIFY_NOT) {
-            Logger.AssertTrue(item_id == -1,
-              "places item exists but it shouldn't: " +
-              JSON.stringify(bookmark));
+    try {
+      let items = [];
+      for (folder in bookmarks) {
+        let last_item_pos = -1;
+        for each (bookmark in bookmarks[folder]) {
+          Logger.clearPotentialError();
+          let placesItem;
+          bookmark['location'] = folder;
+          if (last_item_pos != -1)
+            bookmark['last_item_pos'] = last_item_pos;
+          let item_id = -1;
+          if (action != ACTION_MODIFY && action != ACTION_DELETE)
+            Logger.logInfo("executing action " + action.toUpperCase() + 
+                           " on bookmark " + JSON.stringify(bookmark));
+          if ("uri" in bookmark)
+            placesItem = new Bookmark(bookmark);
+          else if ("folder" in bookmark)
+            placesItem = new BookmarkFolder(bookmark);
+          else if ("livemark" in bookmark)
+            placesItem = new Livemark(bookmark);
+          else if ("separator" in bookmark)
+            placesItem = new Separator(bookmark);
+          if (action == ACTION_ADD) {
+            item_id = placesItem.Create();
           }
-          else
-            Logger.AssertTrue(item_id != -1, "places item not found", true);
-        }
-        
-        last_item_pos = placesItem.GetItemIndex();
-        items.push(placesItem);
-      }
-    }
-
-    if (action == ACTION_DELETE || action == ACTION_MODIFY) {
-      for each (item in items) {
-        Logger.logInfo("executing action " + action.toUpperCase() + 
-                       " on bookmark " + JSON.stringify(item));
-        switch(action) {
-          case ACTION_DELETE:
-            item.Remove();
-            break;
-          case ACTION_MODIFY:
-            if (item.updateProps != null)
-              item.Update();
-            break;
+          else {
+            item_id = placesItem.Find();
+            if (action == ACTION_VERIFY_NOT) {
+              Logger.AssertTrue(item_id == -1,
+                "places item exists but it shouldn't: " +
+                JSON.stringify(bookmark));
+            }
+            else
+              Logger.AssertTrue(item_id != -1, "places item not found", true);
+          }
+          
+          last_item_pos = placesItem.GetItemIndex();
+          items.push(placesItem);
         }
       }
-    }
 
-    Logger.logPass("executing action " + action.toUpperCase() +
-      " on bookmarks");
+      if (action == ACTION_DELETE || action == ACTION_MODIFY) {
+        for each (item in items) {
+          Logger.logInfo("executing action " + action.toUpperCase() + 
+                         " on bookmark " + JSON.stringify(item));
+          switch(action) {
+            case ACTION_DELETE:
+              item.Remove();
+              break;
+            case ACTION_MODIFY:
+              if (item.updateProps != null)
+                item.Update();
+              break;
+          }
+        }
+      }
+
+      Logger.logPass("executing action " + action.toUpperCase() +
+        " on bookmarks");
+    }
+    catch(e) {
+      DumpBookmarks();
+      throw(e);
+    }
   },
 
   MozmillEndTestListener: function TPS__MozmillEndTestListener(obj) {
     Logger.logInfo("mozmill endTest: " + JSON.stringify(obj));
     if (obj.failed > 0) {
       this.DumpError('mozmill test failed, name: ' + obj.name + ', reason: ' + JSON.stringify(obj.fails));
       return;
     }
@@ -466,17 +472,17 @@ var TPS =
       Logger.logInfo("Firefox builddate: " + Services.appinfo.appBuildID);
       Logger.logInfo("Firefox version: " + Services.appinfo.version);
 
       // do some sync housekeeping
       if (Weave.Service.isLoggedIn) {
         this.DumpError("Sync logged in on startup...profile may be dirty");
         return;
       }
-      
+
       // setup observers
       Services.obs.addObserver(this, "weave:service:sync:finish", true);
       Services.obs.addObserver(this, "weave:service:sync:error", true);
       Services.obs.addObserver(this, "sessionstore-windows-restored", true);
       Services.obs.addObserver(this, "private-browsing", true);
 
       // parse the test file
       Services.scriptloader.loadSubScript(file, this);
@@ -493,17 +499,25 @@ var TPS =
         return;
       }
       Logger.logInfo("setting client.name to " + this.phases["phase" + this._currentPhase]);
       Weave.Svc.Prefs.set("client.name", this.phases["phase" + this._currentPhase]);
 
       // wipe the server at the end of the final test phase
       if (this.phases["phase" + (parseInt(this._currentPhase) + 1)] == undefined)
         this_phase.push([this.WipeServer]);
-      
+
+      // Store account details as prefs so they're accessible to the mozmill
+      // framework.
+      let prefs = CC["@mozilla.org/preferences-service;1"]
+                  .getService(CI.nsIPrefBranch);
+      prefs.setCharPref('tps.account.username', this.config.account.username);
+      prefs.setCharPref('tps.account.password', this.config.account.password);
+      prefs.setCharPref('tps.account.passphrase', this.config.account.passphrase);
+
       // start processing the test actions
       this._currentAction = 0;
     }
     catch(e) {
       this.DumpError("Exception caught: " + e);
       return;
     }
   },
--- a/testing/tps/tps/firefoxrunner.py
+++ b/testing/tps/tps/firefoxrunner.py
@@ -59,25 +59,16 @@ class TPSFirefoxRunner(object):
       self.binary = binary
     self.runner = None
     self.installdir = None
 
   def __del__(self):
     if self.installdir:
       shutil.rmtree(self.installdir, True)
 
-  @property
-  def names(self):
-      if sys.platform == 'darwin':
-          return ['firefox', 'minefield']
-      if (sys.platform == 'linux2') or (sys.platform in ('sunos5', 'solaris')):
-          return ['firefox', 'mozilla-firefox', 'minefield']
-      if os.name == 'nt' or sys.platform == 'cygwin':
-          return ['firefox']
-
   def download_build(self, installdir='downloadedbuild',
                      appname='firefox', macAppName='Minefield.app'):
     self.installdir = os.path.abspath(installdir)
     buildName = os.path.basename(self.url)
     pathToBuild = os.path.join(os.path.dirname(os.path.abspath(__file__)),
                                buildName)
 
     # delete the build if it already exists
@@ -105,38 +96,16 @@ class TPSFirefoxRunner(object):
     else:
       binary = '%s/%s/%s%s' % (installdir,
                                appname,
                                appname,
                                '.exe' if platform['name'] == 'Windows' else '')
 
     return binary
 
-  def get_respository_info(self):
-    """Read repository information from application.ini and platform.ini."""
-    import ConfigParser
-
-    config = ConfigParser.RawConfigParser()
-    dirname = os.path.dirname(self.runner.binary)
-    repository = { }
-
-    for entry in [['application', 'App'], ['platform', 'Build']]:
-        (file, section) = entry
-        config.read(os.path.join(dirname, '%s.ini' % file))
-
-        for entry in [['SourceRepository', 'repository'], ['SourceStamp', 'changeset']]:
-            (key, id) = entry
-
-            try:
-                repository['%s_%s' % (file, id)] = config.get(section, key);
-            except:
-                repository['%s_%s' % (file, id)] = None
-
-    return repository
-
   def run(self, profile=None, timeout=PROCESS_TIMEOUT, env=None, args=None):
     """Runs the given FirefoxRunner with the given Profile, waits
        for completion, then returns the process exit code
     """
     if profile is None:
       profile = Profile()
     self.profile = profile
 
--- a/testing/tps/tps/testrunner.py
+++ b/testing/tps/tps/testrunner.py
@@ -282,17 +282,20 @@ class TPSTestRunner(object):
 
     result = {
       'PASS': lambda x: ('TEST-PASS', ''),
       'FAIL': lambda x: ('TEST-UNEXPECTED-FAIL', x.rstrip()),
       'unknown': lambda x: ('TEST-UNEXPECTED-FAIL', 'test did not complete')
     } [phase.status](phase.errline)
     logstr = "\n%s | %s%s\n" % (result[0], testname, (' | %s' % result[1] if result[1] else ''))
 
-    repoinfo = self.firefoxRunner.get_respository_info()
+    try:
+      repoinfo = self.firefoxRunner.runner.get_repositoryInfo()
+    except:
+      repoinfo = {}
     apprepo = repoinfo.get('application_repository', '')
     appchangeset = repoinfo.get('application_changeset', '')
 
     # save logdata to a temporary file for posting to the db
     tmplogfile = None
     if logdata:
       tmplogfile = TempFile(prefix='tps_log_')
       tmplogfile.write(logdata)