Bug 740956 - Part 3: Tests for cancelling uploads. r+a=bienvenu.
authorMike Conley <mconley@mozilla.com>
Thu, 05 Apr 2012 09:51:08 -0400
changeset 11178 a4075036955b049cd483b9de9549bc354d086e1c
parent 11177 e7c41caf1f084c776e83b0fdb7004cf586544301
child 11179 b7d96060bc37f4ae199e4d4b3833e66a51bfed20
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)
bugs740956
Bug 740956 - Part 3: Tests for cancelling uploads. r+a=bienvenu.
mail/test/mozmill/cloudfile/test-cloudfile-attachment-item.js
mail/test/mozmill/cloudfile/test-cloudfile-attachment-urls.js
mail/test/mozmill/cloudfile/test-cloudfile-backend-dropbox.js
mail/test/mozmill/cloudfile/test-cloudfile-backend-yousendit.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
new file mode 100644
--- /dev/null
+++ b/mail/test/mozmill/cloudfile/test-cloudfile-attachment-item.js
@@ -0,0 +1,187 @@
+/* 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 Filelink attachment item behaviour.
+ */
+
+let MODULE_NAME = 'test-cloudfile-attachment-item';
+
+let RELATIVE_ROOT = '../shared-modules';
+let MODULE_REQUIRES = ['folder-display-helpers',
+                       'compose-helpers',
+                       'cloudfile-helpers',
+                       'attachment-helpers']
+
+let elib = {};
+Cu.import('resource://mozmill/modules/elementslib.js', elib);
+
+const kAttachmentItemContextID = "msgComposeAttachmentItemContext";
+
+var ah, cfh;
+
+function setupModule(module) {
+  collector.getModule('folder-display-helpers').installInto(module);
+  collector.getModule('compose-helpers').installInto(module);
+
+  ah = collector.getModule('attachment-helpers');
+  ah.installInto(module);
+  ah.gMockFilePickReg.register();
+
+  cfh = collector.getModule('cloudfile-helpers');
+  cfh.installInto(module);
+  cfh.gMockCloudfileManager.register();
+}
+
+function teardownModule(module) {
+  cfh.gMockCloudfileManager.unregister();
+  ah.gMockFilePickReg.unregister();
+}
+
+/**
+ * Test that when an upload has been started, we can cancel and restart
+ * the upload, and then cancel again.  For this test, we repeat this
+ * 3 times.
+ */
+function test_upload_cancel_repeat() {
+  const kFile = "./data/testFile1";
+
+  // Prepare the mock file picker to return our test file.
+  let file = cfh.getFile(kFile, __file__);
+  gMockFilePicker.returnFiles = [file];
+
+  let provider = new MockCloudfileAccount();
+  provider.init("someKey");
+  let cw = open_compose_new_mail();
+
+  // We've got a compose window open, and our mock Filelink provider
+  // ready.  Let's attach a file...
+  cw.window.AttachFile();
+
+  // Now we override the uploadFile function of the MockCloudfileAccount
+  // so that we're perpetually uploading...
+  let listener;
+  let started;
+  provider.uploadFile = function(aFile, aListener) {
+    listener = aListener;
+    listener.onStartRequest(null, null);
+    started = true;
+  };
+
+  const kAttempts = 3;
+  let cmd = cw.e("cmd_cancelUpload");
+  let menu = cw.getMenu("#" + kAttachmentItemContextID);
+
+  for (let i = 0; i < kAttempts; i++) {
+    listener = null;
+    started = false;
+
+    // Select the attachment, and choose to convert it to a Filelink
+    let attachmentitem = select_attachments(cw, 0)[0];
+    cw.window.convertSelectedToCloudAttachment(provider);
+    cw.waitFor(function() started);
+
+    assert_can_cancel_upload(cw, provider, listener, file);
+  }
+}
+
+/**
+ * Test that we can cancel a whole series of files being uploaded at once.
+ */
+function test_upload_multiple_and_cancel() {
+  const kFiles = ["./data/testFile1",
+                  "./data/testFile2",
+                  "./data/testFile3"];
+
+  // Prepare the mock file picker to return our test file.
+  let files = cfh.collectFiles(kFiles, __file__);
+  gMockFilePicker.returnFiles = files;
+
+  let provider = new MockCloudfileAccount();
+  provider.init("someKey");
+  let cw = open_compose_new_mail();
+
+  let listener;
+  provider.uploadFile = function(aFile, aListener) {
+    listener = aListener;
+    listener.onStartRequest(null, null);
+  };
+
+  cw.window.attachToCloud(provider);
+
+  for (let i = files.length - 1; i >= 0; --i)
+    assert_can_cancel_upload(cw, provider, listener, files[i]);
+}
+
+/**
+ * Helper function that takes an upload in progress, and cancels it,
+ * ensuring that the nsIMsgCloduFileProvider.uploadCanceled status message
+ * is returned to the passed in listener.
+ *
+ * @param aController the compose window controller to use.
+ * @param aProvider a MockCloudfileAccount for which the uploads have already
+ *                  started.
+ * @param aListener the nsIRequestObserver passed to aProvider's uploadFile
+ *                  function.
+ * @param aTargetFile the nsILocalFile to cancel the upload for.
+ */
+function assert_can_cancel_upload(aController, aProvider, aListener,
+                                  aTargetFile) {
+  let cancelled = false;
+
+  // Override the provider's cancelFileUpload function.  We can do this because
+  // it's assumed that the provider is a MockCloudfileAccount.
+  aProvider.cancelFileUpload = function(aFileToCancel) {
+    if (aTargetFile.equals(aFileToCancel)) {
+      aListener.onStopRequest(null, null,
+                              Ci.nsIMsgCloudFileProvider
+                                .uploadCanceled);
+      cancelled = true;
+    }
+  };
+
+  // Retrieve the attachment bucket index for the target file...
+  let index = get_attachmentitem_index_for_file(aController,
+                                                aTargetFile);
+
+  // Select that attachmentitem in the bucket
+  let attachmentitem = select_attachments(aController, index)[0];
+
+  // Bring up the context menu, and click cancel.
+  let cmd = aController.e("cmd_cancelUpload");
+  let menu = aController.getMenu("#" + kAttachmentItemContextID);
+  let elem = new elib.Elem(attachmentitem);
+  menu.open(elem);
+  wait_for_popup_to_open(aController.e(kAttachmentItemContextID));
+  assert_false(cmd.hidden);
+  assert_false(cmd.disabled);
+  let cancelItem = aController.eid("context_cancelUpload");
+  aController.click(cancelItem);
+
+  // Close the popup, and wait for the cancellation to be complete.
+  close_popup(aController, aController.eid(kAttachmentItemContextID));
+  aController.waitFor(function() cancelled);
+}
+
+/**
+ * A helper function to find the attachment bucket index for a particular
+ * nsILocalFile. Returns null if no attachmentitem is found.
+ *
+ * @param aController the compose window controller to use.
+ * @param aFile the nsILocalFile to search for.
+ */
+function get_attachmentitem_index_for_file(aController, aFile) {
+  // Get the fileUrl from the file.
+  let fileUrl = aController.window.FileToAttachment(aFile).url;
+
+  // Get the bucket, and go through each item looking for the matching
+  // attachmentitem.
+  let bucket = aController.e("attachmentBucket");
+  for (let i = 0; i < bucket.getRowCount(); ++i) {
+    let attachmentitem = bucket.getItemAtIndex(i);
+    if (attachmentitem.attachment.url == fileUrl)
+      return i;
+  }
+  return null;
+}
--- a/mail/test/mozmill/cloudfile/test-cloudfile-attachment-urls.js
+++ b/mail/test/mozmill/cloudfile/test-cloudfile-attachment-urls.js
@@ -76,30 +76,16 @@ function teardownModule(module) {
 }
 
 function setupTest() {
   // If our signature got accidentally wiped out, let's just put it back.
   Services.prefs.setCharPref(kDefaultSigKey, kDefaultSig);
 }
 
 /**
- * Helper function for getting the nsILocalFile's for some files located
- * in a subdirectory of the test directory.
- *
- * @param aFiles an array of filename strings for files underneath the test
- *               file directory.
- *
- * Example: let files = collectFiles(['./data/testFile1', './data/testFile2']);
- */
-function collectFiles(aFiles) {
-  return [cfh.getFile(filename, __file__)
-          for each (filename in aFiles)]
-}
-
-/**
  * Given some compose window controller, wait for some Filelink URLs to be
  * inserted.
  *
  * @param aController the controller for a compose window.
  * @param aNumUrls the number of Filelink URLs that are expected.
  * @returns an array containing the root containment node, the list node, and
  *          an array of the link URL nodes.
  */
