add cloud file tests, r=bienvenu, bug 698925
authorMike Conley <mconley@mozilla.com>
Mon, 12 Mar 2012 16:14:37 -0700
changeset 11071 0d9014f1f95325841c3cd01d7183b845c8043be5
parent 11070 c995fdc3e91a5d53cd1d6f7b84c2dd9731bce25f
child 11072 42e362b0c7c6ae8185daa6e4bd033c9193d675b1
push id463
push userbugzilla@standard8.plus.com
push dateTue, 24 Apr 2012 17:34:51 +0000
treeherdercomm-beta@e53588e8f7b0 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersbienvenu, bug
bugs698925
add cloud file tests, r=bienvenu, bug 698925
mail/test/mozmill/cloudfile/data/testFile1
mail/test/mozmill/cloudfile/data/testFile2
mail/test/mozmill/cloudfile/data/testFile3
mail/test/mozmill/cloudfile/test-cloudfile-backend-dropbox.js
mail/test/mozmill/cloudfile/test-cloudfile-backend-yousendit.js
mail/test/mozmill/cloudfile/test-cloudfile-manager.js
mail/test/mozmill/cloudfile/test-cloudfile-notifications.js
mail/test/mozmill/mozmilltests.list
mail/test/mozmill/pref-window/test-attachments-pane.js
mail/test/mozmill/shared-modules/test-attachment-helpers.js
mail/test/mozmill/shared-modules/test-cloudfile-backend-helpers.js
mail/test/mozmill/shared-modules/test-cloudfile-dropbox-helpers.js
mail/test/mozmill/shared-modules/test-cloudfile-helpers.js
mail/test/mozmill/shared-modules/test-cloudfile-yousendit-helpers.js
mail/test/mozmill/shared-modules/test-mock-object-helpers.js
new file mode 100644
--- /dev/null
+++ b/mail/test/mozmill/cloudfile/data/testFile1
@@ -0,0 +1,1 @@
+Thundercats single-origin coffee culpa, irony minim vero sunt laborum synth aesthetic. Wayfarers photo booth dolore 8-bit, DIY four loko skateboard forage portland id consectetur. Aesthetic aliquip raw denim aute tofu consequat. Before they sold out etsy cliche marfa, magna seitan fixie brooklyn voluptate laborum messenger bag chillwave narwhal truffaut. Cray cupidatat PBR delectus aliqua synth cillum gentrify. Aesthetic do vegan etsy locavore. Veniam assumenda ea, cupidatat aute vero qui.
new file mode 100644
--- /dev/null
+++ b/mail/test/mozmill/cloudfile/data/testFile2
@@ -0,0 +1,3 @@
+Nesciunt odio minim, cupidatat photo booth non post-ironic street art banh mi salvia duis aesthetic squid single-origin coffee. Ex DIY trust fund butcher, esse mustache consequat authentic bushwick twee gentrify hella PBR kogi sustainable. PBR nihil VHS veniam, occaecat dreamcatcher odio iphone irony vero seitan mollit fanny pack adipisicing. Swag jean shorts labore, aesthetic dolore letterpress gluten-free lomo ex wes anderson. Et street art cred Austin velit, raw denim do blog godard leggings. Accusamus adipisicing excepteur occaecat cray sriracha. Leggings brunch artisan occaecat, 3 wolf moon forage mlkshk ad farm-to-table.
+
+
new file mode 100644
--- /dev/null
+++ b/mail/test/mozmill/cloudfile/data/testFile3
@@ -0,0 +1,3 @@
+Commodo et laborum fingerstache semiotics etsy. Organic locavore next level, master cleanse raw denim consectetur wes anderson ethical tempor photo booth quis. Leggings pop-up sed trust fund. Chillwave godard velit high life, typewriter umami trust fund. Laboris aliquip assumenda you probably haven't heard of them exercitation portland. Ea do selvage, stumptown dolore etsy commodo tattooed kogi assumenda. Aute tempor carles consequat cray locavore.
+
+Craft beer consectetur anim ex fap consequat, helvetica hella nihil retro before they sold out letterpress cillum mlkshk. Deserunt tempor scenester put a bird on it kale chips mlkshk occaecat, et umami artisan letterpress raw denim sapiente. Echo park pork belly marfa sunt. Iphone aesthetic fanny pack mollit. Irony pork belly bespoke, shoreditch locavore fixie iphone officia mollit mlkshk consequat hoodie mixtape. Nisi consectetur locavore, godard whatever occaecat id blog ethical wolf hoodie PBR. Trust fund sunt mustache, enim eiusmod aesthetic helvetica leggings pinterest laboris polaroid brooklyn.
new file mode 100644
--- /dev/null
+++ b/mail/test/mozmill/cloudfile/test-cloudfile-backend-dropbox.js
@@ -0,0 +1,192 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+  * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * Tests the Dropbox Bigfile backend.
+ */
+
+let Cu = Components.utils;
+let Cc = Components.classes;
+let Ci = Components.interfaces;
+
+const MODULE_NAME = 'test-cloudfile-backend-dropbox';
+
+const RELATIVE_ROOT = '../shared-modules';
+const MODULE_REQUIRES = ['folder-display-helpers',
+                         'compose-helpers',
+                         'cloudfile-dropbox-helpers',
+                         'observer-helpers',];
+
+Cu.import('resource://gre/modules/Services.jsm');
+
+var gServer, gObsManager;
+
+function setupModule(module) {
+  let fdh = collector.getModule('folder-display-helpers');
+  fdh.installInto(module);
+
+  let ch = collector.getModule('compose-helpers');
+  ch.installInto(module);
+
+  let cfh = collector.getModule('cloudfile-helpers');
+  cfh.installInto(module);
+
+  let cbh = collector.getModule('cloudfile-backend-helpers');
+  cbh.installInto(module);
+
+  let cdh = collector.getModule('cloudfile-dropbox-helpers');
+  cdh.installInto(module);
+
+  let oh = collector.getModule('observer-helpers');
+  oh.installInto(module);
+
+  gObsManager = new cbh.SimpleRequestObserverManager();
+
+  // Enable logging for this test.
+  Services.prefs.setCharPref("Dropbox.logging.dump", "All");
+  Services.prefs.setCharPref("TBOAuth.logging.dump", "All");
+};
+
+function teardownModule() {
+  Services.prefs.QueryInterface(Ci.nsIPrefBranch)
+          .deleteBranch("mail.cloud_files.accounts");
+  Services.prefs.clearUserPref("Dropbox.logging.dump");
+  Services.prefs.clearUserPref("TBOAuth.logging.dump");
+}
+
+function setupTest() {
+  gServer = new MockDropboxServer();
+  gServer.init();
+  gServer.start();
+}
+
+function teardownTest() {
+  gObsManager.check();
+  gObsManager.reset();
+  gServer.stop(mc);
+}
+
+function test_simple_case() {
+  const kExpectedUrl = "http://www.example.com/expectedUrl";
+  const kTopics = [kUploadFile, kGetFileURL];
+
+  gServer.setupUser();
+  gServer.planForUploadFile("testFile1");
+  gServer.planForGetFileURL("testFile1", {url: kExpectedUrl});
+
+  let obs = new ObservationRecorder();
+  for each (let [, topic] in Iterator(kTopics)) {
+    obs.planFor(topic);
+    Services.obs.addObserver(obs, topic, false);
+  }
+
+  let requestObserver = gObsManager.create("test_simple_case - Upload 1");
+  let file = getFile("./data/testFile1", __file__);
+  let provider = gServer.getPreparedBackend("someAccountKey");
+  provider.uploadFile(file, requestObserver);
+
+  mc.waitFor(function () requestObserver.success);
+
+  let urlForFile = provider.urlForFile(file);
+  assert_equals(kExpectedUrl, urlForFile);
+  assert_equals(1, obs.numSightings(kUploadFile));
+  assert_equals(1, obs.numSightings(kGetFileURL));
+
+  gServer.planForUploadFile("testFile1");
+  gServer.planForGetFileURL("testFile1", {url: kExpectedUrl});
+  requestObserver = gObsManager.create("test_simple_case - Upload 2");
+  provider.uploadFile(file, requestObserver);
+  mc.waitFor(function () requestObserver.success);
+  urlForFile = provider.urlForFile(file);
+  assert_equals(kExpectedUrl, urlForFile);
+
+  assert_equals(2, obs.numSightings(kUploadFile));
+  assert_equals(2, obs.numSightings(kGetFileURL));
+
+  for each (let [, topic] in Iterator(kTopics)) {
+    Services.obs.removeObserver(obs, topic);
+  }
+}
+
+function test_chained_uploads() {
+  const kExpectedUrlRoot = "http://www.example.com/";
+  const kTopics = [kUploadFile, kGetFileURL];
+  const kFilenames = ["testFile1", "testFile2", "testFile3"];
+
+  gServer.setupUser();
+
+  for each (let [, filename] in Iterator(kFilenames)) {
+    let expectedUrl = kExpectedUrlRoot + filename;
+    gServer.planForUploadFile(filename);
+    gServer.planForGetFileURL(filename, {url: expectedUrl});
+  }
+
+  let obs = new ObservationRecorder();
+  for each (let [, topic] in Iterator(kTopics)) {
+    obs.planFor(topic);
+    Services.obs.addObserver(obs, topic, false);
+  }
+
+  let provider = gServer.getPreparedBackend("someAccountKey");
+
+  let files = [];
+
+  let observers = kFilenames.map(function(aFilename) {
+    let requestObserver = gObsManager.create("test_chained_uploads for filename " + aFilename);
+    let file = getFile("./data/" + aFilename, __file__);
+    files.push(file);
+    provider.uploadFile(file, requestObserver);
+    return requestObserver;
+  });
+
+  mc.waitFor(function() {
+    return observers.every(function(aListener) aListener.success);
+  }, "Timed out waiting for chained uploads to complete.");
+
+  assert_equals(kFilenames.length, obs.numSightings(kUploadFile));
+
+  for (let [index, filename] in Iterator(kFilenames)) {
+    assert_equals(obs.data[kUploadFile][index], filename);
+    let file = getFile("./data/" + filename, __file__);
+    let expectedUriForFile = kExpectedUrlRoot + filename;
+    let uriForFile = provider.urlForFile(files[index]);
+    assert_equals(expectedUriForFile, uriForFile);
+  }
+
+  assert_equals(kFilenames.length, obs.numSightings(kGetFileURL));
+
+  for each (let [, topic] in Iterator(kTopics)) {
+    Services.obs.removeObserver(obs, topic);
+  }
+}
+
+function test_deleting_uploads() {
+  const kFilename = "testFile1";
+  gServer.setupUser();
+  let provider = gServer.getPreparedBackend("someAccountKey");
+  // Upload a file
+
+  let file = getFile("./data/" + kFilename, __file__);
+  gServer.planForUploadFile(kFilename);
+  gServer.planForGetFileURL(kFilename,
+                                {url: "http://www.example.com/someFile"});
+  let requestObserver = gObsManager.create("test_deleting_uploads - upload 1");
+  provider.uploadFile(file, requestObserver);
+  mc.waitFor(function() requestObserver.success);
+
+  // Try deleting a file
+  let obs = new ObservationRecorder();
+  obs.planFor(kDeleteFile)
+  Services.obs.addObserver(obs, kDeleteFile, false);
+
+  gServer.planForDeleteFile(kFilename);
+  let deleteObserver = gObsManager.create("test_deleting_uploads - delete 1");
+  provider.deleteFile(file, deleteObserver);
+  mc.waitFor(function() deleteObserver.success);
+
+  // Check to make sure the file was deleted on the server
+  assert_equals(1, obs.numSightings(kDeleteFile));
+  assert_equals(obs.data[kDeleteFile][0], kFilename);
+  Services.obs.removeObserver(obs, kDeleteFile);
+}
new file mode 100644
--- /dev/null
+++ b/mail/test/mozmill/cloudfile/test-cloudfile-backend-yousendit.js
@@ -0,0 +1,235 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+  * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * Tests the YouSendIt Bigfile backend.
+ */
+
+let Cu = Components.utils;
+let Cc = Components.classes;
+let Ci = Components.interfaces;
+
+const MODULE_NAME = 'test-cloudfile-backend-yousendit';
+
+const RELATIVE_ROOT = '../shared-modules';
+const MODULE_REQUIRES = ['folder-display-helpers',
+                         'compose-helpers',
+                         'cloudfile-yousendit-helpers',
+                         'observer-helpers',];
+
+Cu.import('resource://gre/modules/Services.jsm');
+
+var gServer, gObsManager;
+
+function setupModule(module) {
+  let fdh = collector.getModule('folder-display-helpers');
+  fdh.installInto(module);
+
+  let ch = collector.getModule('compose-helpers');
+  ch.installInto(module);
+
+  let cfh = collector.getModule('cloudfile-helpers');
+  cfh.installInto(module);
+
+  let cbh = collector.getModule('cloudfile-backend-helpers');
+  cbh.installInto(module);
+
+  let cyh = collector.getModule('cloudfile-yousendit-helpers');
+  cyh.installInto(module);
+
+  let oh = collector.getModule('observer-helpers');
+  oh.installInto(module);
+
+  gObsManager = new cbh.SimpleRequestObserverManager();
+
+  // Enable logging for this group of tests.
+  Services.prefs.setCharPref("YouSendIt.logging.dump", "All");
+};
+
+function teardownModule(module) {
+  Services.prefs.clearUserPref("YouSendIt.logging.dump");
+}
+
+function setupTest() {
+  gServer = new MockYouSendItServer();
+  gServer.init();
+  gServer.start();
+}
+
+function teardownTest() {
+  Services.prefs.QueryInterface(Ci.nsIPrefBranch)
+          .deleteBranch("mail.cloud_files.accounts");
+  gObsManager.check();
+  gObsManager.reset();
+  gServer.stop(mc);
+}
+
+function test_simple_case() {
+  const kExpectedUrl = "http://www.example.com/expectedUrl";
+  const kTopics = [kUploadFile, kGetFileURL];
+
+  gServer.planForUploadFile("testFile1");
+  gServer.planForGetFileURL("testFile1", {url: kExpectedUrl});
+
+  let obs = new ObservationRecorder();
+  for each (let [, topic] in Iterator(kTopics)) {
+    obs.planFor(topic);
+    Services.obs.addObserver(obs, topic, false);
+  }
+
+  let requestObserver = gObsManager.create("test_simple_case - Upload 1");
+  let file = getFile("./data/testFile1", __file__);
+  let provider = gServer.getPreparedBackend("someAccountKey");
+  provider.uploadFile(file, requestObserver);
+
+  mc.waitFor(function () requestObserver.success);
+
+  let urlForFile = provider.urlForFile(file);
+  assert_equals(kExpectedUrl, urlForFile);
+  assert_equals(1, obs.numSightings(kUploadFile));
+  assert_equals(1, obs.numSightings(kGetFileURL));
+
+  gServer.planForUploadFile("testFile1");
+  gServer.planForGetFileURL("testFile1", {url: kExpectedUrl});
+  requestObserver = gObsManager.create("test_simple_case - Upload 2");
+  provider.uploadFile(file, requestObserver);
+  mc.waitFor(function () requestObserver.success);
+  urlForFile = provider.urlForFile(file);
+  assert_equals(kExpectedUrl, urlForFile);
+
+  assert_equals(2, obs.numSightings(kUploadFile));
+  assert_equals(2, obs.numSightings(kGetFileURL));
+
+  for each (let [, topic] in Iterator(kTopics)) {
+    Services.obs.removeObserver(obs, topic);
+  }
+}
+
+function test_chained_uploads() {
+  const kExpectedUrlRoot = "http://www.example.com/";
+  const kTopics = [kUploadFile, kGetFileURL];
+  const kFilenames = ["testFile1", "testFile2", "testFile3"];
+
+  for each (let [, filename] in Iterator(kFilenames)) {
+    let expectedUrl = kExpectedUrlRoot + filename;
+    gServer.planForUploadFile(filename);
+    gServer.planForGetFileURL(filename, {url: expectedUrl});
+  }
+
+  let obs = new ObservationRecorder();
+  for each (let [, topic] in Iterator(kTopics)) {
+    obs.planFor(topic);
+    Services.obs.addObserver(obs, topic, false);
+  }
+
+  let provider = gServer.getPreparedBackend("someAccountKey");
+
+  let files = [];
+
+  let observers = kFilenames.map(function(aFilename) {
+    let requestObserver = gObsManager.create("test_chained_uploads for filename " + aFilename);
+    let file = getFile("./data/" + aFilename, __file__);
+    files.push(file);
+    provider.uploadFile(file, requestObserver);
+    return requestObserver;
+  });
+
+  mc.waitFor(function() {
+    return observers.every(function(aListener) aListener.success);
+  }, "Timed out waiting for chained uploads to complete.");
+
+  assert_equals(kFilenames.length, obs.numSightings(kUploadFile));
+
+  for (let [index, filename] in Iterator(kFilenames)) {
+    assert_equals(obs.data[kUploadFile][index], filename);
+    let file = getFile("./data/" + filename, __file__);
+    let expectedUriForFile = kExpectedUrlRoot + filename;
+    let uriForFile = provider.urlForFile(files[index]);
+    assert_equals(expectedUriForFile, uriForFile);
+  }
+
+  assert_equals(kFilenames.length, obs.numSightings(kGetFileURL));
+
+  for each (let [, topic] in Iterator(kTopics)) {
+    Services.obs.removeObserver(obs, topic);
+  }
+}
+
+function test_deleting_uploads() {
+  const kFilename = "testFile1";
+  let provider = gServer.getPreparedBackend("someAccountKey");
+  // Upload a file
+
+  let file = getFile("./data/" + kFilename, __file__);
+  gServer.planForUploadFile(kFilename);
+  let requestObserver = gObsManager.create("test_deleting_uploads - upload 1");
+  provider.uploadFile(file, requestObserver);
+  mc.waitFor(function() requestObserver.success);
+
+  // Try deleting a file
+  let obs = new ObservationRecorder();
+  obs.planFor(kDeleteFile)
+  Services.obs.addObserver(obs, kDeleteFile, false);
+
+  gServer.planForDeleteFile(kFilename);
+  let deleteObserver = gObsManager.create("test_deleting_uploads - delete 1");
+  provider.deleteFile(file, deleteObserver);
+  mc.waitFor(function() deleteObserver.success);
+
+  // Check to make sure the file was deleted on the server
+  assert_equals(1, obs.numSightings(kDeleteFile));
+  assert_equals(obs.data[kDeleteFile][0], kFilename);
+  Services.obs.removeObserver(obs, kDeleteFile);
+}
+
+
+function test_delete_refreshes_stale_token() {
+  const kFilename = "testFile1";
+  const kUserEmail = "test@example.com";
+
+  // Stop the default YSI mock server, and create our own.
+  gServer.stop(mc);
+  gServer = new MockYouSendItServer();
+  // Replace the auth component with one that counts auth
+  // requests.
+  gServer.auth = new MockYouSendItAuthCounter(gServer);
+  // Replace the deleter component with one that returns the
+  // stale token error.
+  gServer.deleter = new MockYouSendItDeleterStaleToken(gServer);
+  // Fire up the server.
+  gServer.init();
+  gServer.start();
+
+  gServer.setupUser({email: kUserEmail});
+
+  // We're testing to make sure that we refresh on stale tokens
+  // iff a password has been remembered for the user, so make
+  // sure we remember a password...
+  remember_ysi_credentials(kUserEmail, "somePassword");
+
+  // Get a prepared provider...
+  let provider = gServer.getPreparedBackend("someAccountKey");
+  let file = getFile("./data/" + kFilename, __file__);
+  gServer.planForUploadFile(kFilename);
+  let requestObserver = gObsManager.create("test_delete_refreshes_stale_token - upload 1");
+  provider.uploadFile(file, requestObserver);
+  mc.waitFor(function() requestObserver.success);
+
+  // At this point, we should not have seen any auth attempts.
+  assert_equals(gServer.auth.count, 0,
+                "Should not have seen any authorization attempts "
+                + "yet");
+
+  // Now delete the file, and get the stale auth token warning.
+  gServer.planForDeleteFile(kFilename);
+  // We have to pass an observer, so we'll just generate a dummy one.
+  let deleteObserver = new SimpleRequestObserver();
+  provider.deleteFile(file, deleteObserver);
+
+  // Now, since the token was stale, we should see us hit authentication
+  // again.
+  mc.waitFor(function() gServer.auth.count > 0,
+             "Timed out waiting for authorization attempt");
+}
+
new file mode 100644
--- /dev/null
+++ b/mail/test/mozmill/cloudfile/test-cloudfile-manager.js
@@ -0,0 +1,114 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * Tests the richlistbox in the manager for attachment storage
+ * services
+ */
+
+let Cu = Components.utils;
+let Cc = Components.classes;
+let Ci = Components.interfaces;
+
+let MODULE_NAME = 'test-cloudfile-manager';
+
+let RELATIVE_ROOT = '../shared-modules';
+let MODULE_REQUIRES = ['folder-display-helpers',
+                       'pref-window-helpers',
+                       'window-helpers'];
+
+Cu.import("resource://gre/modules/Services.jsm");
+
+const kTestAccountType = "mock";
+
+var cfh;
+
+function setupModule(module) {
+  let fdh = collector.getModule('folder-display-helpers');
+  fdh.installInto(module);
+
+  let pwh = collector.getModule('pref-window-helpers');
+  pwh.installInto(module);
+
+  cfh = collector.getModule('cloudfile-helpers');
+  cfh.installInto(module);
+  cfh.gMockCloudfileManager.register();
+
+  let wh = collector.getModule('window-helpers');
+  wh.installInto(module);
+
+  // Let's set up a few dummy accounts;
+  create_dummy_account("someKey1", kTestAccountType,
+                       "carl's Account");
+  create_dummy_account("someKey2", kTestAccountType,
+                       "Amber's Account");
+  create_dummy_account("someKey3", kTestAccountType,
+                       "alice's Account");
+  create_dummy_account("someKey4", kTestAccountType,
+                       "Bob's Account");
+};
+
+function teardownModule(module) {
+  Services.prefs.QueryInterface(Ci.nsIPrefBranch)
+          .deleteBranch("mail.cloud_files.accounts");
+  cfh.gMockCloudfileManager.unregister();
+}
+
+function create_dummy_account(aKey, aType, aDisplayName) {
+  Services.prefs.setCharPref("mail.cloud_files.accounts." + aKey + ".type",
+                             aType);
+
+  Services.prefs.setCharPref("mail.cloud_files.accounts." + aKey + ".displayName",
+                             aDisplayName);
+}
+
+function destroy_account(aKey) {
+  Services.prefs.clearUserPref("mail.cloud_files.accounts." + aKey);
+}
+
+/**
+ * A helper function to open the preferences dialog and switch
+ * to the attachments pane.
+ *
+ * @param aCallback the function to execute once we've switched
+ *                  to the attachment pane.  This function takes
+ *                  precisely one argument, which is the
+ *                  augmented controller for the preferences
+ *                  window.
+ */
+function open_cloudfile_manager(aCallback) {
+  open_pref_window("paneApplications", function(w) {
+    let tabbox = w.e("attachmentPrefs");
+    tabbox.selectedIndex = 1;
+    aCallback(w);
+    close_window(w);
+  });
+}
+
+/**
+ * Tests that we load the accounts and display them in the
+ * account richlistbox in the correct order (by displayName,
+ * case-insensitive)
+ */
+function test_load_accounts_and_properly_order() {
+  open_cloudfile_manager(function(w) {
+    let richList = w.e("cloudFileView");
+    assert_equals(4, richList.itemCount,
+                  "Should be displaying 4 accounts");
+
+    // Since we're sorting alphabetically by the displayName,
+    // case-insensitive, the items should be ordered with the
+    // following accountKeys:
+    //
+    // someKey3, someKey2, someKey4, someKey1
+    const kExpected = ["someKey3", "someKey2", "someKey4",
+                       "someKey1"];
+
+    for (let [index, expectedKey] in Iterator(kExpected)) {
+      let item = richList.getItemAtIndex(index);
+      assert_equals(expectedKey, item.value,
+                    "The account list is out of order");
+    }
+  });
+}
new file mode 100644
--- /dev/null
+++ b/mail/test/mozmill/cloudfile/test-cloudfile-notifications.js
@@ -0,0 +1,114 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+  * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * Tests the get an account workflow.
+ */
+
+let Cu = Components.utils;
+let Cc = Components.classes;
+let Ci = Components.interfaces;
+
+let MODULE_NAME = 'test-cloudfile-notifications';
+
+let RELATIVE_ROOT = '../shared-modules';
+let MODULE_REQUIRES = ['folder-display-helpers',
+                       'compose-helpers'];
+
+let controller = {};
+let mozmill = {};
+let elib = {};
+Cu.import('resource://mozmill/modules/controller.js', controller);
+Cu.import('resource://mozmill/modules/mozmill.js', mozmill);
+Cu.import('resource://mozmill/modules/elementslib.js', elib);
+Cu.import('resource://gre/modules/Services.jsm');
+
+let maxSize;
+
+function setupModule(module) {
+  let fdh = collector.getModule('folder-display-helpers');
+  fdh.installInto(module);
+
+  let ch = collector.getModule('compose-helpers');
+  ch.installInto(module);
+
+  maxSize = Services.prefs
+                    .getIntPref("mail.compose.big_attachments.threshold_kb",
+                                0) * 1024;
+};
+
+function assert_cloudfile_notification_displayed(aController, aDisplayed) {
+  let nb = aController.window
+                      .document
+                      .getElementById("attachmentNotificationBox");
+  let hasNotification = false;
+
+  if (nb.getNotificationWithValue("bigAttachment"))
+    hasNotification = true;
+
+  assert_equals(hasNotification, aDisplayed,
+                "Expected the notification to be " +
+                (aDisplayed ? "shown" : "not shown"));
+}
+
+function test_no_notification_for_small_file() {
+  let cwc = open_compose_new_mail(mc);
+  add_attachments(cwc, "http://www.example.com/1", 0);
+  assert_cloudfile_notification_displayed(cwc, false);
+
+  add_attachments(cwc, "http://www.example.com/2", 1);
+  assert_cloudfile_notification_displayed(cwc, false);
+
+  add_attachments(cwc, "http://www.example.com/3", 100);
+  assert_cloudfile_notification_displayed(cwc, false);
+
+  add_attachments(cwc, "http://www.example.com/4", 500);
+  assert_cloudfile_notification_displayed(cwc, false);
+}
+
+function test_notification_for_big_files() {
+  let cwc = open_compose_new_mail(mc);
+  add_attachments(cwc, "http://www.example.com/1", maxSize);
+  assert_cloudfile_notification_displayed(cwc, true);
+
+  add_attachments(cwc, "http://www.example.com/2", maxSize + 1000);
+  assert_cloudfile_notification_displayed(cwc, true);
+
+  add_attachments(cwc, "http://www.example.com/3", maxSize + 10000);
+  assert_cloudfile_notification_displayed(cwc, true);
+
+  add_attachments(cwc, "http://www.example.com/4", maxSize + 100000);
+  assert_cloudfile_notification_displayed(cwc, true);
+}
+
+function test_graduate_to_notification() {
+  let cwc = open_compose_new_mail(mc);
+  add_attachments(cwc, "http://www.example.com/1", maxSize - 100);
+  assert_cloudfile_notification_displayed(cwc, false);
+
+  add_attachments(cwc, "http://www.example.com/2", maxSize - 25);
+  assert_cloudfile_notification_displayed(cwc, false);
+
+  add_attachments(cwc, "http://www.example.com/3", maxSize);
+  assert_cloudfile_notification_displayed(cwc, true);
+}
+
+function test_no_notification_if_disabled() {
+  let cwc = open_compose_new_mail(mc);
+
+  Services.prefs.setBoolPref("mail.cloud_files.enabled", false);
+  add_attachments(cwc, "http://www.example.com/1", maxSize);
+  assert_cloudfile_notification_displayed(cwc, false);
+
+  add_attachments(cwc, "http://www.example.com/2", maxSize + 1000);
+  assert_cloudfile_notification_displayed(cwc, false);
+
+  add_attachments(cwc, "http://www.example.com/3", maxSize + 10000);
+  assert_cloudfile_notification_displayed(cwc, false);
+
+  add_attachments(cwc, "http://www.example.com/4", maxSize + 100000);
+  assert_cloudfile_notification_displayed(cwc, false);
+
+  Services.prefs.setBoolPref("mail.cloud_files.enabled", true);
+}
--- a/mail/test/mozmill/mozmilltests.list
+++ b/mail/test/mozmill/mozmilltests.list
@@ -1,11 +1,12 @@
 account
 addrbook
 attachment
