Bug 1413685 - Unify AsyncResource and Resource. r?tcsc draft
authorEdouard Oger <eoger@fastmail.com>
Thu, 02 Nov 2017 14:30:59 -0400
changeset 692986 e6c0173c368f8babfe20361767fcc5e518b8f758
parent 692177 c019340bd9bd448054d593565079309106881c21
child 738899 b0ad753367fae6a8bf6346c98d954fb685045fa8
push id87654
push userbmo:eoger@fastmail.com
push dateFri, 03 Nov 2017 19:10:32 +0000
reviewerstcsc
bugs1413685
milestone58.0a1
Bug 1413685 - Unify AsyncResource and Resource. r?tcsc MozReview-Commit-ID: 1yEeAwpbGxJ
services/sync/modules/bookmark_repair.js
services/sync/modules/doctor.js
services/sync/modules/engines.js
services/sync/modules/engines/clients.js
services/sync/modules/record.js
services/sync/modules/resource.js
services/sync/modules/telemetry.js
services/sync/tests/unit/test_fxa_node_reassignment.js
services/sync/tests/unit/test_node_reassignment.js
services/sync/tests/unit/test_resource.js
services/sync/tests/unit/test_resource_async.js
services/sync/tests/unit/test_resource_ua.js
services/sync/tests/unit/test_telemetry.js
services/sync/tests/unit/test_warn_on_truncated_response.js
services/sync/tests/unit/xpcshell.ini
--- a/services/sync/modules/bookmark_repair.js
+++ b/services/sync/modules/bookmark_repair.js
@@ -552,17 +552,17 @@ class BookmarkRepairRequestor extends Co
     arrayOfClientIDs.push(clientID);
     this.prefs.set(PREF.REPAIR_PREVIOUS_CLIENTS, arrayOfClientIDs.join(" "));
   }
 
   /* Used for test mocks.
   */
   _now() {
     // We use the server time, which is SECONDS
-    return AsyncResource.serverTime;
+    return Resource.serverTime;
   }
 }
 
 /* An object that responds to repair requests initiated by some other device.
 */
 class BookmarkRepairResponder extends CollectionRepairResponder {
   async repair(request, rawCommand) {
     if (request.request != "upload") {
--- a/services/sync/modules/doctor.js
+++ b/services/sync/modules/doctor.js
@@ -242,17 +242,17 @@ this.Doctor = {
 
   set lastRepairAdvance(value) {
     Svc.Prefs.set("doctor.lastRepairAdvance", value);
   },
 
   // functions used so tests can mock them
   _now() {
     // We use the server time, which is SECONDS
-    return AsyncResource.serverTime;
+    return Resource.serverTime;
   },
 
   _getRepairRequestor(name) {
     return getRepairRequestor(name);
   },
 
   _getAllRepairRequestors() {
     return getAllRepairRequestors();
--- a/services/sync/modules/engines.js
+++ b/services/sync/modules/engines.js
@@ -1437,17 +1437,17 @@ SyncEngine.prototype = {
 
     // We start reconciling by collecting a bunch of state. We do this here
     // because some state may change during the course of this function and we
     // need to operate on the original values.
     let existsLocally   = await this._store.itemExists(item.id);
     let locallyModified = this._modified.has(item.id);
 
     // TODO Handle clock drift better. Tracked in bug 721181.
-    let remoteAge = AsyncResource.serverTime - item.modified;
+    let remoteAge = Resource.serverTime - item.modified;
     let localAge  = locallyModified ?
       (Date.now() / 1000 - this._modified.getModifiedTimestamp(item.id)) : null;
     let remoteIsNewer = remoteAge < localAge;
 
     this._log.trace("Reconciling " + item.id + ". exists=" +
                     existsLocally + "; modified=" + locallyModified +
                     "; local age=" + localAge + "; incoming age=" +
                     remoteAge);
--- a/services/sync/modules/engines/clients.js
+++ b/services/sync/modules/engines/clients.js
@@ -427,17 +427,17 @@ ClientEngine.prototype = {
         }
         if (!names.has(record.name)) {
           if (record.fxaDeviceId) {
             seenDeviceIds.add(record.fxaDeviceId);
           }
           names.add(record.name);
           continue;
         }
-        let remoteAge = AsyncResource.serverTime - this._incomingClients[id];
+        let remoteAge = Resource.serverTime - this._incomingClients[id];
         if (remoteAge > STALE_CLIENT_REMOTE_AGE) {
           this._log.info(`Hiding stale client ${id} with age ${remoteAge}`);
           record.stale = true;
           continue;
         }
         if (record.fxaDeviceId && seenDeviceIds.has(record.fxaDeviceId)) {
           this._log.info(`Hiding stale client ${record.id}` +
                          ` - duplicate device id ${record.fxaDeviceId}`);
--- a/services/sync/modules/record.js
+++ b/services/sync/modules/record.js
@@ -716,17 +716,17 @@ Collection.prototype = {
     if (batchSize <= 0 || batchSize >= totalLimit) {
       throw new Error("Invalid batch size");
     }
 
     if (!this.full) {
       throw new Error("getBatched is unimplemented for guid-only GETs");
     }
 
-    // _onComplete and _onProgress are reset after each `get` by AsyncResource.
+    // _onComplete and _onProgress are reset after each `get` by Resource.
     let { _onComplete, _onProgress } = this;
     let recordBuffer = [];
     let resp;
     try {
       let lastModifiedTime;
       this.limit = batchSize;
 
       do {
--- a/services/sync/modules/resource.js
+++ b/services/sync/modules/resource.js
@@ -1,21 +1,15 @@
 /* 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/. */
 
-this.EXPORTED_SYMBOLS = [
-  "AsyncResource",
-  "Resource"
-];
+this.EXPORTED_SYMBOLS = ["Resource"];
 
-var Cc = Components.classes;
-var Ci = Components.interfaces;
-var Cr = Components.results;
-var Cu = Components.utils;
+const {classes: Cc, interfaces: Ci, results: Cr, utils: Cu} = Components;
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/NetUtil.jsm");
 Cu.import("resource://gre/modules/Log.jsm");
 Cu.import("resource://services-common/async.js");
 Cu.import("resource://services-common/observers.js");
 Cu.import("resource://services-common/utils.js");
 Cu.import("resource://services-sync/constants.js");
@@ -24,20 +18,20 @@ Cu.import("resource://services-sync/util
 const DEFAULT_LOAD_FLAGS =
   // Always validate the cache:
   Ci.nsIRequest.LOAD_BYPASS_CACHE |
   Ci.nsIRequest.INHIBIT_CACHING |
   // Don't send user cookies over the wire (Bug 644734).
   Ci.nsIRequest.LOAD_ANONYMOUS;
 
 /*
- * AsyncResource represents a remote network resource, identified by a URI.
+ * Resource represents a remote network resource, identified by a URI.
  * Create an instance like so:
  *
- *   let resource = new AsyncResource("http://foobar.com/path/to/resource");
+ *   let resource = new Resource("http://foobar.com/path/to/resource");
  *
  * The 'resource' object has the following methods to issue HTTP requests
  * of the corresponding HTTP methods:
  *
  *   get(callback)
  *   put(data, callback)
  *   post(data, callback)
  *   delete(callback)
@@ -45,44 +39,44 @@ const DEFAULT_LOAD_FLAGS =
  * 'callback' is a function with the following signature:
  *
  *   function callback(error, result) {...}
  *
  * 'error' will be null on successful requests. Likewise, result will not be
  * passed (=undefined) when an error occurs. Note that this is independent of
  * the status of the HTTP response.
  */
-this.AsyncResource = function AsyncResource(uri) {
+this.Resource = function Resource(uri) {
   this._log = Log.repository.getLogger(this._logName);
   this._log.level =
     Log.Level[Svc.Prefs.get("log.logger.network.resources")];
   this.uri = uri;
   this._headers = {};
   this._onComplete = Utils.bind2(this, this._onComplete);
 };
-AsyncResource.prototype = {
-  _logName: "Sync.AsyncResource",
+Resource.prototype = {
+  _logName: "Sync.Resource",
 
-  // ** {{{ AsyncResource.serverTime }}} **
+  // ** {{{ Resource.serverTime }}} **
   //
   // Caches the latest server timestamp (X-Weave-Timestamp header).
   serverTime: null,
 
   /**
    * Callback to be invoked at request time to add authentication details.
    *
    * By default, a global authenticator is provided. If this is set, it will
    * be used instead of the global one.
    */
   authenticator: null,
 
   // Wait 5 minutes before killing a request.
   ABORT_TIMEOUT: 300000,
 
-  // ** {{{ AsyncResource.headers }}} **
+  // ** {{{ Resource.headers }}} **
   //
   // Headers to be included when making a request for the resource.
   // Note: Header names should be all lower case, there's no explicit
   // check for duplicates due to case!
   get headers() {
     return this._headers;
   },
   set headers(value) {
@@ -90,50 +84,50 @@ AsyncResource.prototype = {
   },
   setHeader: function Res_setHeader(header, value) {
     this._headers[header.toLowerCase()] = value;
   },
   get headerNames() {
     return Object.keys(this.headers);
   },
 
-  // ** {{{ AsyncResource.uri }}} **
+  // ** {{{ Resource.uri }}} **
   //
   // URI representing this resource.
   get uri() {
     return this._uri;
   },
   set uri(value) {
     if (typeof value == "string")
       this._uri = CommonUtils.makeURI(value);
     else
       this._uri = value;
   },
 
-  // ** {{{ AsyncResource.spec }}} **
+  // ** {{{ Resource.spec }}} **
   //
   // Get the string representation of the URI.
   get spec() {
     if (this._uri)
       return this._uri.spec;
     return null;
   },
 
-  // ** {{{ AsyncResource.data }}} **
+  // ** {{{ Resource.data }}} **
   //
   // Get and set the data encapulated in the resource.
   _data: null,
   get data() {
     return this._data;
   },
   set data(value) {
     this._data = value;
   },
 
-  // ** {{{ AsyncResource._createRequest }}} **
+  // ** {{{ Resource._createRequest }}} **
   //
   // This method returns a new IO Channel for requests to be made
   // through. It is never called directly, only {{{_doRequest}}} uses it
   // to obtain a request channel.
   //
   _createRequest(method) {
     this.method = method;
     let channel = NetUtil.newChannel({uri: this.spec, loadUsingSystemPrincipal: true})
@@ -343,20 +337,16 @@ AsyncResource.prototype = {
     return this._doRequest("POST", data);
   },
 
   delete() {
     return this._doRequest("DELETE", undefined);
   }
 };
 
-// TODO: We still export both "Resource" and "AsyncRecourse" as the same
-// object, but we should decide on one and unify all references.
-this.Resource = AsyncResource;
-
 // = ChannelListener =
 //
 // This object implements the {{{nsIStreamListener}}} interface
 // and is called as the network operation proceeds.
 function ChannelListener(onComplete, onProgress, logger, timeout) {
   this._onComplete = onComplete;
   this._onProgress = onProgress;
   this._log = logger;
@@ -373,17 +363,17 @@ ChannelListener.prototype = {
     } catch (ex) {
       this._log.error("Unexpected error: channel is not a nsIHttpChannel!");
       channel.cancel(Cr.NS_BINDING_ABORTED);
       return;
     }
 
     // Save the latest server timestamp when possible.
     try {
-      AsyncResource.serverTime = channel.getResponseHeader("X-Weave-Timestamp") - 0;
+      Resource.serverTime = channel.getResponseHeader("X-Weave-Timestamp") - 0;
     } catch (ex) {}
 
     this._log.trace("onStartRequest: " + channel.requestMethod + " " +
                     channel.URI.spec);
     this._data = "";
     this.delayAbort();
   },
 
--- a/services/sync/modules/telemetry.js
+++ b/services/sync/modules/telemetry.js
@@ -569,18 +569,18 @@ class SyncTelemetryImpl {
 
     if (!validateTelemetryEvent(eventDetails)) {
       // we've already logged what the problem is...
       return;
     }
     log.debug("recording event", eventDetails);
 
     let { object, method, value, extra } = eventDetails;
-    if (extra && AsyncResource.serverTime && !extra.serverTime) {
-      extra.serverTime = String(AsyncResource.serverTime);
+    if (extra && Resource.serverTime && !extra.serverTime) {
+      extra.serverTime = String(Resource.serverTime);
     }
     let category = "sync";
     let ts = Math.floor(tryGetMonotonicTimestamp());
 
     // An event record is a simple array with at least 4 items.
     let event = [ts, category, method, object];
     // It may have up to 6 elements if |extra| is defined
     if (value) {
--- a/services/sync/tests/unit/test_fxa_node_reassignment.js
+++ b/services/sync/tests/unit/test_fxa_node_reassignment.js
@@ -15,17 +15,16 @@ Cu.import("resource://services-sync/serv
 Cu.import("resource://services-sync/status.js");
 Cu.import("resource://services-sync/util.js");
 Cu.import("resource://testing-common/services/sync/rotaryengine.js");
 Cu.import("resource://services-sync/browserid_identity.js");
 Cu.import("resource://testing-common/services/sync/utils.js");
 Cu.import("resource://gre/modules/PromiseUtils.jsm");
 
 add_task(async function setup() {
-  Log.repository.getLogger("Sync.AsyncResource").level = Log.Level.Trace;
   Log.repository.getLogger("Sync.ErrorHandler").level  = Log.Level.Trace;
   Log.repository.getLogger("Sync.Resource").level      = Log.Level.Trace;
   Log.repository.getLogger("Sync.RESTRequest").level   = Log.Level.Trace;
   Log.repository.getLogger("Sync.Service").level       = Log.Level.Trace;
   Log.repository.getLogger("Sync.SyncScheduler").level = Log.Level.Trace;
   initTestLogging();
 
   // Disables all built-in engines. Important for avoiding errors thrown by the
--- a/services/sync/tests/unit/test_node_reassignment.js
+++ b/services/sync/tests/unit/test_node_reassignment.js
@@ -11,17 +11,16 @@ Cu.import("resource://services-sync/serv
 Cu.import("resource://services-sync/status.js");
 Cu.import("resource://services-sync/util.js");
 Cu.import("resource://testing-common/services/sync/rotaryengine.js");
 Cu.import("resource://testing-common/services/sync/utils.js");
 Cu.import("resource://gre/modules/PromiseUtils.jsm");
 
 
 add_task(async function setup() {
-  Log.repository.getLogger("Sync.AsyncResource").level = Log.Level.Trace;
   Log.repository.getLogger("Sync.ErrorHandler").level  = Log.Level.Trace;
   Log.repository.getLogger("Sync.Resource").level      = Log.Level.Trace;
   Log.repository.getLogger("Sync.RESTRequest").level   = Log.Level.Trace;
   Log.repository.getLogger("Sync.Service").level       = Log.Level.Trace;
   Log.repository.getLogger("Sync.SyncScheduler").level = Log.Level.Trace;
   initTestLogging();
   validate_all_future_pings();
 
--- a/services/sync/tests/unit/test_resource.js
+++ b/services/sync/tests/unit/test_resource.js
@@ -1,20 +1,21 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 Cu.import("resource://gre/modules/Log.jsm");
 Cu.import("resource://services-common/observers.js");
 Cu.import("resource://services-common/utils.js");
 Cu.import("resource://services-sync/resource.js");
-Cu.import("resource://services-sync/status.js");
 Cu.import("resource://services-sync/util.js");
 Cu.import("resource://services-sync/browserid_identity.js");
 Cu.import("resource://testing-common/services/sync/utils.js");
 
+var logger;
+
 var fetched = false;
 function server_open(metadata, response) {
   let body;
   if (metadata.method == "GET") {
     fetched = true;
     body = "This path exists";
     response.setStatusLine(metadata.httpVersion, 200, "OK");
   } else {
@@ -43,24 +44,24 @@ function server_protected(metadata, resp
 function server_404(metadata, response) {
   let body = "File not found";
   response.setStatusLine(metadata.httpVersion, 404, "Not Found");
   response.bodyOutputStream.write(body, body.length);
 }
 
 var pacFetched = false;
 function server_pac(metadata, response) {
+  _("Invoked PAC handler.");
   pacFetched = true;
   let body = 'function FindProxyForURL(url, host) { return "DIRECT"; }';
   response.setStatusLine(metadata.httpVersion, 200, "OK");
   response.setHeader("Content-Type", "application/x-ns-proxy-autoconfig", false);
   response.bodyOutputStream.write(body, body.length);
 }
 
-
 var sample_data = {
   some: "sample_data",
   injson: "format",
   number: 42
 };
 
 function server_upload(metadata, response) {
   let body;
@@ -144,355 +145,484 @@ function server_headers(metadata, respon
   for (let header of header_names) {
     headers[header] = metadata.getHeader(header);
   }
   let body = JSON.stringify(headers);
   response.setStatusLine(metadata.httpVersion, 200, "OK");
   response.bodyOutputStream.write(body, body.length);
 }
 
-add_task(async function test() {
-  initTestLogging("Trace");
+var quotaValue;
+Observers.add("weave:service:quota:remaining",
+              function(subject) { quotaValue = subject; });
 
-  let logger = Log.repository.getLogger("Test");
+function run_test() {
+  logger = Log.repository.getLogger("Test");
   Log.repository.rootLogger.addAppender(new Log.DumpAppender());
 
+  Svc.Prefs.set("network.numRetries", 1); // speed up test
+  run_next_test();
+}
+
+// This apparently has to come first in order for our PAC URL to be hit.
+// Don't put any other HTTP requests earlier in the file!
+add_task(async function test_proxy_auth_redirect() {
+  _("Ensure that a proxy auth redirect (which switches out our channel) " +
+    "doesn't break Resource.");
   let server = httpd_setup({
     "/open": server_open,
+    "/pac2": server_pac
+  });
+
+  PACSystemSettings.PACURI = server.baseURI + "/pac2";
+  installFakePAC();
+  let res = new Resource(server.baseURI + "/open");
+  let result = await res.get();
+  do_check_true(pacFetched);
+  do_check_true(fetched);
+  do_check_eq("This path exists", result);
+  pacFetched = fetched = false;
+  uninstallFakePAC();
+  await promiseStopServer(server);
+});
+
+add_task(async function test_new_channel() {
+  _("Ensure a redirect to a new channel is handled properly.");
+
+  let resourceRequested = false;
+  function resourceHandler(metadata, response) {
+    resourceRequested = true;
+
+    let body = "Test";
+    response.setHeader("Content-Type", "text/plain");
+    response.bodyOutputStream.write(body, body.length);
+  }
+
+  let locationURL;
+  function redirectHandler(metadata, response) {
+    let body = "Redirecting";
+    response.setStatusLine(metadata.httpVersion, 307, "TEMPORARY REDIRECT");
+    response.setHeader("Location", locationURL);
+    response.bodyOutputStream.write(body, body.length);
+  }
+
+  let server = httpd_setup({"/resource": resourceHandler,
+                            "/redirect": redirectHandler});
+  locationURL = server.baseURI + "/resource";
+
+  let request = new Resource(server.baseURI + "/redirect");
+  let content = await request.get();
+  do_check_true(resourceRequested);
+  do_check_eq(200, content.status);
+  do_check_true("content-type" in content.headers);
+  do_check_eq("text/plain", content.headers["content-type"]);
+
+  await promiseStopServer(server);
+});
+
+
+var server;
+
+add_test(function setup() {
+  server = httpd_setup({
+    "/open": server_open,
     "/protected": server_protected,
     "/404": server_404,
     "/upload": server_upload,
     "/delete": server_delete,
     "/json": server_json,
     "/timestamp": server_timestamp,
     "/headers": server_headers,
     "/backoff": server_backoff,
-    "/pac1": server_pac,
+    "/pac2": server_pac,
     "/quota-notice": server_quota_notice,
     "/quota-error": server_quota_error
   });
 
-  Svc.Prefs.set("network.numRetries", 1); // speed up test
+  run_next_test();
+});
 
-  // This apparently has to come first in order for our PAC URL to be hit.
-  // Don't put any other HTTP requests earlier in the file!
-  _("Testing handling of proxy auth redirection.");
-  PACSystemSettings.PACURI = server.baseURI + "/pac1";
-  installFakePAC();
-  let proxiedRes = new Resource(server.baseURI + "/open");
-  let content = await proxiedRes.get();
-  do_check_true(pacFetched);
-  do_check_true(fetched);
-  do_check_eq(content, "This path exists");
-  pacFetched = fetched = false;
-  uninstallFakePAC();
-
+add_test(function test_members() {
   _("Resource object members");
-  let res = new Resource(server.baseURI + "/open");
+  let uri = server.baseURI + "/open";
+  let res = new Resource(uri);
   do_check_true(res.uri instanceof Ci.nsIURI);
-  do_check_eq(res.uri.spec, server.baseURI + "/open");
-  do_check_eq(res.spec, server.baseURI + "/open");
+  do_check_eq(res.uri.spec, uri);
+  do_check_eq(res.spec, uri);
   do_check_eq(typeof res.headers, "object");
   do_check_eq(typeof res.authenticator, "object");
   // Initially res.data is null since we haven't performed a GET or
   // PUT/POST request yet.
   do_check_eq(res.data, null);
 
+  run_next_test();
+});
+
+add_task(async function test_get() {
   _("GET a non-password-protected resource");
-  content = await res.get();
+  let res = new Resource(server.baseURI + "/open");
+  let content = await res.get();
   do_check_eq(content, "This path exists");
   do_check_eq(content.status, 200);
   do_check_true(content.success);
   // res.data has been updated with the result from the request
   do_check_eq(res.data, content);
 
   // Observe logging messages.
-  logger = res._log;
-  let dbg    = logger.debug;
+  let resLogger = res._log;
+  let dbg    = resLogger.debug;
   let debugMessages = [];
-  logger.debug = function(msg) {
+  resLogger.debug = function(msg) {
     debugMessages.push(msg);
     dbg.call(this, msg);
   };
 
   // Since we didn't receive proper JSON data, accessing content.obj
-  // will result in a SyntaxError from JSON.parse.
-  // Furthermore, we'll have logged.
+  // will result in a SyntaxError from JSON.parse
   let didThrow = false;
   try {
     content.obj;
   } catch (ex) {
     didThrow = true;
   }
   do_check_true(didThrow);
   do_check_eq(debugMessages.length, 1);
   do_check_eq(debugMessages[0],
               "Parse fail: Response body starts: \"\"This path exists\"\".");
-  logger.debug = dbg;
+  resLogger.debug = dbg;
+});
 
+add_test(function test_basicauth() {
+  _("Test that the BasicAuthenticator doesn't screw up header case.");
+  let res1 = new Resource(server.baseURI + "/foo");
+  res1.setHeader("Authorization", "Basic foobar");
+  do_check_eq(res1._headers.authorization, "Basic foobar");
+  do_check_eq(res1.headers.authorization, "Basic foobar");
+
+  run_next_test();
+});
+
+add_task(async function test_get_protected_fail() {
   _("GET a password protected resource (test that it'll fail w/o pass, no throw)");
   let res2 = new Resource(server.baseURI + "/protected");
-  content = await res2.get();
+  let content = await res2.get();
   do_check_eq(content, "This path exists and is protected - failed");
   do_check_eq(content.status, 401);
   do_check_false(content.success);
+});
 
+add_task(async function test_get_protected_success() {
   _("GET a password protected resource");
-  let res3 = new Resource(server.baseURI + "/protected");
   let identityConfig = makeIdentityConfig();
-  let browseridManager = Status._authManager;
+  let browseridManager = new BrowserIDManager();
   configureFxAccountIdentity(browseridManager, identityConfig);
   let auth = browseridManager.getResourceAuthenticator();
+  let res3 = new Resource(server.baseURI + "/protected");
   res3.authenticator = auth;
   do_check_eq(res3.authenticator, auth);
-  content = await res3.get();
+  let content = await res3.get();
   do_check_eq(content, "This path exists and is protected");
   do_check_eq(content.status, 200);
   do_check_true(content.success);
+});
 
+add_task(async function test_get_404() {
   _("GET a non-existent resource (test that it'll fail, but not throw)");
   let res4 = new Resource(server.baseURI + "/404");
-  content = await res4.get();
+  let content = await res4.get();
   do_check_eq(content, "File not found");
   do_check_eq(content.status, 404);
   do_check_false(content.success);
 
   // Check some headers of the 404 response
   do_check_eq(content.headers.connection, "close");
   do_check_eq(content.headers.server, "httpd.js");
   do_check_eq(content.headers["content-length"], 14);
+});
 
+add_task(async function test_put_string() {
   _("PUT to a resource (string)");
-  let res5 = new Resource(server.baseURI + "/upload");
-  content = await res5.put(JSON.stringify(sample_data));
+  let res_upload = new Resource(server.baseURI + "/upload");
+  let content = await res_upload.put(JSON.stringify(sample_data));
   do_check_eq(content, "Valid data upload via PUT");
   do_check_eq(content.status, 200);
-  do_check_eq(res5.data, content);
+  do_check_eq(res_upload.data, content);
+});
 
+add_task(async function test_put_object() {
   _("PUT to a resource (object)");
-  content = await res5.put(sample_data);
+  let res_upload = new Resource(server.baseURI + "/upload");
+  let content = await res_upload.put(sample_data);
   do_check_eq(content, "Valid data upload via PUT");
   do_check_eq(content.status, 200);
-  do_check_eq(res5.data, content);
+  do_check_eq(res_upload.data, content);
+});
 
+add_task(async function test_put_data_string() {
   _("PUT without data arg (uses resource.data) (string)");
-  res5.data = JSON.stringify(sample_data);
-  content = await res5.put();
+  let res_upload = new Resource(server.baseURI + "/upload");
+  res_upload.data = JSON.stringify(sample_data);
+  let content = await res_upload.put();
   do_check_eq(content, "Valid data upload via PUT");
   do_check_eq(content.status, 200);
-  do_check_eq(res5.data, content);
+  do_check_eq(res_upload.data, content);
+});
 
+add_task(async function test_put_data_object() {
   _("PUT without data arg (uses resource.data) (object)");
-  res5.data = sample_data;
-  content = await res5.put();
+  let res_upload = new Resource(server.baseURI + "/upload");
+  res_upload.data = sample_data;
+  let content = await res_upload.put();
   do_check_eq(content, "Valid data upload via PUT");
   do_check_eq(content.status, 200);
-  do_check_eq(res5.data, content);
+  do_check_eq(res_upload.data, content);
+});
 
+add_task(async function test_post_string() {
   _("POST to a resource (string)");
-  content = await res5.post(JSON.stringify(sample_data));
+  let res_upload = new Resource(server.baseURI + "/upload");
+  let content = await res_upload.post(JSON.stringify(sample_data));
   do_check_eq(content, "Valid data upload via POST");
   do_check_eq(content.status, 200);
-  do_check_eq(res5.data, content);
+  do_check_eq(res_upload.data, content);
+});
 
+add_task(async function test_post_object() {
   _("POST to a resource (object)");
-  content = await res5.post(sample_data);
+  let res_upload = new Resource(server.baseURI + "/upload");
+  let content = await res_upload.post(sample_data);
   do_check_eq(content, "Valid data upload via POST");
   do_check_eq(content.status, 200);
-  do_check_eq(res5.data, content);
+  do_check_eq(res_upload.data, content);
+});
 
+add_task(async function test_post_data_string() {
   _("POST without data arg (uses resource.data) (string)");
-  res5.data = JSON.stringify(sample_data);
-  content = await res5.post();
+  let res_upload = new Resource(server.baseURI + "/upload");
+  res_upload.data = JSON.stringify(sample_data);
+  let content = await res_upload.post();
   do_check_eq(content, "Valid data upload via POST");
   do_check_eq(content.status, 200);
-  do_check_eq(res5.data, content);
+  do_check_eq(res_upload.data, content);
+});
 
+add_task(async function test_post_data_object() {
   _("POST without data arg (uses resource.data) (object)");
-  res5.data = sample_data;
-  content = await res5.post();
+  let res_upload = new Resource(server.baseURI + "/upload");
+  res_upload.data = sample_data;
+  let content = await res_upload.post();
   do_check_eq(content, "Valid data upload via POST");
   do_check_eq(content.status, 200);
-  do_check_eq(res5.data, content);
+  do_check_eq(res_upload.data, content);
+});
 
+add_task(async function test_delete() {
   _("DELETE a resource");
   let res6 = new Resource(server.baseURI + "/delete");
-  content = await res6.delete();
+  let content = await res6.delete();
   do_check_eq(content, "This resource has been deleted");
   do_check_eq(content.status, 200);
+});
 
+add_task(async function test_json_body() {
   _("JSON conversion of response body");
   let res7 = new Resource(server.baseURI + "/json");
-  content = await res7.get();
+  let content = await res7.get();
   do_check_eq(content, JSON.stringify(sample_data));
   do_check_eq(content.status, 200);
   do_check_eq(JSON.stringify(content.obj), JSON.stringify(sample_data));
+});
 
-  _("X-Weave-Timestamp header updates AsyncResource.serverTime");
+add_task(async function test_weave_timestamp() {
+  _("X-Weave-Timestamp header updates Resource.serverTime");
   // Before having received any response containing the
-  // X-Weave-Timestamp header, AsyncResource.serverTime is null.
-  do_check_eq(AsyncResource.serverTime, null);
+  // X-Weave-Timestamp header, Resource.serverTime is null.
+  do_check_eq(Resource.serverTime, null);
   let res8 = new Resource(server.baseURI + "/timestamp");
-  content = await res8.get();
-  do_check_eq(AsyncResource.serverTime, TIMESTAMP);
+  await res8.get();
+  do_check_eq(Resource.serverTime, TIMESTAMP);
+});
 
+add_task(async function test_get_no_headers() {
   _("GET: no special request headers");
-  let res9 = new Resource(server.baseURI + "/headers");
-  content = await res9.get();
+  let res_headers = new Resource(server.baseURI + "/headers");
+  let content = await res_headers.get();
   do_check_eq(content, "{}");
+});
 
+add_task(async function test_put_default_content_type() {
   _("PUT: Content-Type defaults to text/plain");
-  content = await res9.put("data");
+  let res_headers = new Resource(server.baseURI + "/headers");
+  let content = await res_headers.put("data");
   do_check_eq(content, JSON.stringify({"content-type": "text/plain"}));
+});
 
+add_task(async function test_post_default_content_type() {
   _("POST: Content-Type defaults to text/plain");
-  content = await res9.post("data");
+  let res_headers = new Resource(server.baseURI + "/headers");
+  let content = await res_headers.post("data");
   do_check_eq(content, JSON.stringify({"content-type": "text/plain"}));
+});
 
+add_task(async function test_setHeader() {
   _("setHeader(): setting simple header");
-  res9.setHeader("X-What-Is-Weave", "awesome");
-  do_check_eq(res9.headers["x-what-is-weave"], "awesome");
-  content = await res9.get();
+  let res_headers = new Resource(server.baseURI + "/headers");
+  res_headers.setHeader("X-What-Is-Weave", "awesome");
+  do_check_eq(res_headers.headers["x-what-is-weave"], "awesome");
+  let content = await res_headers.get();
   do_check_eq(content, JSON.stringify({"x-what-is-weave": "awesome"}));
+});
 
+add_task(async function test_setHeader_overwrite() {
   _("setHeader(): setting multiple headers, overwriting existing header");
-  res9.setHeader("X-WHAT-is-Weave", "more awesomer");
-  res9.setHeader("X-Another-Header", "hello world");
-  do_check_eq(res9.headers["x-what-is-weave"], "more awesomer");
-  do_check_eq(res9.headers["x-another-header"], "hello world");
-  content = await res9.get();
+  let res_headers = new Resource(server.baseURI + "/headers");
+  res_headers.setHeader("X-WHAT-is-Weave", "more awesomer");
+  res_headers.setHeader("X-Another-Header", "hello world");
+  do_check_eq(res_headers.headers["x-what-is-weave"], "more awesomer");
+  do_check_eq(res_headers.headers["x-another-header"], "hello world");
+  let content = await res_headers.get();
   do_check_eq(content, JSON.stringify({"x-another-header": "hello world",
                                        "x-what-is-weave": "more awesomer"}));
+});
 
+add_task(async function test_headers_object() {
   _("Setting headers object");
-  res9.headers = {};
-  content = await res9.get();
+  let res_headers = new Resource(server.baseURI + "/headers");
+  res_headers.headers = {};
+  let content = await res_headers.get();
   do_check_eq(content, "{}");
+});
 
-  _("PUT/POST: override default Content-Type");
-  res9.setHeader("Content-Type", "application/foobar");
-  do_check_eq(res9.headers["content-type"], "application/foobar");
-  content = await res9.put("data");
+add_task(async function test_put_override_content_type() {
+  _("PUT: override default Content-Type");
+  let res_headers = new Resource(server.baseURI + "/headers");
+  res_headers.setHeader("Content-Type", "application/foobar");
+  do_check_eq(res_headers.headers["content-type"], "application/foobar");
+  let content = await res_headers.put("data");
   do_check_eq(content, JSON.stringify({"content-type": "application/foobar"}));
-  content = await res9.post("data");
+});
+
+add_task(async function test_post_override_content_type() {
+  _("POST: override default Content-Type");
+  let res_headers = new Resource(server.baseURI + "/headers");
+  res_headers.setHeader("Content-Type", "application/foobar");
+  let content = await res_headers.post("data");
   do_check_eq(content, JSON.stringify({"content-type": "application/foobar"}));
+});
 
-
+add_task(async function test_weave_backoff() {
   _("X-Weave-Backoff header notifies observer");
   let backoffInterval;
   function onBackoff(subject, data) {
     backoffInterval = subject;
   }
   Observers.add("weave:service:backoff:interval", onBackoff);
 
   let res10 = new Resource(server.baseURI + "/backoff");
-  content = await res10.get();
+  await res10.get();
   do_check_eq(backoffInterval, 600);
+});
 
-
+add_task(async function test_quota_error() {
   _("X-Weave-Quota-Remaining header notifies observer on successful requests.");
-  let quotaValue;
-  function onQuota(subject, data) {
-    quotaValue = subject;
-  }
-  Observers.add("weave:service:quota:remaining", onQuota);
-
-  res10 = new Resource(server.baseURI + "/quota-error");
-  content = await res10.get();
+  let res10 = new Resource(server.baseURI + "/quota-error");
+  let content = await res10.get();
   do_check_eq(content.status, 400);
   do_check_eq(quotaValue, undefined); // HTTP 400, so no observer notification.
+});
 
-  res10 = new Resource(server.baseURI + "/quota-notice");
-  content = await res10.get();
+add_task(async function test_quota_notice() {
+  let res10 = new Resource(server.baseURI + "/quota-notice");
+  let content = await res10.get();
   do_check_eq(content.status, 200);
   do_check_eq(quotaValue, 1048576);
-
+});
 
-  _("Error handling in _request() preserves exception information");
-  let error;
+add_task(async function test_preserve_exceptions() {
+  _("Error handling in ChannelListener etc. preserves exception information");
   let res11 = new Resource("http://localhost:12345/does/not/exist");
-  try {
-    content = await res11.get();
-  } catch (ex) {
-    error = ex;
-  }
-  do_check_eq(error.result, Cr.NS_ERROR_CONNECTION_REFUSED);
-  do_check_eq(error.message, "NS_ERROR_CONNECTION_REFUSED");
-  do_check_eq(typeof error.stack, "string");
+  await Assert.rejects(res11.get(), error => {
+    do_check_neq(error, null);
+    do_check_eq(error.result, Cr.NS_ERROR_CONNECTION_REFUSED);
+    do_check_eq(error.message, "NS_ERROR_CONNECTION_REFUSED");
+    return true;
+  });
+});
 
-  _("Checking handling of errors in onProgress.");
-  let res18 = new Resource(server.baseURI + "/json");
-  let onProgress = function(rec) {
+add_task(async function test_xpc_exception_handling() {
+  _("Exception handling inside fetches.");
+  let res14 = new Resource(server.baseURI + "/json");
+  res14._onProgress = function(rec) {
     // Provoke an XPC exception without a Javascript wrapper.
     Services.io.newURI("::::::::");
   };
-  res18._onProgress = onProgress;
   let warnings = [];
-  res18._log.warn = function(msg) { warnings.push(msg); };
-  error = undefined;
-  try {
-    content = await res18.get();
-  } catch (ex) {
-    error = ex;
-  }
+  res14._log.warn = function(msg) { warnings.push(msg); };
 
-  // It throws and logs.
-  do_check_eq(error.result, Cr.NS_ERROR_MALFORMED_URI);
-  do_check_eq(error.message, "NS_ERROR_MALFORMED_URI");
-  // Note the strings haven't been formatted yet, but that's OK for this test.
-  do_check_eq(warnings.pop(), "${action} request to ${url} failed: ${ex}");
+  await Assert.rejects(res14.get(), error => {
+    do_check_eq(error.result, Cr.NS_ERROR_MALFORMED_URI);
+    do_check_eq(error.message, "NS_ERROR_MALFORMED_URI");
+    return true;
+  });
+  do_check_eq(warnings.pop(),
+              "${action} request to ${url} failed: ${ex}");
   do_check_eq(warnings.pop(),
               "Got exception calling onProgress handler during fetch of " +
               server.baseURI + "/json");
+});
 
-  // And this is what happens if JS throws an exception.
-  res18 = new Resource(server.baseURI + "/json");
-  onProgress = function(rec) {
+add_task(async function test_js_exception_handling() {
+  _("JS exception handling inside fetches.");
+  let res15 = new Resource(server.baseURI + "/json");
+  res15._onProgress = function(rec) {
     throw new Error("BOO!");
   };
-  res18._onProgress = onProgress;
-  let oldWarn = res18._log.warn;
-  warnings = [];
-  res18._log.warn = function(msg) { warnings.push(msg); };
-  error = undefined;
-  try {
-    content = await res18.get();
-  } catch (ex) {
-    error = ex;
-  }
+  let warnings = [];
+  res15._log.warn = function(msg) { warnings.push(msg); };
 
-  // It throws and logs.
-  do_check_eq(error.result, Cr.NS_ERROR_XPC_JAVASCRIPT_ERROR_WITH_DETAILS);
-  do_check_eq(error.message, "NS_ERROR_XPC_JAVASCRIPT_ERROR_WITH_DETAILS");
-  do_check_eq(warnings.pop(), "${action} request to ${url} failed: ${ex}");
+  await Assert.rejects(res15.get(), error => {
+    do_check_eq(error.result, Cr.NS_ERROR_XPC_JAVASCRIPT_ERROR_WITH_DETAILS);
+    do_check_eq(error.message, "NS_ERROR_XPC_JAVASCRIPT_ERROR_WITH_DETAILS");
+    return true;
+  });
+  do_check_eq(warnings.pop(),
+              "${action} request to ${url} failed: ${ex}");
   do_check_eq(warnings.pop(),
               "Got exception calling onProgress handler during fetch of " +
               server.baseURI + "/json");
+});
 
-  res18._log.warn = oldWarn;
-
+add_task(async function test_timeout() {
   _("Ensure channel timeouts are thrown appropriately.");
   let res19 = new Resource(server.baseURI + "/json");
   res19.ABORT_TIMEOUT = 0;
-  error = undefined;
-  try {
-    content = await res19.get();
-  } catch (ex) {
-    error = ex;
-  }
-  do_check_eq(error.result, Cr.NS_ERROR_NET_TIMEOUT);
+  await Assert.rejects(res19.get(), error => {
+    do_check_eq(error.result, Cr.NS_ERROR_NET_TIMEOUT);
+    return true;
+  });
+});
 
+add_test(function test_uri_construction() {
   _("Testing URI construction.");
   let args = [];
   args.push("newer=" + 1234);
   args.push("limit=" + 1234);
   args.push("sort=" + 1234);
 
   let query = "?" + args.join("&");
 
   let uri1 = CommonUtils.makeURI("http://foo/" + query)
                   .QueryInterface(Ci.nsIURL);
   let uri2 = CommonUtils.makeURI("http://foo/")
                   .QueryInterface(Ci.nsIURL);
   uri2.query = query;
   do_check_eq(uri1.query, uri2.query);
-  server.stop(do_test_finished);
+
+  run_next_test();
 });
+
+/**
+ * End of tests that rely on a single HTTP server.
+ * All tests after this point must begin and end their own.
+ */
+add_test(function eliminate_server() {
+  server.stop(run_next_test);
+});
deleted file mode 100644
--- a/services/sync/tests/unit/test_resource_async.js
+++ /dev/null
@@ -1,628 +0,0 @@
-/* Any copyright is dedicated to the Public Domain.
- * http://creativecommons.org/publicdomain/zero/1.0/ */
-
-Cu.import("resource://gre/modules/Log.jsm");
-Cu.import("resource://services-common/observers.js");
-Cu.import("resource://services-common/utils.js");
-Cu.import("resource://services-sync/resource.js");
-Cu.import("resource://services-sync/util.js");
-Cu.import("resource://services-sync/browserid_identity.js");
-Cu.import("resource://testing-common/services/sync/utils.js");
-
-var logger;
-
-var fetched = false;
-function server_open(metadata, response) {
-  let body;
-  if (metadata.method == "GET") {
-    fetched = true;
-    body = "This path exists";
-    response.setStatusLine(metadata.httpVersion, 200, "OK");
-  } else {
-    body = "Wrong request method";
-    response.setStatusLine(metadata.httpVersion, 405, "Method Not Allowed");
-  }
-  response.bodyOutputStream.write(body, body.length);
-}
-
-function server_protected(metadata, response) {
-  let body;
-
-  if (has_hawk_header(metadata, "guest", "guest")) {
-    body = "This path exists and is protected";
-    response.setStatusLine(metadata.httpVersion, 200, "OK, authorized");
-    response.setHeader("WWW-Authenticate", 'Basic realm="secret"', false);
-  } else {
-    body = "This path exists and is protected - failed";
-    response.setStatusLine(metadata.httpVersion, 401, "Unauthorized");
-    response.setHeader("WWW-Authenticate", 'Basic realm="secret"', false);
-  }
-
-  response.bodyOutputStream.write(body, body.length);
-}
-
-function server_404(metadata, response) {
-  let body = "File not found";
-  response.setStatusLine(metadata.httpVersion, 404, "Not Found");
-  response.bodyOutputStream.write(body, body.length);
-}
-
-var pacFetched = false;
-function server_pac(metadata, response) {
-  _("Invoked PAC handler.");
-  pacFetched = true;
-  let body = 'function FindProxyForURL(url, host) { return "DIRECT"; }';
-  response.setStatusLine(metadata.httpVersion, 200, "OK");
-  response.setHeader("Content-Type", "application/x-ns-proxy-autoconfig", false);
-  response.bodyOutputStream.write(body, body.length);
-}
-
-var sample_data = {
-  some: "sample_data",
-  injson: "format",
-  number: 42
-};
-
-function server_upload(metadata, response) {
-  let body;
-
-  let input = readBytesFromInputStream(metadata.bodyInputStream);
-  if (input == JSON.stringify(sample_data)) {
-    body = "Valid data upload via " + metadata.method;
-    response.setStatusLine(metadata.httpVersion, 200, "OK");
-  } else {
-    body = "Invalid data upload via " + metadata.method + ": " + input;
-    response.setStatusLine(metadata.httpVersion, 500, "Internal Server Error");
-  }
-
-  response.bodyOutputStream.write(body, body.length);
-}
-
-function server_delete(metadata, response) {
-  let body;
-  if (metadata.method == "DELETE") {
-    body = "This resource has been deleted";
-    response.setStatusLine(metadata.httpVersion, 200, "OK");
-  } else {
-    body = "Wrong request method";
-    response.setStatusLine(metadata.httpVersion, 405, "Method Not Allowed");
-  }
-  response.bodyOutputStream.write(body, body.length);
-}
-
-function server_json(metadata, response) {
-  let body = JSON.stringify(sample_data);
-  response.setStatusLine(metadata.httpVersion, 200, "OK");
-  response.bodyOutputStream.write(body, body.length);
-}
-
-const TIMESTAMP = 1274380461;
-
-function server_timestamp(metadata, response) {
-  let body = "Thank you for your request";
-  response.setHeader("X-Weave-Timestamp", "" + TIMESTAMP, false);
-  response.setStatusLine(metadata.httpVersion, 200, "OK");
-  response.bodyOutputStream.write(body, body.length);
-}
-
-function server_backoff(metadata, response) {
-  let body = "Hey, back off!";
-  response.setHeader("X-Weave-Backoff", "600", false);
-  response.setStatusLine(metadata.httpVersion, 200, "OK");
-  response.bodyOutputStream.write(body, body.length);
-}
-
-function server_quota_notice(request, response) {
-  let body = "You're approaching quota.";
-  response.setHeader("X-Weave-Quota-Remaining", "1048576", false);
-  response.setStatusLine(request.httpVersion, 200, "OK");
-  response.bodyOutputStream.write(body, body.length);
-}
-
-function server_quota_error(request, response) {
-  let body = "14";
-  response.setHeader("X-Weave-Quota-Remaining", "-1024", false);
-  response.setStatusLine(request.httpVersion, 400, "OK");
-  response.bodyOutputStream.write(body, body.length);
-}
-
-function server_headers(metadata, response) {
-  let ignore_headers = ["host", "user-agent", "accept", "accept-language",
-                        "accept-encoding", "accept-charset", "keep-alive",
-                        "connection", "pragma", "cache-control",
-                        "content-length"];
-  let headers = metadata.headers;
-  let header_names = [];
-  while (headers.hasMoreElements()) {
-    let header = headers.getNext().toString();
-    if (ignore_headers.indexOf(header) == -1) {
-      header_names.push(header);
-    }
-  }
-  header_names = header_names.sort();
-
-  headers = {};
-  for (let header of header_names) {
-    headers[header] = metadata.getHeader(header);
-  }
-  let body = JSON.stringify(headers);
-  response.setStatusLine(metadata.httpVersion, 200, "OK");
-  response.bodyOutputStream.write(body, body.length);
-}
-
-var quotaValue;
-Observers.add("weave:service:quota:remaining",
-              function(subject) { quotaValue = subject; });
-
-function run_test() {
-  logger = Log.repository.getLogger("Test");
-  Log.repository.rootLogger.addAppender(new Log.DumpAppender());
-
-  Svc.Prefs.set("network.numRetries", 1); // speed up test
-  run_next_test();
-}
-
-// This apparently has to come first in order for our PAC URL to be hit.
-// Don't put any other HTTP requests earlier in the file!
-add_task(async function test_proxy_auth_redirect() {
-  _("Ensure that a proxy auth redirect (which switches out our channel) " +
-    "doesn't break AsyncResource.");
-  let server = httpd_setup({
-    "/open": server_open,
-    "/pac2": server_pac
-  });
-
-  PACSystemSettings.PACURI = server.baseURI + "/pac2";
-  installFakePAC();
-  let res = new AsyncResource(server.baseURI + "/open");
-  let result = await res.get();
-  do_check_true(pacFetched);
-  do_check_true(fetched);
-  do_check_eq("This path exists", result);
-  pacFetched = fetched = false;
-  uninstallFakePAC();
-  await promiseStopServer(server);
-});
-
-add_task(async function test_new_channel() {
-  _("Ensure a redirect to a new channel is handled properly.");
-
-  let resourceRequested = false;
-  function resourceHandler(metadata, response) {
-    resourceRequested = true;
-
-    let body = "Test";
-    response.setHeader("Content-Type", "text/plain");
-    response.bodyOutputStream.write(body, body.length);
-  }
-
-  let locationURL;
-  function redirectHandler(metadata, response) {
-    let body = "Redirecting";
-    response.setStatusLine(metadata.httpVersion, 307, "TEMPORARY REDIRECT");
-    response.setHeader("Location", locationURL);
-    response.bodyOutputStream.write(body, body.length);
-  }
-
-  let server = httpd_setup({"/resource": resourceHandler,
-                            "/redirect": redirectHandler});
-  locationURL = server.baseURI + "/resource";
-
-  let request = new AsyncResource(server.baseURI + "/redirect");
-  let content = await request.get();
-  do_check_true(resourceRequested);
-  do_check_eq(200, content.status);
-  do_check_true("content-type" in content.headers);
-  do_check_eq("text/plain", content.headers["content-type"]);
-
-  await promiseStopServer(server);
-});
-
-
-var server;
-
-add_test(function setup() {
-  server = httpd_setup({
-    "/open": server_open,
-    "/protected": server_protected,
-    "/404": server_404,
-    "/upload": server_upload,
-    "/delete": server_delete,
-    "/json": server_json,
-    "/timestamp": server_timestamp,
-    "/headers": server_headers,
-    "/backoff": server_backoff,
-    "/pac2": server_pac,
-    "/quota-notice": server_quota_notice,
-    "/quota-error": server_quota_error
-  });
-
-  run_next_test();
-});
-
-add_test(function test_members() {
-  _("Resource object members");
-  let uri = server.baseURI + "/open";
-  let res = new AsyncResource(uri);
-  do_check_true(res.uri instanceof Ci.nsIURI);
-  do_check_eq(res.uri.spec, uri);
-  do_check_eq(res.spec, uri);
-  do_check_eq(typeof res.headers, "object");
-  do_check_eq(typeof res.authenticator, "object");
-  // Initially res.data is null since we haven't performed a GET or
-  // PUT/POST request yet.
-  do_check_eq(res.data, null);
-
-  run_next_test();
-});
-
-add_task(async function test_get() {
-  _("GET a non-password-protected resource");
-  let res = new AsyncResource(server.baseURI + "/open");
-  let content = await res.get();
-  do_check_eq(content, "This path exists");
-  do_check_eq(content.status, 200);
-  do_check_true(content.success);
-  // res.data has been updated with the result from the request
-  do_check_eq(res.data, content);
-
-  // Observe logging messages.
-  let resLogger = res._log;
-  let dbg    = resLogger.debug;
-  let debugMessages = [];
-  resLogger.debug = function(msg) {
-    debugMessages.push(msg);
-    dbg.call(this, msg);
-  };
-
-  // Since we didn't receive proper JSON data, accessing content.obj
-  // will result in a SyntaxError from JSON.parse
-  let didThrow = false;
-  try {
-    content.obj;
-  } catch (ex) {
-    didThrow = true;
-  }
-  do_check_true(didThrow);
-  do_check_eq(debugMessages.length, 1);
-  do_check_eq(debugMessages[0],
-              "Parse fail: Response body starts: \"\"This path exists\"\".");
-  resLogger.debug = dbg;
-});
-
-add_test(function test_basicauth() {
-  _("Test that the BasicAuthenticator doesn't screw up header case.");
-  let res1 = new AsyncResource(server.baseURI + "/foo");
-  res1.setHeader("Authorization", "Basic foobar");
-  do_check_eq(res1._headers.authorization, "Basic foobar");
-  do_check_eq(res1.headers.authorization, "Basic foobar");
-
-  run_next_test();
-});
-
-add_task(async function test_get_protected_fail() {
-  _("GET a password protected resource (test that it'll fail w/o pass, no throw)");
-  let res2 = new AsyncResource(server.baseURI + "/protected");
-  let content = await res2.get();
-  do_check_eq(content, "This path exists and is protected - failed");
-  do_check_eq(content.status, 401);
-  do_check_false(content.success);
-});
-
-add_task(async function test_get_protected_success() {
-  _("GET a password protected resource");
-  let identityConfig = makeIdentityConfig();
-  let browseridManager = new BrowserIDManager();
-  configureFxAccountIdentity(browseridManager, identityConfig);
-  let auth = browseridManager.getResourceAuthenticator();
-  let res3 = new AsyncResource(server.baseURI + "/protected");
-  res3.authenticator = auth;
-  do_check_eq(res3.authenticator, auth);
-  let content = await res3.get();
-  do_check_eq(content, "This path exists and is protected");
-  do_check_eq(content.status, 200);
-  do_check_true(content.success);
-});
-
-add_task(async function test_get_404() {
-  _("GET a non-existent resource (test that it'll fail, but not throw)");
-  let res4 = new AsyncResource(server.baseURI + "/404");
-  let content = await res4.get();
-  do_check_eq(content, "File not found");
-  do_check_eq(content.status, 404);
-  do_check_false(content.success);
-
-  // Check some headers of the 404 response
-  do_check_eq(content.headers.connection, "close");
-  do_check_eq(content.headers.server, "httpd.js");
-  do_check_eq(content.headers["content-length"], 14);
-});
-
-add_task(async function test_put_string() {
-  _("PUT to a resource (string)");
-  let res_upload = new AsyncResource(server.baseURI + "/upload");
-  let content = await res_upload.put(JSON.stringify(sample_data));
-  do_check_eq(content, "Valid data upload via PUT");
-  do_check_eq(content.status, 200);
-  do_check_eq(res_upload.data, content);
-});
-
-add_task(async function test_put_object() {
-  _("PUT to a resource (object)");
-  let res_upload = new AsyncResource(server.baseURI + "/upload");
-  let content = await res_upload.put(sample_data);
-  do_check_eq(content, "Valid data upload via PUT");
-  do_check_eq(content.status, 200);
-  do_check_eq(res_upload.data, content);
-});
-
-add_task(async function test_put_data_string() {
-  _("PUT without data arg (uses resource.data) (string)");
-  let res_upload = new AsyncResource(server.baseURI + "/upload");
-  res_upload.data = JSON.stringify(sample_data);
-  let content = await res_upload.put();
-  do_check_eq(content, "Valid data upload via PUT");
-  do_check_eq(content.status, 200);
-  do_check_eq(res_upload.data, content);
-});
-
-add_task(async function test_put_data_object() {
-  _("PUT without data arg (uses resource.data) (object)");
-  let res_upload = new AsyncResource(server.baseURI + "/upload");
-  res_upload.data = sample_data;
-  let content = await res_upload.put();
-  do_check_eq(content, "Valid data upload via PUT");
-  do_check_eq(content.status, 200);
-  do_check_eq(res_upload.data, content);
-});
-
-add_task(async function test_post_string() {
-  _("POST to a resource (string)");
-  let res_upload = new AsyncResource(server.baseURI + "/upload");
-  let content = await res_upload.post(JSON.stringify(sample_data));
-  do_check_eq(content, "Valid data upload via POST");
-  do_check_eq(content.status, 200);
-  do_check_eq(res_upload.data, content);
-});
-
-add_task(async function test_post_object() {
-  _("POST to a resource (object)");
-  let res_upload = new AsyncResource(server.baseURI + "/upload");
-  let content = await res_upload.post(sample_data);
-  do_check_eq(content, "Valid data upload via POST");
-  do_check_eq(content.status, 200);
-  do_check_eq(res_upload.data, content);
-});
-
-add_task(async function test_post_data_string() {
-  _("POST without data arg (uses resource.data) (string)");
-  let res_upload = new AsyncResource(server.baseURI + "/upload");
-  res_upload.data = JSON.stringify(sample_data);
-  let content = await res_upload.post();
-  do_check_eq(content, "Valid data upload via POST");
-  do_check_eq(content.status, 200);
-  do_check_eq(res_upload.data, content);
-});
-
-add_task(async function test_post_data_object() {
-  _("POST without data arg (uses resource.data) (object)");
-  let res_upload = new AsyncResource(server.baseURI + "/upload");
-  res_upload.data = sample_data;
-  let content = await res_upload.post();
-  do_check_eq(content, "Valid data upload via POST");
-  do_check_eq(content.status, 200);
-  do_check_eq(res_upload.data, content);
-});
-
-add_task(async function test_delete() {
-  _("DELETE a resource");
-  let res6 = new AsyncResource(server.baseURI + "/delete");
-  let content = await res6.delete();
-  do_check_eq(content, "This resource has been deleted");
-  do_check_eq(content.status, 200);
-});
-
-add_task(async function test_json_body() {
-  _("JSON conversion of response body");
-  let res7 = new AsyncResource(server.baseURI + "/json");
-  let content = await res7.get();
-  do_check_eq(content, JSON.stringify(sample_data));
-  do_check_eq(content.status, 200);
-  do_check_eq(JSON.stringify(content.obj), JSON.stringify(sample_data));
-});
-
-add_task(async function test_weave_timestamp() {
-  _("X-Weave-Timestamp header updates AsyncResource.serverTime");
-  // Before having received any response containing the
-  // X-Weave-Timestamp header, AsyncResource.serverTime is null.
-  do_check_eq(AsyncResource.serverTime, null);
-  let res8 = new AsyncResource(server.baseURI + "/timestamp");
-  await res8.get();
-  do_check_eq(AsyncResource.serverTime, TIMESTAMP);
-});
-
-add_task(async function test_get_no_headers() {
-  _("GET: no special request headers");
-  let res_headers = new AsyncResource(server.baseURI + "/headers");
-  let content = await res_headers.get();
-  do_check_eq(content, "{}");
-});
-
-add_task(async function test_put_default_content_type() {
-  _("PUT: Content-Type defaults to text/plain");
-  let res_headers = new AsyncResource(server.baseURI + "/headers");
-  let content = await res_headers.put("data");
-  do_check_eq(content, JSON.stringify({"content-type": "text/plain"}));
-});
-
-add_task(async function test_post_default_content_type() {
-  _("POST: Content-Type defaults to text/plain");
-  let res_headers = new AsyncResource(server.baseURI + "/headers");
-  let content = await res_headers.post("data");
-  do_check_eq(content, JSON.stringify({"content-type": "text/plain"}));
-});
-
-add_task(async function test_setHeader() {
-  _("setHeader(): setting simple header");
-  let res_headers = new AsyncResource(server.baseURI + "/headers");
-  res_headers.setHeader("X-What-Is-Weave", "awesome");
-  do_check_eq(res_headers.headers["x-what-is-weave"], "awesome");
-  let content = await res_headers.get();
-  do_check_eq(content, JSON.stringify({"x-what-is-weave": "awesome"}));
-});
-
-add_task(async function test_setHeader_overwrite() {
-  _("setHeader(): setting multiple headers, overwriting existing header");
-  let res_headers = new AsyncResource(server.baseURI + "/headers");
-  res_headers.setHeader("X-WHAT-is-Weave", "more awesomer");
-  res_headers.setHeader("X-Another-Header", "hello world");
-  do_check_eq(res_headers.headers["x-what-is-weave"], "more awesomer");
-  do_check_eq(res_headers.headers["x-another-header"], "hello world");
-  let content = await res_headers.get();
-  do_check_eq(content, JSON.stringify({"x-another-header": "hello world",
-                                       "x-what-is-weave": "more awesomer"}));
-});
-
-add_task(async function test_headers_object() {
-  _("Setting headers object");
-  let res_headers = new AsyncResource(server.baseURI + "/headers");
-  res_headers.headers = {};
-  let content = await res_headers.get();
-  do_check_eq(content, "{}");
-});
-
-add_task(async function test_put_override_content_type() {
-  _("PUT: override default Content-Type");
-  let res_headers = new AsyncResource(server.baseURI + "/headers");
-  res_headers.setHeader("Content-Type", "application/foobar");
-  do_check_eq(res_headers.headers["content-type"], "application/foobar");
-  let content = await res_headers.put("data");
-  do_check_eq(content, JSON.stringify({"content-type": "application/foobar"}));
-});
-
-add_task(async function test_post_override_content_type() {
-  _("POST: override default Content-Type");
-  let res_headers = new AsyncResource(server.baseURI + "/headers");
-  res_headers.setHeader("Content-Type", "application/foobar");
-  let content = await res_headers.post("data");
-  do_check_eq(content, JSON.stringify({"content-type": "application/foobar"}));
-});
-
-add_task(async function test_weave_backoff() {
-  _("X-Weave-Backoff header notifies observer");
-  let backoffInterval;
-  function onBackoff(subject, data) {
-    backoffInterval = subject;
-  }
-  Observers.add("weave:service:backoff:interval", onBackoff);
-
-  let res10 = new AsyncResource(server.baseURI + "/backoff");
-  await res10.get();
-  do_check_eq(backoffInterval, 600);
-});
-
-add_task(async function test_quota_error() {
-  _("X-Weave-Quota-Remaining header notifies observer on successful requests.");
-  let res10 = new AsyncResource(server.baseURI + "/quota-error");
-  let content = await res10.get();
-  do_check_eq(content.status, 400);
-  do_check_eq(quotaValue, undefined); // HTTP 400, so no observer notification.
-});
-
-add_task(async function test_quota_notice() {
-  let res10 = new AsyncResource(server.baseURI + "/quota-notice");
-  let content = await res10.get();
-  do_check_eq(content.status, 200);
-  do_check_eq(quotaValue, 1048576);
-});
-
-add_task(async function test_preserve_exceptions() {
-  _("Error handling in ChannelListener etc. preserves exception information");
-  let res11 = new AsyncResource("http://localhost:12345/does/not/exist");
-  await Assert.rejects(res11.get(), error => {
-    do_check_neq(error, null);
-    do_check_eq(error.result, Cr.NS_ERROR_CONNECTION_REFUSED);
-    do_check_eq(error.message, "NS_ERROR_CONNECTION_REFUSED");
-    return true;
-  });
-});
-
-add_task(async function test_xpc_exception_handling() {
-  _("Exception handling inside fetches.");
-  let res14 = new AsyncResource(server.baseURI + "/json");
-  res14._onProgress = function(rec) {
-    // Provoke an XPC exception without a Javascript wrapper.
-    Services.io.newURI("::::::::");
-  };
-  let warnings = [];
-  res14._log.warn = function(msg) { warnings.push(msg); };
-
-  await Assert.rejects(res14.get(), error => {
-    do_check_eq(error.result, Cr.NS_ERROR_MALFORMED_URI);
-    do_check_eq(error.message, "NS_ERROR_MALFORMED_URI");
-    return true;
-  });
-  do_check_eq(warnings.pop(),
-              "${action} request to ${url} failed: ${ex}");
-  do_check_eq(warnings.pop(),
-              "Got exception calling onProgress handler during fetch of " +
-              server.baseURI + "/json");
-});
-
-add_task(async function test_js_exception_handling() {
-  _("JS exception handling inside fetches.");
-  let res15 = new AsyncResource(server.baseURI + "/json");
-  res15._onProgress = function(rec) {
-    throw new Error("BOO!");
-  };
-  let warnings = [];
-  res15._log.warn = function(msg) { warnings.push(msg); };
-
-  await Assert.rejects(res15.get(), error => {
-    do_check_eq(error.result, Cr.NS_ERROR_XPC_JAVASCRIPT_ERROR_WITH_DETAILS);
-    do_check_eq(error.message, "NS_ERROR_XPC_JAVASCRIPT_ERROR_WITH_DETAILS");
-    return true;
-  });
-  do_check_eq(warnings.pop(),
-              "${action} request to ${url} failed: ${ex}");
-  do_check_eq(warnings.pop(),
-              "Got exception calling onProgress handler during fetch of " +
-              server.baseURI + "/json");
-});
-
-add_task(async function test_timeout() {
-  _("Ensure channel timeouts are thrown appropriately.");
-  let res19 = new AsyncResource(server.baseURI + "/json");
-  res19.ABORT_TIMEOUT = 0;
-  await Assert.rejects(res19.get(), error => {
-    do_check_eq(error.result, Cr.NS_ERROR_NET_TIMEOUT);
-    return true;
-  });
-});
-
-add_test(function test_uri_construction() {
-  _("Testing URI construction.");
-  let args = [];
-  args.push("newer=" + 1234);
-  args.push("limit=" + 1234);
-  args.push("sort=" + 1234);
-
-  let query = "?" + args.join("&");
-
-  let uri1 = CommonUtils.makeURI("http://foo/" + query)
-                  .QueryInterface(Ci.nsIURL);
-  let uri2 = CommonUtils.makeURI("http://foo/")
-                  .QueryInterface(Ci.nsIURL);
-  uri2.query = query;
-  do_check_eq(uri1.query, uri2.query);
-
-  run_next_test();
-});
-
-/**
- * End of tests that rely on a single HTTP server.
- * All tests after this point must begin and end their own.
- */
-add_test(function eliminate_server() {
-  server.stop(run_next_test);
-});
--- a/services/sync/tests/unit/test_resource_ua.js
+++ b/services/sync/tests/unit/test_resource_ua.js
@@ -53,37 +53,37 @@ add_task(async function test_fetchInfo()
   await Service._fetchInfo();
   _("User-Agent: " + ua);
   do_check_eq(ua, expectedUA + ".desktop");
   ua = "";
 });
 
 add_task(async function test_desktop_post() {
   _("Testing direct Resource POST.");
-  let r = new AsyncResource(server.baseURI + "/1.1/johndoe/storage/meta/global");
+  let r = new Resource(server.baseURI + "/1.1/johndoe/storage/meta/global");
   await r.post("foo=bar");
   _("User-Agent: " + ua);
   do_check_eq(ua, expectedUA + ".desktop");
   ua = "";
 });
 
 add_task(async function test_desktop_get() {
   _("Testing async.");
   Svc.Prefs.set("client.type", "desktop");
-  let r = new AsyncResource(server.baseURI + "/1.1/johndoe/storage/meta/global");
+  let r = new Resource(server.baseURI + "/1.1/johndoe/storage/meta/global");
   await r.get();
   _("User-Agent: " + ua);
   do_check_eq(ua, expectedUA + ".desktop");
   ua = "";
 });
 
 add_task(async function test_mobile_get() {
   _("Testing mobile.");
   Svc.Prefs.set("client.type", "mobile");
-  let r = new AsyncResource(server.baseURI + "/1.1/johndoe/storage/meta/global");
+  let r = new Resource(server.baseURI + "/1.1/johndoe/storage/meta/global");
   await r.get();
   _("User-Agent: " + ua);
   do_check_eq(ua, expectedUA + ".mobile");
   ua = "";
 });
 
 add_test(function tear_down() {
   server.stop(run_next_test);
--- a/services/sync/tests/unit/test_telemetry.js
+++ b/services/sync/tests/unit/test_telemetry.js
@@ -636,17 +636,17 @@ add_task(async function test_events() {
 
   await Service.engineManager.register(BogusEngine);
   let engine = Service.engineManager.get("bogus");
   engine.enabled = true;
   let server = await serverForFoo(engine);
 
   await SyncTestingInfrastructure(server);
   try {
-    let serverTime = AsyncResource.serverTime;
+    let serverTime = Resource.serverTime;
     Service.recordTelemetryEvent("object", "method", "value", { foo: "bar" });
     let ping = await wait_for_ping(() => Service.sync(), true, true);
     equal(ping.events.length, 1);
     let [timestamp, category, method, object, value, extra] = ping.events[0];
     ok((typeof timestamp == "number") && timestamp > 0); // timestamp.
     equal(category, "sync");
     equal(method, "method");
     equal(object, "object");
--- a/services/sync/tests/unit/test_warn_on_truncated_response.js
+++ b/services/sync/tests/unit/test_warn_on_truncated_response.js
@@ -51,17 +51,17 @@ add_task(async function test_resource_lo
   equal(result, BODY);
 
   await promiseStopServer(httpServer);
 });
 
 add_task(async function test_async_resource_logs_content_length_mismatch() {
   _("Issuing request.");
   let httpServer = httpd_setup({"/content": contentHandler});
-  let asyncResource = new AsyncResource(httpServer.baseURI + "/content");
+  let asyncResource = new Resource(httpServer.baseURI + "/content");
 
   let warnMessages = getWarningMessages(asyncResource._log);
 
   let content = await asyncResource.get();
   equal(content, BODY);
   notEqual(warnMessages.length, 0, "test that warning was logged");
   notEqual(content.length, contentLength);
   await promiseStopServer(httpServer);
--- a/services/sync/tests/unit/xpcshell.ini
+++ b/services/sync/tests/unit/xpcshell.ini
@@ -37,17 +37,16 @@ support-files =
 # We have a number of other libraries that are pretty much standalone.
 [test_addon_utils.js]
 run-sequentially = Restarts server, can't change pref.
 tags = addons
 [test_httpd_sync_server.js]
 
 # HTTP layers.
 [test_resource.js]
-[test_resource_async.js]
 [test_resource_header.js]
 [test_resource_ua.js]
 [test_syncstoragerequest.js]
 
 # Generic Sync types.
 [test_browserid_identity.js]
 [test_browserid_identity_telemetry.js]
 [test_collection_getBatched.js]