@@ -133,17 +119,17 @@ function wait_for_attachment_urls(aContr
  * @param aText an array of strings to type into the compose window. Each
  *              string is followed by pressing the RETURN key, except for
  *              the final string.  Pass an empty array if you don't want
  *              anything typed.
  * @param aFiles an array of filename strings for files located beneath
  *               the test directory.
  */
 function prepare_some_attachments_and_reply(aText, aFiles) {
-  gMockFilePicker.returnFiles = collectFiles(aFiles);
+  gMockFilePicker.returnFiles = collectFiles(aFiles, __file__);
 
   let provider = new MockCloudfileAccount();
   provider.init("someKey");
 
   be_in_folder(gFolder);
   let msg = select_click_row(0);
   assert_selected_and_displayed(mc, msg);
 
@@ -164,17 +150,17 @@ function prepare_some_attachments_and_re
  * @param aText an array of strings to type into the compose window. Each
  *              string is followed by pressing the RETURN key, except for
  *              the final string.  Pass an empty array if you don't want
  *              anything typed.
  * @param aFiles an array of filename strings for files located beneath
  *               the test directory.
  */
 function prepare_some_attachments_and_forward(aText, aFiles) {
-  gMockFilePicker.returnFiles = collectFiles(aFiles);
+  gMockFilePicker.returnFiles = collectFiles(aFiles, __file__);
 
   let provider = new MockCloudfileAccount();
   provider.init("someKey");
 
   be_in_folder(gFolder);
   let msg = select_click_row(0);
   assert_selected_and_displayed(mc, msg);
 
@@ -242,17 +228,17 @@ function test_inserts_linebreak_on_empty
   try_without_signature(subtest_inserts_linebreak_on_empty_compose);
 }
 
 /**
  * Subtest for test_inserts_linebreak_on_empty_compose - can be executed
  * on both plaintext and HTML compose windows.
  */
 function subtest_inserts_linebreak_on_empty_compose() {
-  gMockFilePicker.returnFiles = collectFiles(kFiles);
+  gMockFilePicker.returnFiles = collectFiles(kFiles, __file__);
   let provider = new MockCloudfileAccount();
   provider.init("someKey");
   let cw = open_compose_new_mail();
   cw.window.attachToCloud(provider);
 
   let [root, list, urls] = wait_for_attachment_urls(cw, kFiles.length);
 
   let br = root.previousSibling;
@@ -271,17 +257,17 @@ function subtest_inserts_linebreak_on_em
 /**
  * Test that if we open up a composer and immediately attach a Filelink,
  * a linebreak is inserted before the containment node. This test also
  * ensures that, with a signature already in the compose window, we don't
  * accidentally insert the attachment URL containment within the signature
  * node.
  */
 function test_inserts_linebreak_on_empty_compose_with_signature() {
-  gMockFilePicker.returnFiles = collectFiles(kFiles);
+  gMockFilePicker.returnFiles = collectFiles(kFiles, __file__);
   let provider = new MockCloudfileAccount();
   provider.init("someKey");
   let cw = open_compose_new_mail();
   cw.window.attachToCloud(provider);
   // wait_for_attachment_urls ensures that the attachment URL containment
   // node is an immediate child of the body of the message, so if this
   // succeeds, then we were not in the signature node.
   let [root, list, urls] = wait_for_attachment_urls(cw, kFiles.length);
@@ -372,17 +358,17 @@ function test_adding_filelinks_to_writte
   try_without_signature(subtest_adding_filelinks_to_written_message);
 }
 
 /**
  * Subtest for test_adding_filelinks_to_written_message - generalized for both
  * HTML and plaintext mail.
  */
 function subtest_adding_filelinks_to_written_message() {
-  gMockFilePicker.returnFiles = collectFiles(kFiles);
+  gMockFilePicker.returnFiles = collectFiles(kFiles, __file__);
   let provider = new MockCloudfileAccount();
   provider.init("someKey");
   let cw = open_compose_new_mail();
 
   type_in_composer(cw, kLines);
   cw.window.attachToCloud(provider);
 
   let [root, list, urls] = wait_for_attachment_urls(cw, kFiles.length);
--- a/mail/test/mozmill/cloudfile/test-cloudfile-backend-dropbox.js
+++ b/mail/test/mozmill/cloudfile/test-cloudfile-backend-dropbox.js
@@ -208,16 +208,45 @@ function test_create_existing_account() 
     },
   }
 
   provider.createExistingAccount(myObs);
   mc.waitFor(function() done);
 }
 
 /**
+ * Test that cancelling an upload causes onStopRequest to be
+ * called with nsIMsgCloudFileProvider.uploadCanceled.
+ */
+function test_can_cancel_upload() {
+  const kFilename = "testFile1";
+  gServer.setupUser();
+  let provider = gServer.getPreparedBackend("someNewAccount");
+  let file = getFile("./data/" + kFilename, __file__);
+  gServer.planForUploadFile(kFilename, 2000);
+  assert_can_cancel_uploads(mc, provider, [file]);
+}
+
+/**
+ * Test that cancelling several uploads causes onStopRequest to be
+ * called with nsIMsgCloudFileProvider.uploadCanceled.
+ */
+function test_can_cancel_uploads() {
+  const kFiles = ["testFile2", "testFile3", "testFile4"];
+  gServer.setupUser();
+  let provider = gServer.getPreparedBackend("someNewAccount");
+  let files = [];
+  for each (let [, filename] in Iterator(kFiles)) {
+    gServer.planForUploadFile(filename, 2000);
+    files.push(getFile("./data/" + filename, __file__));
+  }
+  assert_can_cancel_uploads(mc, provider, files);
+}
+
+/**
  * Test that completing the OAuth procedure results in an attempt to logout.
  */
 function test_oauth_complete_causes_logout() {
   let provider = gServer.getPreparedBackend("someNewAccount");
   let dummyObs = gObsManager.create("test_oauth_complete_causes_logout");
   let obs = new ObservationRecorder();
   obs.planFor(kLogout);
   Services.obs.addObserver(obs, kLogout, false);
--- a/mail/test/mozmill/cloudfile/test-cloudfile-backend-yousendit.js
+++ b/mail/test/mozmill/cloudfile/test-cloudfile-backend-yousendit.js
@@ -183,16 +183,43 @@ function test_deleting_uploads() {
 
   // 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);
 }
 
 /**
+ * Test that cancelling an upload causes onStopRequest to be
+ * called with nsIMsgCloudFileProvider.uploadCanceled.
+ */
+function test_can_cancel_upload() {
+  const kFilename = "testFile1";
+  let provider = gServer.getPreparedBackend("anAccount");
+  let file = getFile("./data/" + kFilename, __file__);
+  gServer.planForUploadFile(kFilename, 2000);
+  assert_can_cancel_uploads(mc, provider, [file]);
+}
+
+/**
+ * Test that cancelling several uploads causes onStopRequest to be
+ * called with nsIMsgCloudFileProvider.uploadCanceled.
+ */
+function test_can_cancel_uploads() {
+  const kFiles = ["testFile2", "testFile3", "testFile4"];
+  let provider = gServer.getPreparedBackend("anAccount");
+  let files = [];
+  for each (let [, filename] in Iterator(kFiles)) {
+    gServer.planForUploadFile(filename, 2000);
+    files.push(getFile("./data/" + filename, __file__));
+  }
+  assert_can_cancel_uploads(mc, provider, files);
+}
+
+/**
  * Test that when we call createExistingAccount, onStopRequest is successfully
  * called, and we pass the correct parameters.
  */
 function test_create_existing_account() {
   // We have to mock out the auth prompter, or else we'll lock up.
   gMockAuthPromptReg.register();
   let provider = gServer.getPreparedBackend("someNewAccount");
   let done = false;
@@ -254,9 +281,8 @@ function test_delete_refreshes_stale_tok
   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");
 }
-
--- a/mail/test/mozmill/shared-modules/test-cloudfile-backend-helpers.js
+++ b/mail/test/mozmill/shared-modules/test-cloudfile-backend-helpers.js
@@ -4,44 +4,47 @@
 
 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 MODULE_REQUIRES = ['folder-display-helpers',
+                         'window-helpers'];
 
 const kUserAuthRequested = "cloudfile:auth";
 const kUserDataRequested = "cloudfile:user";
 const kUploadFile = "cloudfile:uploadFile";
 const kGetFileURL = "cloudfile:getFileURL";
 const kDeleteFile = "cloudfile:deleteFile";
 const kLogout = "cloudfile:logout";
 
 Cu.import('resource://mozmill/stdlib/os.js', os);
 Cu.import('resource://gre/modules/XPCOMUtils.jsm');
 
-var fdh;
+var fdh, wh;
 
 function installInto(module) {
   setupModule(module);
   module.kUserAuthRequested = kUserAuthRequested;
   module.kUserDataRequested = kUserDataRequested;
   module.kUploadFile = kUploadFile;
   module.kGetFileURL = kGetFileURL;
   module.kDeleteFile = kDeleteFile;
   module.kLogout = kLogout;
   module.SimpleRequestObserverManager = SimpleRequestObserverManager;
   module.SimpleRequestObserver = SimpleRequestObserver;
+  module.assert_can_cancel_uploads = assert_can_cancel_uploads;
 }
 
 function setupModule(module) {
   fdh = collector.getModule('folder-display-helpers');
+  wh = collector.getModule('window-helpers');
 }
 
 function SimpleRequestObserverManager() {
   this._observers = [];
 }
 
 SimpleRequestObserverManager.prototype = {
   create: function(aName) {
@@ -76,8 +79,54 @@ SimpleRequestObserver.prototype = {
       this.success = true;
     } else {
       this.success = false;
     }
   },
   QueryInterface: XPCOMUtils.generateQI([Ci.nsIRequestObserver,
                                          Ci.nsISupportsWeakReference]),
 }
+
+/**
+ * This function uploads one or more files, and then proceeds to cancel
+ * them.  This function assumes that the mock server for the provider
+ * is prepared for the uploaded files, and will give enough time for
+ * the uploads to be cancelled before they complete.
+ *
+ * @param aController the controller to use for waitFors.
+ * @param aProvider the provider to upload and cancel the files with.
+ * @param aFiles the array of files to upload.
+ */
+function assert_can_cancel_uploads(aController, aProvider, aFiles) {
+  let fileListenerMap = [];
+  wh.plan_for_observable_event("cloudfile:uploadStarted");
+
+  for each (let [, file] in Iterator(aFiles)) {
+    let mapping = {};
+    mapping.listener = {
+      onStartRequest: function(aRequest, aContext) {
+        mapping.started = true;
+      },
+      onStopRequest: function(aRequest, aContext, aStatusCode) {
+        if (aStatusCode == Ci.nsIMsgCloudFileProvider.uploadCanceled)
+          mapping.cancelled = true;
+      },
+    }
+
+    aProvider.uploadFile(file, mapping.listener);
+    fileListenerMap.push(mapping);
+  }
+
+  // Wait for the first file to start uploading...
+  wh.wait_for_observable_event("cloudfile:uploadStarted");
+
+  // Go backwards through the file list, ensuring that we can cancel the
+  // last file, all the way to the first.
+  for (let i = aFiles.length - 1; i >= 0; --i)
+    aProvider.cancelFileUpload(aFiles[i]);
+
+  aController.waitFor(function() {
+    return fileListenerMap.length == aFiles.length &&
+           fileListenerMap.every(function(aMapping) {
+             return aMapping.cancelled
+           })
+  }, "Timed out waiting for cancellation to occur");
+}
--- a/mail/test/mozmill/shared-modules/test-cloudfile-dropbox-helpers.js
+++ b/mail/test/mozmill/shared-modules/test-cloudfile-dropbox-helpers.js
@@ -96,20 +96,21 @@ function installInto(module) {
   module.MockDropboxServer = MockDropboxServer;
 }
 
 function MockDropboxServer() {}
 
 MockDropboxServer.prototype = {
   _server: null,
   _toDelete: [],
+  _timers: [],
 
   getPreparedBackend: function MDBS_getPreparedBackend(aAccountKey) {
     let dropbox = Cc["@mozilla.org/mail/dropbox;1"]
-                  .getService(Ci.nsIMsgCloudFileProvider);
+                  .createInstance(Ci.nsIMsgCloudFileProvider);
 
     let urls = [kServerURL, kContentURL, kAuthURL, kLogoutURL];
     dropbox.overrideUrls(urls.length, urls);
     dropbox.init(aAccountKey);
     return dropbox;
   },
 
   init: function MDBS_init(aConfig) {
@@ -129,39 +130,77 @@ MockDropboxServer.prototype = {
   },
 
   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!");
+                        "Timed out waiting for Dropbox server to stop!",
+                        10000);
   },
 
   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) {
+  /**
+   * Plan to upload a file with a particular filename.
+   *
+   * @param aFilename the name of the file that will be uploaded.
+   * @param aMSeconds an optional argument, for how long the upload should
+   *                  last in milliseconds.
+   */
+  planForUploadFile: function MDBS_planForUploadFile(aFilename, aMSeconds) {
     let data = kDefaultFilePutReturn;
-    data.path = aFileName;
-    aData = this._overrideDefault(data, aData);
+    data.path = aFilename;
+
+    // Prepare the function that will receive the upload and respond
+    // appropriately.
+    let putFileFunc = this._noteAndReturnString("cloudfile:uploadFile",
+                                                aFilename,
+                                                JSON.stringify(data));
+
+    // Also prepare a function that will, if necessary, wait aMSeconds before
+    // firing putFileFunc.
+    let waitWrapperFunc = function(aRequest, aResponse) {
+      Services.obs.notifyObservers(null, "cloudfile:uploadStarted",
+                                   aFilename);
 
-    let putFileFunc = this._noteAndReturnString("cloudfile:uploadFile",
-                                                aFileName,
-                                                JSON.stringify(aData));
-    this._server.registerPathHandler(kContentPath + kPutFilePath + aFileName,
-                                     putFileFunc);
+      if (!aMSeconds) {
+        putFileFunc(aRequest, aResponse);
+        return;
+      }
+
+      // Ok, we're waiting a bit.  Tell the HTTP server that we're going to
+      // generate a response asynchronously, then set a timer to send the
+      // response after aMSeconds milliseconds.
+      aResponse.processAsync();
+      let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+      let timerEvent = {
+        notify: function(aTimer) {
+          putFileFunc(aRequest, aResponse);
+          aResponse.finish();
+        },
+      };
+      timer.initWithCallback(timerEvent, aMSeconds,
+                             Ci.nsITimer.TYPE_ONE_SHOT);
+      // We can't let the timer get garbage collected, so we store it.
+      this._timers.push(timer);
+    }.bind(this);
+
+    this._server.registerPathHandler(kContentPath + kPutFilePath + aFilename,
+                                     waitWrapperFunc);
   },
 
   planForGetFileURL: function MDBS_planForGetShare(aFileName, aData) {
     aData = this._overrideDefault(kDefaultShareReturn, aData);
 
     let getShareFunc = this._noteAndReturnString("cloudfile:getFileURL",
                                                  aFileName,
                                                  JSON.stringify(aData));
--- a/mail/test/mozmill/shared-modules/test-cloudfile-helpers.js
+++ b/mail/test/mozmill/shared-modules/test-cloudfile-helpers.js
@@ -23,17 +23,17 @@ const kDefaults = {
   accountKey: null,
   settingsURL: "",
   managementURL: "",
 };
 
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
-var fdh, gMockCloudfileComponent;
+var cfh, 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(
@@ -42,68 +42,78 @@ function setupModule(module) {
       MockCloudfileAccount);
 }
 
 function installInto(module) {
   setupModule(module);
   module.gMockCloudfileManager = gMockCloudfileManager;
   module.MockCloudfileAccount = MockCloudfileAccount;
   module.getFile = getFile;
+  module.collectFiles = collectFiles;
 }
 
 
 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;
 }
 
+/**
+ * Helper function for getting the nsILocalFile's for some files located
+ * in a subdirectory of the test directory.
+ *
+ * @param aFiles an array of filename strings for files underneath the test
+ *               file directory.
+ * @param aFileRoot the file who's parent directory we should start looking
+ *                  for aFiles in.
+ *
+ * Example:
+ * let files = collectFiles(['./data/testFile1', './data/testFile2'],
+ *                          __file__);
+ */
+function collectFiles(aFiles, aFileRoot) {
+  return [getFile(filename, aFileRoot)
+          for each (filename in aFiles)]
+}
+
 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) {
+  uploadFile: function MCA_uploadFile(aFile, aListener) {
     aListener.onStartRequest(null, null);
     aListener.onStopRequest(null, null, Cr.NS_OK);
   },
 
-  urlForFile: function(aFile) {
+  urlForFile: function MCA_urlForFile(aFile) {
     return "http://www.example.com/download/someFile";
   },
 
-  refreshUserInfo: function(aWithUI, aCallback) {
+  refreshUserInfo: function MCA_refreshUserInfo(aWithUI,
+                                                aCallback) {
     aCallback.onStartRequest(null, null);
     aCallback.onStopRequest(null, null, Cr.NS_OK);
-  }
+  },
+
+  cancelFileUpload: function MCA_cancelFileUpload(aFile) {
+    throw Cr.NS_ERROR_NOT_IMPLEMENTED;
+  },
 };
 
 
-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, kMockContractID,
                                       false, true);
     gMockCloudfileComponent.register();
   },