+cloudfile
 composition
 content-policy
 content-tabs
 cookies
 folder-display
 folder-pane
 folder-tree-modes
 folder-widget
new file mode 100644
--- /dev/null
+++ b/mail/test/mozmill/pref-window/test-attachments-pane.js
@@ -0,0 +1,71 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+  * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * Tests the manager for attachment storage services
+ */
+
+let Cu = Components.utils;
+let Cc = Components.classes;
+let Ci = Components.interfaces;
+
+let MODULE_NAME = 'test-attachments-pane';
+
+let RELATIVE_ROOT = '../shared-modules';
+let MODULE_REQUIRES = ['folder-display-helpers',
+                       'pref-window-helpers',
+                       'window-helpers'];
+
+function setupModule(module) {
+  let fdh = collector.getModule('folder-display-helpers');
+  fdh.installInto(module);
+
+  let pwh = collector.getModule('pref-window-helpers');
+  pwh.installInto(module);
+
+  let wh = collector.getModule('window-helpers');
+  wh.installInto(module);
+};
+
+/**
+ * Test that if we come back to the Attachment pane, then
+ * we'll automatically be viewing the same tab we were viewing
+ * last time.
+ */
+function test_persist_tabs() {
+  open_pref_window("paneApplications", function(w) {
+    let tabbox = w.e("attachmentPrefs");
+
+    // We should default to be viewing the first tab
+    assert_equals(0, tabbox.selectedIndex,
+                  "The first tab should have been selected");
+    // Switch to the second tab
+    tabbox.selectedIndex = 1;
+    close_window(w);
+  });
+
+  open_pref_window("paneApplications", function(w) {
+    let tabbox = w.e("attachmentPrefs");
+
+    // We should default to be viewing the second tab
+    // now
+    assert_equals(1, tabbox.selectedIndex,
+                  "The second tab selection should have been "
+                  + "persisted");
+    // Switch back to the first tab
+    tabbox.selectedIndex = 0;
+    close_window(w);
+  });
+
+  open_pref_window("paneApplications", function(w) {
+    let tabbox = w.e("attachmentPrefs");
+
+    // We should default to be viewing the first tab
+    assert_equals(0, tabbox.selectedIndex,
+                  "The first tab selection should have been "
+                  + "persisted");
+    close_window(w);
+  });
+
+}
--- a/mail/test/mozmill/shared-modules/test-attachment-helpers.js
+++ b/mail/test/mozmill/shared-modules/test-attachment-helpers.js
@@ -35,30 +35,96 @@
  *
  * ***** END LICENSE BLOCK ***** */
 
 var Ci = Components.interfaces;
 var Cc = Components.classes;
 var Cu = Components.utils;
 
 const MODULE_NAME = "attachment-helpers";
+const RELATIVE_ROOT = "../shared-modules";
+const MODULE_REQUIRES = ['mock-object-helpers'];
 
-const RELATIVE_ROOT = "../shared-modules";
-const MODULE_REQUIRES = [];
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+let gMockFilePickReg;
 
-function setupModule() {
+function setupModule(module) {
+  let moh = collector.getModule('mock-object-helpers');
+
+  gMockFilePickReg = new moh.MockObjectReplacer("@mozilla.org/filepicker;1",
+                                                  MockFilePickerConstructor);
 }
 
 function installInto(module) {
-  setupModule();
+  setupModule(module);
 
   // Now copy helper functions
   module.create_body_part = create_body_part;
   module.create_detached_attachment = create_detached_attachment;
   module.create_deleted_attachment = create_deleted_attachment;
+  module.gMockFilePickReg = gMockFilePickReg;
+  module.gMockFilePicker = gMockFilePicker;
+}
+
+function MockFilePickerConstructor() {
+  return gMockFilePicker;
+};
+
+let gMockFilePicker = {
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsIFilePicker]),
+  defaultExtension: "",
+  filterIndex: null,
+  displayDirectory: null,
+  returnFiles: [],
+  addToRecentDocs: false,
+
+  get defaultString() {
+    throw Cr.NS_ERROR_FAILURE;
+  },
+
+  get fileURL() {
+    return null;
+  },
+
+  get file() {
+    if (this.returnFiles.length >= 1)
+      return this.returnFiles[0];
+    return null;
+  },
+
+  get files() {
+    let self = this;
+    return {
+      index: 0,
+      QueryInterface: XPCOMUtils.generateQI([Ci.nsISimpleEnumerator]),
+      hasMoreElements: function() {
+        return this.index < self.returnFiles.length;
+      },
+      getNext: function() {
+        return self.returnFiles[this.index++];
+      }
+    }
+  },
+
+  init: function gMFP_init(aParent, aTitle, aMode) {
+  },
+
+  appendFilters: function gMFP_appendFilters(aFilterMask) {
+  },
+
+  appendFilter: function gMFP_appendFilter(aTitle, aFilter) {
+  },
+
+  show: function gMFP_show() {
+    return Ci.nsIFilePicker.returnOK;
+  },
+
+  set defaultString(aVal) {
+  },
 }
 
 /**
  * Create a body part with attachments for the message generator
  *
  * @param body the text of the main body of the message
  * @param attachments an array of attachment objects (as strings)
  * @param boundary an optional string defining the boundary of the parts
new file mode 100644
--- /dev/null
+++ b/mail/test/mozmill/shared-modules/test-cloudfile-backend-helpers.js
@@ -0,0 +1,81 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+let Cu = Components.utils;
+let Cc = Components.classes;
+let Ci = Components.interfaces;
+
+const MODULE_NAME = 'cloudfile-backend-helpers';
+
+const RELATIVE_ROOT = '../shared-modules';
+const MODULE_REQUIRES = ['folder-display-helpers'];
+
+const kUserAuthRequested = "cloudfile:auth";
+const kUserDataRequested = "cloudfile:user";
+const kUploadFile = "cloudfile:uploadFile";
+const kGetFileURL = "cloudfile:getFileURL";
+const kDeleteFile = "cloudfile:deleteFile";
+
+Cu.import('resource://mozmill/stdlib/os.js', os);
+Cu.import('resource://gre/modules/XPCOMUtils.jsm');
+
+var fdh;
+
+function installInto(module) {
+  setupModule(module);
+  module.kUserAuthRequested = kUserAuthRequested;
+  module.kUserDataRequested = kUserDataRequested;
+  module.kUploadFile = kUploadFile;
+  module.kGetFileURL = kGetFileURL;
+  module.kDeleteFile = kDeleteFile;
+  module.SimpleRequestObserverManager = SimpleRequestObserverManager;
+  module.SimpleRequestObserver = SimpleRequestObserver;
+}
+
+function setupModule(module) {
+  fdh = collector.getModule('folder-display-helpers');
+}
+
+function SimpleRequestObserverManager() {
+  this._observers = [];
+}
+
+SimpleRequestObserverManager.prototype = {
+  create: function(aName) {
+    let obs = new SimpleRequestObserver(aName);
+    this._observers.push(obs);
+    return obs;
+  },
+
+  check: function() {
+    for each (let [, observer] in Iterator(this._observers)) {
+      if (!observer.success)
+        throw new Error("An observer named " + observer.name + " was leftover, "
+                        + "with its success attribute set to: "
+                        + observer.success);
+    }
+  },
+
+  reset: function() {
+    this._observers = [];
+  }
+}
+
+function SimpleRequestObserver(aName) {
+  this.name = aName;
+};
+
+SimpleRequestObserver.prototype = {
+  success: null,
+  onStartRequest: function(aRequest, aContext) {},
+  onStopRequest: function(aRequest, aContext, aStatusCode) {
+    if (Components.isSuccessCode(aStatusCode)) {
+      this.success = true;
+    } else {
+      this.success = false;
+    }
+  },
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsIRequestObserver,
+                                         Ci.nsISupportsWeakReference]),
+}
new file mode 100644
--- /dev/null
+++ b/mail/test/mozmill/shared-modules/test-cloudfile-dropbox-helpers.js
@@ -0,0 +1,273 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+let Cu = Components.utils;
+let Cc = Components.classes;
+let Ci = Components.interfaces;
+
+const MODULE_NAME = 'cloudfile-dropbox-helpers';
+
+const RELATIVE_ROOT = '../shared-modules';
+const MODULE_REQUIRES = [];
+
+let httpd = {};
+Cu.import('resource://mozmill/stdlib/httpd.js', httpd);
+Cu.import('resource://gre/modules/Services.jsm');
+
+const kDefaultServerPort = 4444;
+const kServerRoot = "http://localhost:" + kDefaultServerPort;
+const kServerPath = "/server/";
+const kContentPath = "/content/";
+const kAuthPath = "/auth/";
+const kServerURL = kServerRoot + kServerPath;
+const kContentURL = kServerRoot + kContentPath;
+const kAuthURL = kServerRoot + kAuthPath;
+const kOAuthTokenPath = "oauth/request_token";
+const kOAuthAuthorizePath = "oauth/authorize";
+const kOAuthAccessTokenPath = "oauth/access_token";
+const kUserInfoPath = "account/info";
+const kPutFilePath = "files_put/sandbox/";
+const kSharesPath = "shares/sandbox/";
+const kDeletePath = "fileops/delete/";
+
+const kDefaultConfig = {
+  port: kDefaultServerPort
+}
+
+const kAuthTokenString = "oauth_token=requestkey&oauth_token_secret=requestsecret";
+
+const kDefaultUser = {
+  referral_link: "https://www.dropbox.com/referrals/r1a2n3d4m5s6t7",
+  display_name: "John P. User",
+  uid: 12345678,
+  country: "US",
+  quota_info: {
+    shared: 253738410565,
+    quota: 107374182400000,
+    normal: 680031877871
+  },
+  email: "john@example.com"
+}
+
+const kDefaultFilePutReturn = {
+  size: "225.4KB",
+  rev: "35e97029684fe",
+  thumb_exists: false,
+  bytes: 230783,
+  modified: "Tue, 19 Jul 2011 21:55:38 +0000",
+//  path: "/Getting_Started.pdf",
+  is_dir: false,
+  icon: "page_white_acrobat",
+  root: "dropbox",
+  mime_type: "application/pdf",
+  revision: 220823
+}
+
+const kDefaultShareReturn = {
+  url: "http://db.tt/APqhX1",
+  expires: "Wed, 17 Aug 2011 02:34:33 +0000"
+}
+
+const kDefaultReturnHeader = {
+  statusCode: 200,
+  statusString: "OK",
+  contentType: "text/plain",
+}
+
+const kDefaultDeleteReturn = {
+  size: "0 bytes",
+  is_deleted: true,
+  bytes: 0,
+  thumb_exists: false,
+  rev: "1f33043551f",
+  modified: "Wed, 10 Aug 2011 18:21:30 +0000",
+//  path: "/test .txt",
+  is_dir: false,
+  icon: "page_white_text",
+  root: "dropbox",
+  mime_type: "text/plain",
+  revision: 492341,
+}
+
+function installInto(module) {
+  module.MockDropboxServer = MockDropboxServer;
+}
+
+function MockDropboxServer() {}
+
+MockDropboxServer.prototype = {
+  _server: null,
+  _toDelete: [],
+
+  getPreparedBackend: function MDBS_getPreparedBackend(aAccountKey) {
+    let dropbox = Cc["@mozilla.org/mail/dropbox;1"]
+                  .getService(Ci.nsIMsgCloudFileProvider);
+
+    let urls = [kServerURL, kContentURL, kAuthURL];
+    dropbox.overrideUrls(urls.length, urls);
+    dropbox.init(aAccountKey);
+    return dropbox;
+  },
+
+  init: function MDBS_init(aConfig) {
+    this._config = kDefaultConfig;
+
+    for (let param in aConfig) {
+      this._config[param] = aConfig[param];
+    }
+
+    this._server = httpd.getServer(this._config.port, '');
+    this._wireOAuth();
+    this._wireDeleter();
+  },
+
+  start: function MDBS_start() {
+    this._server.start(this._config.port);
+  },
+
+  stop: function MDBS_stop(aController) {
+    let allDone = false;
+    this._server.stop(function() {
+      allDone = true;
+    });
+    aController.waitFor(function () allDone,
+                        "Timed out waiting for Dropbox server to stop!");
+  },
+
+  setupUser: function MDBS_wireUser(aData) {
+    aData = this._overrideDefault(kDefaultUser, aData);
+
+    let userFunc = this._noteAndReturnString("cloudfile:user", "",
+                                             JSON.stringify(aData));
+
+    this._server.registerPathHandler(kServerPath + kUserInfoPath,
+                                     userFunc);
+  },
+
+  planForUploadFile: function MDBS_planForUploadFile(aFileName, aData) {
+    let data = kDefaultFilePutReturn;
+    data.path = aFileName;
+    aData = this._overrideDefault(data, aData);
+
+    let putFileFunc = this._noteAndReturnString("cloudfile:uploadFile",
+                                                aFileName,
+                                                JSON.stringify(aData));
+    this._server.registerPathHandler(kContentPath + kPutFilePath + aFileName,
+                                     putFileFunc);
+  },
+
+  planForGetFileURL: function MDBS_planForGetShare(aFileName, aData) {
+    aData = this._overrideDefault(kDefaultShareReturn, aData);
+
+    let getShareFunc = this._noteAndReturnString("cloudfile:getFileURL",
+                                                 aFileName,
+                                                 JSON.stringify(aData));
+    this._server.registerPathHandler(kServerPath + kSharesPath + aFileName,
+                                     getShareFunc);
+  },
+
+  planForDeleteFile: function MDBS_planForDeleteFile(aFilename) {
+    this._toDelete.push(aFilename);
+  },
+
+  _wireDeleter: function MDBS__wireDeleter() {
+    this._server.registerPathHandler(kServerPath + kDeletePath,
+                                     this._delete.bind(this));
+  },
+
+  _delete: function MDBS__delete(aRequest, aResponse) {
+    // Extract the query params
+    let params = parseQueryString(aRequest.queryString);
+    let pathIndex = this._toDelete.indexOf(params.path);
+
+    if (pathIndex == -1) {
+      aResponse.setStatusLine(null, 500, "Bad request");
+      aResponse.write("Was not prepared to delete a file at path: "
+                      + params.path);
+      return;
+    }
+
+    this._toDelete.splice(pathIndex, 1);
+
+    Services.obs.notifyObservers(null, "cloudfile:deleteFile",
+                                 params.path);
+
+    let data = kDefaultDeleteReturn;
+    data.path = params.path;
+
+    aResponse.setStatusLine(null, 200, "OK");
+    aResponse.setHeader("Content-Type", "text/plain");
+    aResponse.write(JSON.stringify(data));
+  },
+
+  _noteAndReturnString: function MDBS__noteAndReturnString(aKey, aValue,
+                                                           aString,
+                                                           aOptions) {
+
+    aOptions = this._overrideDefault(kDefaultReturnHeader, aOptions);
+    let self = this;
+
+    let subjectString = Cc["@mozilla.org/supports-string;1"]
+                        .createInstance(Ci.nsISupportsString);
+    subjectString.data = aString;
+
+    let func = function(aMeta, aResponse) {
+      try {
+        aResponse.setStatusLine(null, aOptions.statusCode,
+                                aOptions.statusString);
+        aResponse.setHeader("Content-Type", aOptions.contentType);
+        aResponse.write(aString);
+        Services.obs.notifyObservers(subjectString, aKey, aValue);
+      } catch(e) {
+        dump("Failed to generate server response: " + e);
+      }
+    }
+    return func;
+  },
+
+  _overrideDefault: function MDBS__overrideDefault(aDefault, aData) {
+    if (aData === undefined)
+      return aDefault;
+
+    for (let param in aDefault) {
+      if (param in aData)
+        aDefault[param] = aData[param];
+    }
+    return aDefault;
+  },
+
+  _wireOAuth: function MDBS__wireOAuth() {
+    let authFunc = this._noteAndReturnString("cloudfile:auth", "",
+                                             kAuthTokenString);
+
+    this._server.registerPathHandler(kServerPath + kOAuthTokenPath,
+                                     authFunc);
+    this._server.registerPathHandler(kServerPath + kOAuthAccessTokenPath,
+                                     authFunc);
+    this._server.registerPathHandler(kAuthPath + kOAuthAuthorizePath,
+                                     this._authHandler);
+  },
+
+  _authHandler: function MDBS__authHandler(meta, response) {
+    response.setStatusLine(null, 302, "Found");
+    response.setHeader("Location", "http://oauthcallback.local/",
+                       false);
+  },
+}
+
+function parseQueryString(str)
+{
+  let paramArray = str.split("&");
+  let regex = /^([^=]+)=(.*)$/;
+  let params = {};
+  for (let i = 0; i < paramArray.length; i++)
+  {
+    let match = regex.exec(paramArray[i]);
+    if (!match)
+      throw "Bad parameter in queryString!  '" + paramArray[i] + "'";
+    params[decodeURIComponent(match[1])] = decodeURIComponent(match[2]);
+  }
+
+  return params;
+}
new file mode 100644
--- /dev/null
+++ b/mail/test/mozmill/shared-modules/test-cloudfile-helpers.js
@@ -0,0 +1,121 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+  * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+let Cu = Components.utils;
+let Cc = Components.classes;
+let Ci = Components.interfaces;
+let Cr = Components.results;
+
+const MODULE_NAME = 'cloudfile-helpers';
+
+const RELATIVE_ROOT = '../shared-modules';
+const MODULE_REQUIRES = ['folder-display-helpers'];
+
+const kMockContractID = "@mozilla.org/mail/mockCloudFile;1";
+const kMockCID = "614fd1f7-a404-4505-92fd-8b0ceff2f66c";
+const kMockID = "mock";
+
+const kDefaults = {
+  type: kMockID,
+  displayName: "Mock Storage",
+  iconClass: "chrome://messenger/skin/icons/dropbox.png",
+  accountKey: null,
+  settingsURL: "",
+  managementURL: "",
+};
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+var fdh, gMockCloudfileComponent;
+
+function setupModule(module) {
+  fdh = collector.getModule("folder-display-helpers");
+  fdh.installInto(module);
+
+  let moh = collector.getModule("mock-object-helpers");
+
+  gMockCloudfileComponent = new moh.MockObjectRegisterer(
+      kMockContractID,
+      kMockCID,
+      MockCloudfileAccount);
+}
+
+function installInto(module) {
+  setupModule(module);
+  module.gMockCloudfileManager = gMockCloudfileManager;
+  module.MockCloudfileAccount = MockCloudfileAccount;
+  module.getFile = getFile;
+}
+
+
+function getFile(aFilename, aRoot) {
+  let path = os.getFileForPath(aRoot);
+  let file = os.getFileForPath(os.abspath(aFilename, path));
+  fdh.assert_true(file.exists, "File " + aFilename + " does not exist.");
+  return file;
+}
+
+function MockCloudfileAccount() {
+  for(let someDefault in kDefaults)
+    this[someDefault] = kDefaults[someDefault];
+}
+
+MockCloudfileAccount.prototype = {
+
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsIMsgCloudFileProvider]),
+  init: function MCA_init(aAccountKey) {
+    this.accountKey = aAccountKey;
+  },
+
+  uploadFile: function(aFile, aListener) {
+    aListener.onStartRequest(null, null);
+    aListener.onStopRequest(null, null, Cr.NS_OK);
+  },
+
+  urlForFile: function(aFile) {
+    return "http://www.example.com/download/someFile";
+  },
+
+  refreshUserInfo: function(aWithUI, aCallback) {
+    aCallback.onStartRequest(null, null);
+    aCallback.onStopRequest(null, null, Cr.NS_OK);
+  }
+};
+
+
+function MockCloudfileController(aAccountKey) {
+  this.instances = [];
+  this.accountKey = aAccountKey;
+}
+
+MockCloudfileController.prototype = {
+  get connected() {
+    return this.account != null;
+  },
+  connect: function MCC_connect(aAccount) {
+    this.account = aAccount;
+  },
+};
+
+var gMockCloudfileManager = {
+  _mock_map: {},
+
+  register: function MCM_register() {
+    gCategoryManager.addCategoryEntry("cloud-files", kMockID,
+                                      "service," + kMockContractID,
+                                      false, true);
+    gMockCloudfileComponent.register();
+  },
+
+  unregister: function MCM_unregister() {
+    gCategoryManager.deleteCategoryEntry("cloud-files", kMockID, false);
+    gMockCloudfileComponent.unregister();
+  },
+
+}
+
+XPCOMUtils.defineLazyServiceGetter(this, "gCategoryManager",
+                                   "@mozilla.org/categorymanager;1",
+                                   "nsICategoryManager");
new file mode 100644
--- /dev/null
+++ b/mail/test/mozmill/shared-modules/test-cloudfile-yousendit-helpers.js
@@ -0,0 +1,741 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+let Cu = Components.utils;
+let Cc = Components.classes;
+let Ci = Components.interfaces;
+
+const MODULE_NAME = 'cloudfile-yousendit-helpers';
+
+const RELATIVE_ROOT = '../shared-modules';
+const MODULE_REQUIRES = [];
+
+let httpd = {};
+Cu.import('resource://mozmill/stdlib/httpd.js', httpd);
+Cu.import('resource://gre/modules/Services.jsm');
+Cu.import('resource:///modules/cloudFileAccounts.js');
+
+const kDefaultServerPort = 4444;
+const kServerRoot = "http://localhost:" + kDefaultServerPort;
+const kServerPath = "";
+const kServerURL = kServerRoot + kServerPath;
+const kAuthPath = "/dpi/v1/auth";
+const kUserInfoPath = "/dpi/v1/user";
+const kFilePreparePath = "/dpi/v1/item/send";
+const kDefaultFileUploadPath = "/uploads";
+const kCommitPath = "/dpi/v1/item/commit";
+const kDeletePath = "/dpi/v1/item";
+
+const kDownloadURLPrefix = "http://www.example.com/downloads";
+
+const kAuthResult = {
+  authToken: "someAuthToken",
+  errorStatus: null,
+}
+
+const kDefaultConfig = {
+  port: kDefaultServerPort
+}
+
+const kDefaultReturnHeader = {
+  statusCode: 200,
+  statusString: "OK",
+  contentType: "text/plain",
+}
+
+const kDefaultUser = {
+  key: null,
+  id: null,
+  type: "BAS",
+  policy: null,
+  version: "v3",
+  password: null,
+  role:null, 
+  email: "john@example.com",
+  firstname: "John",
+  lastname: "User",
+  created: null,
+  account: {
+    passwordProtect: "Pay-per-use",
+    returnReceipt: "Pay-per-use",
+    availableStorage: "2147483648",
+    billingPlan: null,
+    controlExpirationDate: null,
+    dropboxUrl: null,
+    knowledgeBase: "Yes",
+    maxDownloadBWpermonth: "1073741824",
+    maxFileDownloads: "100",
+    maxFileSize: "104857600",
+    premiumDelivery: null,
+    verifyRecipientIdentity: "Included"
+  },
+  status: null,
+  errorStatus: null,
+};
+
+const kDefaultFilePrepare = {
+  itemId: "",
+  uploadUrl: [
+  ],
+  status: null,
+  errorStatus: null,
+}
+
+const kDefaultCommitReturn = {
+  downloadUrl: "",
+  errorStatus: null,
+}
+
+function installInto(module) {
+  module.MockYouSendItServer = MockYouSendItServer;
+  module.MockYouSendItAuthCounter = MockYouSendItAuthCounter;
+  module.MockYouSendItDeleterStaleToken = MockYouSendItDeleterStaleToken;
+  module.remember_ysi_credentials = remember_ysi_credentials;
+}
+
+function MockYouSendItServer() {
+  this.auth = new MockYouSendItAuthSimple(this);
+  this.userInfo = new MockYouSendItUserInfoSimple(this);
+  this.registry = new MockYouSendItItemIdRegistry(this);
+  this.committer = new MockYouSendItCommitterSimple(this);
+  this.receiver = new MockYouSendItReceiverSimple(this);
+  this.deleter = new MockYouSendItDeleterSimple(this);
+  this.preparer = new MockYouSendItPrepareSimple(this);
+}
+
+MockYouSendItServer.prototype = {
+  _server: null,
+
+  getPreparedBackend: function MDBS_getPreparedBackend(aAccountKey) {
+    let username = this.userInfo.username;
+    Services.prefs.setCharPref("mail.cloud_files.accounts." + aAccountKey
+                               + ".username", username);
+
+    cloudFileAccounts.setSecretValue(aAccountKey, cloudFileAccounts.kTokenRealm, "someAuthToken");
+
+    let yousendit = Cc["@mozilla.org/mail/yousendit;1"]
+                    .getService(Ci.nsIMsgCloudFileProvider);
+
+    let urls = [kServerURL];
+    yousendit.overrideUrls(urls.length, urls);
+    yousendit.init(aAccountKey);
+
+    return yousendit;
+  },
+
+  init: function MDBS_init(aConfig) {
+
+    this._config = overrideDefault(kDefaultConfig, aConfig);
+
+    this._server = httpd.getServer(this._config.port, '');
+    this.auth.init(this._server);
+    this.userInfo.init(this._server);
+    this.registry.init(this._server);
+    this.receiver.init(this._server);
+    this.preparer.init(this._server);
+    this.deleter.init(this._server);
+    this.committer.init(this._server);
+
+    this.userInfo.setupUser();
+  },
+
+  start: function MDBS_start() {
+    this._server.start(this._config.port);
+  },
+
+  stop: function MDBS_stop(aController) {
+    this.auth.shutdown();
+    this.userInfo.shutdown();
+    this.registry.shutdown();
+    this.receiver.shutdown();
+    this.preparer.shutdown();
+    this.deleter.shutdown();
+    this.committer.shutdown();
+
+    let allDone = false;
+    this._server.stop(function() {
+      allDone = true;
+    });
+    aController.waitFor(function () allDone,
+                        "Timed out waiting for Dropbox server to stop!");
+  },
+
+  setupUser: function MDBS_wireUser(aData) {
+    this.userInfo.shutdown();
+    this.userInfo.init(this._server);
+    this.userInfo.setupUser(aData);
+  },
+
+  planForUploadFile: function MDBS_planForUploadFile(aFilename, aData) {
+    this.receiver.expect(aFilename);
+    let downloadUrl = kDownloadURLPrefix + "/" + aFilename;
+    this.committer.prepareDownloadURL(aFilename, downloadUrl);
+  },
+
+  planForGetFileURL: function MDBS_planForGetShare(aFilename, aData) {
+    this.committer.prepareDownloadURL(aFilename, aData.url);
+  },
+
+  planForDeleteFile: function MYSS_planForDeleteFile(aFilename) {
+    this.deleter.prepareForDelete(aFilename);
+  },
+}
+
+/**
+ * A simple authentication handler, regardless of input, returns
+ * an authorization token, and fires the cloudfile:auth observable
+ * topic.
+ */
+function MockYouSendItAuthSimple(aYouSendIt) {
+  this._server = null;
+  this._auth = null;
+}
+
+MockYouSendItAuthSimple.prototype = {
+  init: function(aServer) {
+    this._server = aServer;
+    this._auth = generateObservableRequestHandler(
+        "cloudfile:auth", "", JSON.stringify(kAuthResult));
+
+    this._server.registerPathHandler(kAuthPath, this._auth);
+  },
+
+  shutdown: function() {
+    this._server.registerPathHandler(kAuthPath, null);
+    this._server = null;
+    this._auth = null;
+  },
+}
+
+function MockYouSendItUserInfoSimple(aYouSendIt) {
+  this._server = null;
+  this._userInfo = null;
+}
+
+MockYouSendItUserInfoSimple.prototype = {
+  init: function(aServer) {
+    this._server = aServer;
+  },
+
+  setupUser: function(aData) {
+    aData = overrideDefault(kDefaultUser, aData);
+
+    this._userInfo = generateObservableRequestHandler(
+        "cloudfile:auth", "", JSON.stringify(aData));
+    this._server.registerPathHandler(kUserInfoPath, this._userInfo);
+  },
+
+  shutdown: function() {
+    this._server.registerPathHandler(kUserInfoPath, null);
+    this._server = null;
+  },
+
+  get username() {
+    return kDefaultUser.email;
+  },
+};
+
+function MockYouSendItItemIdRegistry(aYouSendIt) {
+  this._itemIdMap = {};
+  this._itemIds = [];
+}
+
+MockYouSendItItemIdRegistry.prototype = {
+  init: function(aServer) {
+    this._itemIdMap = {};
+    this._itemIds = [];
+  },
+
+  shutdown: function() {
+  },
+
+  createItemId: function createItemId() {
+    let itemId = generateUUID();
+    this._itemIds.push(itemId);
+    return itemId;
+  },
+
+  setMapping: function setMapping(aItemId, aFilename) {
+    let oldItemId = this.lookupItemId(aFilename);
+    if (oldItemId)
+      delete this._itemIdMap[oldItemId]
+
+    if (this._itemIds.indexOf(aItemId) != -1) {
+      this._itemIdMap[aItemId] = aFilename;
+    }
+  },
+
+  lookupFilename: function lookupFilename(aItemId) {
+    return this._itemIdMap[aItemId];
+  },
+
+  hasItemId: function hasItemId(aItemId) {
+    return (aItemId in this._itemIdMap);
+  },
+
+  lookupItemId: function lookupItemId(aFilename) {
+    // Slow lookup
+    for (let itemId in this._itemIdMap) {
+      if (this._itemIdMap[itemId] == aFilename)
+        return itemId;
+    }
+    return null;
+  },
+}
+
+/**
+ * A simple preparation handler for a YouSendIt mock server. Allows
+ * a client to query for a unique URL to upload to.  Redirects the actual
+ * uploads to the passed receiver
+ */
+function MockYouSendItPrepareSimple(aYouSendIt) {
+  this._server = null;
+  this._ysi = aYouSendIt;
+}
+
+MockYouSendItPrepareSimple.prototype = {
+  init: function(aServer) {
+    this._server = aServer;
+    this._server.registerPathHandler(kFilePreparePath,
+                                     this._prepare.bind(this));
+  },
+
+  shutdown: function() {
+    this._server.registerPathHandler(kFilePreparePath, null);
+    this._server = null;
+  },
+
+  _prepare: function(aRequest, aResponse) {
+    let itemId = this._ysi.registry.createItemId();
+    let uploadPath = kDefaultFileUploadPath + "/" + itemId;
+
+    let injectedData = {
+      itemId: itemId,
+      uploadUrl: [
+        kServerURL + uploadPath,
+      ]
+    }
+
+    // Set up the path that will accept an uploaded file
+    this._server.registerPathHandler(uploadPath,
+                                     this._ysi.receiver.receiveUpload
+                                                       .bind(this._ysi.receiver));
+
+    // Set up the path that will accept file deletion
+
+    let deletePath = kDeletePath + "/" + itemId;
+
+    this._server.registerPathHandler(deletePath,
+                                     this._ysi.deleter.receiveDelete
+                                                      .bind(this._ysi.deleter));
+
+    let data = overrideDefault(kDefaultFilePrepare, injectedData);
+    aResponse.setStatusLine(null, 200, "OK");
+    aResponse.setHeader("Content-Type", "application/json");
+    aResponse.write(JSON.stringify(data));
+  },
+
+};
+
+function MockYouSendItReceiverSimple(aYouSendIt) {
+  this._server = null;
+  this._ysi = aYouSendIt;
+  this._expectedFiles = [];
+}
+
+MockYouSendItReceiverSimple.prototype = {
+  init: function(aServer) {
+    this._server = aServer;
+  },
+
+  shutdown: function() {
+    this._server = null;
+    this._expectedFiles = [];
+  },
+
+  receiveUpload: function(aRequest, aResponse) {
+    if (aRequest.method != "POST")
+      throw new Error("Uploads should occur with a POST request");
+
+    let formData = parseMultipartForm(aRequest);
+
+    if (!formData)
+      throw new Error("Could not parse multi-part form during upload");
+
+    let filename = formData['filename'];
+    let filenameIndex = this._expectedFiles.indexOf(filename);
+
+    if (filenameIndex == -1)
+      throw new Error("Unexpected file upload: " + formData['filename']);
+
+    Services.obs.notifyObservers(null, "cloudfile:uploadFile",
+                                 filename);
+
+    this._expectedFiles.splice(filenameIndex, 1);
+
+    let itemId = formData['bid'];
+    // Tell the committer how to map the uuid to the filename
+    this._ysi.registry.setMapping(itemId, filename);
+    this._ysi.committer.expectCommit(filename);
+
+    // De-register this URL...
+    this._server.registerPathHandler(aRequest.path, null);
+  },
+
+  expect: function(aFilename) {
+    this._expectedFiles.push(aFilename);
+  },
+
+  get expecting() {
+    return this._expectedFiles;
+  }
+};
+
+function MockYouSendItCommitterSimple(aYouSendIt) {
+  this._server = null;
+  this._ysi = aYouSendIt;
+  this._itemIdMap = {};
+  this._filenameURLMap = {};
+}
+
+MockYouSendItCommitterSimple.prototype = {
+  init: function(aServer) {
+    this._server = aServer;
+  },
+
+  shutdown: function() {
+    this._server = null;
+    this._itemIdMap = {};
+    this._filenameURLMap = {};
+  },
+
+  expectCommit: function(aFilename) {
+    let itemId = this._ysi.registry.lookupItemId(aFilename);
+    let commitPath = kCommitPath + "/" + itemId;
+    this._server.registerPathHandler(commitPath, this.commit.bind(this));
+  },
+
+  prepareDownloadURL: function(aFilename, aURL) {
+    this._filenameURLMap[aFilename] = aURL;
+  },
+
+  commit: function(aRequest, aResponse) {
+    let itemId = aRequest.path.substring(kCommitPath.length + 1);
+
+    if (!this._ysi.registry.hasItemId(itemId)) {
+      aResponse.setStatusLine(null, 500, "Bad request");
+      aResponse.write("The item ID " + itemId + " did not map to an item we "
+                      + "were prepared for committing");
+      return;
+    }
+
+    let filename = this._ysi.registry.lookupFilename(itemId);
+    let url;
+
+    if (filename in this._filenameURLMap)
+      url = this._filenameURLMap[filename];
+    else
+      url = kDownloadURLPrefix + "/" + filename;
+
+    let injectedData = {
+      downloadUrl: url,
+    }
+    let data = overrideDefault(kDefaultCommitReturn, injectedData);
+
+    // Return the default share URL
+    aResponse.setStatusLine(null, 200, "OK");
+    aResponse.setHeader("Content-Type", "application/json");
+    aResponse.write(JSON.stringify(data));
+
+    Services.obs.notifyObservers(null, "cloudfile:getFileURL", filename);
+
+    // Unregister this commit URL
+    this._server.registerPathHandler(aRequest.path, null);
+  },
+};
+
+function MockYouSendItDeleterSimple(aYouSendIt) {
+  this._server = null;
+  this._ysi = aYouSendIt;
+  this._expectDelete = [];
+}
+
+MockYouSendItDeleterSimple.prototype = {
+  init: function(aServer) {
+    this._server = aServer;
+  },
+  shutdown: function() {},
+
+  receiveDelete: function(aRequest, aResponse) {
+    if (aRequest.method != "DELETE") {
+      aResponse.setStatusLine(null, 500, "Bad request");
+      aResponse.write("Expected a DELETE for deleting.");
+      return;
+    }
+
+
+    let itemId = aRequest.path.substring(kDeletePath.length + 1);
+
+    if (!this._ysi.registry.hasItemId(itemId)) {
+      aResponse.setStatusLine(null, 500, "Bad request");
+      aResponse.write("The item ID " + itemId + " did not map to an item "
+                      + "we were prepared for deleting");
+      return;
+    }
+
+    let filename = this._ysi.registry.lookupFilename(itemId);
+    let itemIndex = this._expectDelete.indexOf(filename);
+    if (itemIndex == -1) {
+      aResponse.setStatusLine(null, 500, "Bad request");
+      aRespones.write("Not prepared to delete file with filename: "
+                      + filename);
+      return;
+    }
+
+    this._expectDelete.splice(itemIndex, 1);
+
+    let response = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?><delete><status>OK</status></delete>';
+    aResponse.setStatusLine(null, 200, "OK");
+    aResponse.setHeader("Content-Type", "application/xml");
+    aResponse.write(response);
+
+    Services.obs.notifyObservers(null, "cloudfile:deleteFile", filename);
+    this._server.registerPathHandler(aRequest.path, null);
+  },
+  prepareForDelete: function(aFilename) {
+    this._expectDelete.push(aFilename);
+  },
+};
+
+function MockYouSendItDeleterStaleToken(aYouSendIt) {
+  this._server = null;
+  this._ysi = aYouSendIt;
+}
+
+MockYouSendItDeleterStaleToken.prototype = {
+  init: function(aServer) {
+    this._server = aServer;
+  },
+
+  shutdown: function() {},
+
+  receiveDelete: function(aRequest, aResponse) {
+    let data = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?><delete><errorStatus><code>401</code><message>Invalid auth token</message></errorStatus></delete>';
+
+    aResponse.setStatusLine(null, 200, "OK");
+    aResponse.setHeader("Content-Type", "application/json");
+    aResponse.write(data);
+  },
+
+  prepareForDelete: function(aFilename) {},
+};
+
+function MockYouSendItAuthCounter(aYouSendIt) {
+  this._server = null;
+  this._ysi = aYouSendIt;
+  this.count = 0;
+}
+
+MockYouSendItAuthCounter.prototype = {
+
+  init: function(aServer) {
+    this._server = aServer;
+    this._server.registerPathHandler(kAuthPath, this._auth.bind(this));
+  },
+
+  _auth: function(aRequest, aResponse) {
+    this.count += 1;
+    aResponse.setStatusLine(null, 200, "OK");
+    aResponse.setHeader("Content-Type", "application/json");
+    aResponse.write(JSON.stringify(kAuthResult));
+  },
+
+  shutdown: function() {
+    this._server.registerPathHandler(kAuthPath, null);
+    this._server = null;
+    this._auth = null;
+  },
+}
+
+/**
+ * Adds a username and password pair to nsILoginManager
+ * for the YouSendIt provider instance to retrieve. This may
+ * fail if a password already exists for aUserrname.
+ *
+ * @param aUsername the username to save the password for
+ * @param aPassword the password to save
+ */
+function remember_ysi_credentials(aUsername, aPassword) {
+  let loginInfo = Cc["@mozilla.org/login-manager/loginInfo;1"]
+                  .createInstance(Ci.nsILoginInfo);
+
+  loginInfo.init(kServerURL, null, kServerURL, aUsername,
+                 aPassword, "", "");
+  Services.logins.addLogin(loginInfo);
+}
+
+/**
+ * Utility functions
+ */
+
+function generateObservableRequestHandler(aKey, aValue, aString, aOptions) {
+  aOptions = overrideDefault(kDefaultReturnHeader, aOptions);
+
+  let subjectString = Cc["@mozilla.org/supports-string;1"]
+                      .createInstance(Ci.nsISupportsString);
+  subjectString.data = aString;
+
+  let func = function(aMeta, aResponse) {
+    aResponse.setStatusLine(null, aOptions.statusCode,
+                            aOptions.statusString);
+    aResponse.setHeader("Content-Type", aOptions.contentType);
+    aResponse.write(aString);
+    Services.obs.notifyObservers(subjectString, aKey, aValue);
+  }
+  return func;
+}
+
+function overrideDefault(aDefault, aData) {
+  if (aData === undefined)
+    return aDefault;
+
+  for (let param in aDefault) {
+    if (param in aData)
+      aDefault[param] = aData[param];
+  }
+  return aDefault;
+}
+
+
+/**
+ * Large swaths of this were liberally stolen from 
+ * mozilla/toolkit/crashreporter/test/browser/crashreport.sjs
+ *
+ */
+function generateUUID() {
+  let uuidGen = Cc["@mozilla.org/uuid-generator;1"]
+                  .getService(Ci.nsIUUIDGenerator);
+  let uuid = uuidGen.generateUUID().toString();
+  return uuid.substring(1, uuid.length - 2);
+  
+}
+
+function parseHeaders(data, start)
+{
+  let headers = {};
+
+  while (true) {
+    let done = false;
+    let end = data.indexOf("\r\n", start);
+    if (end == -1) {
+      done = true;
+      end = data.length;
+    }
+    let line = data.substring(start, end);
+    start = end + 2;
+    if (line == "")
+      // empty line, we're done
+      break;
+
+    //XXX: this doesn't handle multi-line headers. do we care?
+    let [name, value] = line.split(':');
+    //XXX: not normalized, should probably use nsHttpHeaders or something
+    headers[name] = value.trimLeft();
+  }
+  return [headers, start];
+}
+
+function parseMultipartForm(request)
+{
+  let boundary = null;
+  // See if this is a multipart/form-data request, and if so, find the
+  // boundary string
+  if (request.hasHeader("Content-Type")) {
+    var contenttype = request.getHeader("Content-Type");
+    var bits = contenttype.split(";");
+    if (bits[0] == "multipart/form-data") {
+      for (var i = 1; i < bits.length; i++) {
+        var b = bits[i].trimLeft();
+        if (b.indexOf("boundary=") == 0) {
+          // grab everything after boundary=
+          boundary = "--" + b.substring(9);
+          break;
+        }
+      }
+    }
+  }
+  if (boundary == null)
+    return null;
+
+  let body = Cc["@mozilla.org/binaryinputstream;1"]
+               .createInstance(Ci.nsIBinaryInputStream);
+  body.setInputStream(request.bodyInputStream);
+
+  let avail;
+  let bytes = [];
+  while ((avail = body.available()) > 0)
+    Array.prototype.push.apply(bytes, body.readByteArray(avail));
+  let data = String.fromCharCode.apply(null, bytes);
+  let formData = {};
+  let done = false;
+  let start = 0;
+  while (true) {
+    // read first line
+    let end = data.indexOf("\r\n", start);
+    if (end == -1) {
+      done = true;
+      end = data.length;
+    }
+
+    let line = data.substring(start, end);
+    // look for closing boundary delimiter line
+    if (line == boundary + "--") {
+      break;
+    }
+
+    if (line != boundary) {
+      dump("expected boundary line but didn't find it!");
+      break;
+    }
+
+    // parse headers
+    start = end + 2;
+    let headers = null;
+    [headers, start] = parseHeaders(data, start);
+
+    // find next boundary string
+    end = data.indexOf("\r\n" + boundary, start);
+    if (end == -1) {
+      dump("couldn't find next boundary string\n");
+      break;
+    }
+
+    // read part data, stick in formData using Content-Disposition header
+    let part = data.substring(start, end);
+    start = end + 2;
+
+    if ("Content-Disposition" in headers) {
+      let bits = headers["Content-Disposition"].split(';');
+      if (bits[0] == 'form-data') {
+        for (let i = 0; i < bits.length; i++) {
+          let b = bits[i].trimLeft();
+          if (b.indexOf('name=') == 0) {
+            //TODO: handle non-ascii here?
+            let name = b.substring(6, b.length - 1);
+            //TODO: handle multiple-value properties?
+            formData[name] = part;
+          }
+          if (b.indexOf('filename=') == 0) {
+            let filename = b.substring(10, b.length - 1);
+            formData['filename'] = filename;
+          }
+          //TODO: handle filename= ?
+          //TODO: handle multipart/mixed for multi-file uploads?
+        }
+      }
+    }
+  }
+  return formData;
+}
+
new file mode 100644
--- /dev/null
+++ b/mail/test/mozmill/shared-modules/test-mock-object-helpers.js
@@ -0,0 +1,173 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+  * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const MODULE_NAME = 'mock-object-helpers';
+const RELATIVE_ROOT = '../shared-modules';
+const MODULE_REQUIRES = [];
+
+let Cm = Components.manager;
+let Ci = Components.interfaces;
+
+function installInto(module) {
+  module.MockObjectReplacer = MockObjectSwapper;
+  module.MockObjectRegisterer = MockObjectRegisterer;
+}
+
+function MockObjectRegisterer(aContractID, aCID, aComponent) {
+  this._contractID = aContractID;
+  this._cid = Components.ID("{" + aCID + "}");
+  this._component = aComponent;
+}
+
+MockObjectRegisterer.prototype = {
+  register: function MOR_register() {
+    let providedConstructor = this._component;
+    this._mockFactory = {
+      createInstance: function MF_createInstance(aOuter, aIid) {
+        if (aOuter != null)
+          throw Components.results.NS_ERROR_NO_AGGREGATION;
+        return new providedConstructor().QueryInterface(aIid);
+      }
+    };
+
+   let componentRegistrar = Cm.QueryInterface(Ci.nsIComponentRegistrar);
+
+   componentRegistrar.registerFactory(this._cid,
+        "",
+        this._contractID,
+        this._mockFactory);
+  },
+
+  unregister: function MOR_unregister() {
+
+    let componentRegistrar = Cm.QueryInterface(Ci.nsIComponentRegistrar);
+
+ 
+    componentRegistrar.unregisterFactory(this._cid,
+        this._mockFactory);
+  },
+}
+
+/**
+ * Allows registering a mock XPCOM component, that temporarily replaces the
+ *  original one when an object implementing a given ContractID is requested
+ *  using createInstance.
+ *
+ * @param aContractID
+ *        The ContractID of the component to replace, for example
+ *        "@mozilla.org/filepicker;1".
+ *
+ * @param aReplacementCtor
+ *        The constructor function for the JavaScript object that will be
+ *        created every time createInstance is called. This object must
+ *        implement QueryInterface and provide the XPCOM interfaces required by
+ *        the specified ContractID (for example
+ *        Components.interfaces.nsIFilePicker).
+ */
+
+function MockObjectReplacer(aContractID, aReplacementCtor) {
+  this._contractID = aContractID;
+  this._replacementCtor = aReplacementCtor;
+}
+
+MockObjectReplacer.prototype = {
+  /**
+   * Replaces the current factory with one that returns a new mock object.
+   *
+   * After register() has been called, it is mandatory to call unregister() to
+   * restore the original component. Usually, you should use a try-catch block
+   * to ensure that unregister() is called.
+   */
+  register: function MORe_register() {
+    if (this._originalFactory)
+      throw Error("Invalid object state when calling register()");
+
+    // Define a factory that creates a new object using the given constructor.
+    var providedConstructor = this._replacementCtor;
+    this._mockFactory = {
+      createInstance: function MF_createInstance(aOuter, aIid) {
+        if (aOuter != null)
+          throw Components.results.NS_ERROR_NO_AGGREGATION;
+        return new providedConstructor().QueryInterface(aIid);
+      }
+    };
+
+    var retVal = swapFactoryRegistration(this._cid, this._contractID, this._mockFactory, this._originalFactory);
+    if ('error' in retVal) {
+      throw new Exception("ERROR: " + retVal.error);
+    } else {
+      this._cid = retVal.cid;
+      this._originalFactory = retVal.originalFactory;
+    }
+  },
+
+  /**
+   * Restores the original factory.
+   */
+  unregister: function MORe_unregister() {
+    if (!this._originalFactory)
+      throw Error("Invalid object state when calling unregister()");
+
+    // Free references to the mock factory.
+    swapFactoryRegistration(this._cid, this._contractID, this._mockFactory, this._originalFactory);
+
+    // Allow registering a mock factory again later.
+    this._cid = null;
+    this._originalFactory = null;
+    this._mockFactory = null;
+  },
+
+  // --- Private methods and properties ---
+
+  /**
+   * The factory of the component being replaced.
+   */
+  _originalFactory: null,
+
+  /**
+   * The CID under which the mock contractID was registered.
+   */
+  _cid: null,
+
+  /**
+   * The nsIFactory that was automatically generated by this object.
+   */
+  _mockFactory: null
+}
+
+/**
+ * Swiped from mozilla/testing/mochitest/tests/SimpleTest/specialpowersAPI.js
+ */
+function swapFactoryRegistration(cid, contractID, newFactory, oldFactory) {
+  var componentRegistrar = Components
+                           .manager
+                           .QueryInterface(Components.interfaces.nsIComponentRegistrar);
+
+  var unregisterFactory = newFactory;
+  var registerFactory = oldFactory;
+
+  if (cid == null) {
+    if (contractID != null) {
+      cid = componentRegistrar.contractIDToCID(contractID);
+      oldFactory = Components.manager.getClassObject(Components.classes[contractID],
+          Components.interfaces.nsIFactory);
+    } else {
+      return {'error': "trying to register a new contract ID: Missing contractID"};
+    }
+
+    unregisterFactory = oldFactory;
+    registerFactory = newFactory;
+  }
+
+  componentRegistrar.unregisterFactory(cid,
+      unregisterFactory);
+
+  // Restore the original factory.
+  componentRegistrar.registerFactory(cid,
+      "",
+      contractID,
+      registerFactory);
+  return {'cid':cid, 'originalFactory':oldFactory};
+}
+