--- a/mail/test/mozmill/shared-modules/test-cloudfile-yousendit-helpers.js
+++ b/mail/test/mozmill/shared-modules/test-cloudfile-yousendit-helpers.js
@@ -110,17 +110,17 @@ MockYouSendItServer.prototype = {
   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);
+                    .createInstance(Ci.nsIMsgCloudFileProvider);
 
     let urls = [kServerURL];
     yousendit.overrideUrls(urls.length, urls);
     yousendit.init(aAccountKey);
 
     return yousendit;
   },
 
@@ -162,18 +162,26 @@ MockYouSendItServer.prototype = {
   },
 
   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);
+  /**
+   * Prepare the mock server to have a file with filename aFilename be
+   * uploaded.
+   *
+   * @param aFilename the name of the file to be uploaded.
+   * @param aMSeconds an optional argument, for the amount of time the upload
+   *                  should take in milliseconds.
+   */
+  planForUploadFile: function MDBS_planForUploadFile(aFilename, aMSeconds) {
+    this.receiver.expect(aFilename, aMSeconds);
     let downloadUrl = kDownloadURLPrefix + "/" + aFilename;
     this.committer.prepareDownloadURL(aFilename, downloadUrl);
   },
 
   planForGetFileURL: function MDBS_planForGetShare(aFilename, aData) {
     this.committer.prepareDownloadURL(aFilename, aData.url);
   },
 
@@ -337,16 +345,18 @@ MockYouSendItPrepareSimple.prototype = {
   },
 
 };
 
 function MockYouSendItReceiverSimple(aYouSendIt) {
   this._server = null;
   this._ysi = aYouSendIt;
   this._expectedFiles = [];
+  this._mSeconds = {};
+  this._timers = [];
 }
 
 MockYouSendItReceiverSimple.prototype = {
   init: function(aServer) {
     this._server = aServer;
   },
 
   shutdown: function() {
@@ -361,35 +371,59 @@ MockYouSendItReceiverSimple.prototype = 
     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);
 
+    Services.obs.notifyObservers(null, "cloudfile:uploadStarted",
+                                 filename);
+
+    if (filename in this._mSeconds)
+      aResponse.processAsync();
+
     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);
+
+    if (filename in this._mSeconds) {
+      let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+      let timerEvent = {
+        notify: function(aTimer) {
+          aResponse.finish();
+        },
+      }
+      timer.initWithCallback(timerEvent, this._mSeconds[filename],
+                             Ci.nsITimer.TYPE_ONE_SHOT);
+      // This is kind of ridiculous, but it seems that we have to hold a
+      // reference to this timer, or else it can get garbage collected before
+      // it fires.
+      this._timers.push(timer);
+    }
   },
 
-  expect: function(aFilename) {
+  expect: function(aFilename, aMSeconds) {
     this._expectedFiles.push(aFilename);
+
+    if (aMSeconds)
+      this._mSeconds[aFilename] = aMSeconds;
   },
 
   get expecting() {
     return this._expectedFiles;
   }
 };
 
 function MockYouSendItCommitterSimple(aYouSendIt) {