Bug 633062 p2 - Make getTokenFromBrowserIDAssertion async. r?markh draft
authorEdouard Oger <eoger@fastmail.com>
Thu, 07 Dec 2017 14:45:15 -0500
changeset 749587 a92f9747bc6588315ba52bab408df0aad5740007
parent 749586 40f49bba1e41b782435dd90a8dee4ec27d9de0ff
child 749588 046b023350ad3b3f04fd1e09d89167c3b1d86f50
push id97449
push userbmo:eoger@fastmail.com
push dateWed, 31 Jan 2018 19:28:47 +0000
reviewersmarkh
bugs633062
milestone60.0a1
Bug 633062 p2 - Make getTokenFromBrowserIDAssertion async. r?markh Also move promiseStopServer to the common/ head_helpers.js MozReview-Commit-ID: B3Idnj6rPAZ
services/common/tests/unit/head_helpers.js
services/common/tests/unit/test_hawkclient.js
services/common/tests/unit/test_tokenserverclient.js
services/common/tokenserverclient.js
services/fxaccounts/tests/xpcshell/test_client.js
services/fxaccounts/tests/xpcshell/test_oauth_grant_client_server.js
services/sync/modules-testing/utils.js
services/sync/modules/browserid_identity.js
services/sync/tests/unit/head_helpers.js
services/sync/tests/unit/test_errorhandler_1.js
services/sync/tests/unit/test_errorhandler_2.js
services/sync/tests/unit/test_fxa_node_reassignment.js
services/sync/tests/unit/test_node_reassignment.js
--- a/services/common/tests/unit/head_helpers.js
+++ b/services/common/tests/unit/head_helpers.js
@@ -100,16 +100,20 @@ function httpd_handler(statusCode, statu
 
     response.setStatusLine(request.httpVersion, statusCode, status);
     if (body) {
       response.bodyOutputStream.write(body, body.length);
     }
   };
 }
 
+function promiseStopServer(server) {
+  return new Promise(resolve => server.stop(resolve));
+}
+
 /*
  * Read bytes string from an nsIInputStream.  If 'count' is omitted,
  * all available input is read.
  */
 function readBytesFromInputStream(inputStream, count) {
   return CommonUtils.readBytesFromInputStream(inputStream, count);
 }
 
--- a/services/common/tests/unit/test_hawkclient.js
+++ b/services/common/tests/unit/test_hawkclient.js
@@ -57,17 +57,17 @@ add_task(async function test_authenticat
 
   let client = new HawkClient(server.baseURI);
 
   let response = await client.request("/foo", method, TEST_CREDS);
   let result = JSON.parse(response.body);
 
   Assert.equal("Great Success!", result.msg);
 
-  await deferredStop(server);
+  await promiseStopServer(server);
 });
 
 async function check_authenticated_request(method) {
   let server = httpd_setup({"/foo": (request, response) => {
       Assert.ok(request.hasHeader("Authorization"));
 
       response.setStatusLine(request.httpVersion, 200, "OK");
       response.setHeader("Content-Type", "application/json");
@@ -77,17 +77,17 @@ async function check_authenticated_reque
 
   let client = new HawkClient(server.baseURI);
 
   let response = await client.request("/foo", method, TEST_CREDS, {foo: "bar"});
   let result = JSON.parse(response.body);
 
   Assert.equal("bar", result.foo);
 
-  await deferredStop(server);
+  await promiseStopServer(server);
 }
 
 add_task(function test_authenticated_post_request() {
   check_authenticated_request("POST");
 });
 
 add_task(function test_authenticated_put_request() {
   check_authenticated_request("PUT");
@@ -112,17 +112,17 @@ add_task(async function test_extra_heade
   let client = new HawkClient(server.baseURI);
 
   let response = await client.request("/foo", "POST", TEST_CREDS, {foo: "bar"},
                                       {"myHeader": "fake"});
   let result = JSON.parse(response.body);
 
   Assert.equal("bar", result.foo);
 
-  await deferredStop(server);
+  await promiseStopServer(server);
 });
 
 add_task(async function test_credentials_optional() {
   let method = "GET";
   let server = httpd_setup({
     "/foo": (request, response) => {
       Assert.ok(!request.hasHeader("Authorization"));
 
@@ -132,17 +132,17 @@ add_task(async function test_credentials
       response.bodyOutputStream.write(message, message.length);
     }
   });
 
   let client = new HawkClient(server.baseURI);
   let result = await client.request("/foo", method); // credentials undefined
   Assert.equal(JSON.parse(result.body).msg, "you're in the friend zone");
 
-  await deferredStop(server);
+  await promiseStopServer(server);
 });
 
 add_task(async function test_server_error() {
   let message = "Ohai!";
   let method = "GET";
 
   let server = httpd_setup({"/foo": (request, response) => {
       response.setStatusLine(request.httpVersion, 418, "I am a Teapot");
@@ -155,17 +155,17 @@ add_task(async function test_server_erro
   try {
     await client.request("/foo", method, TEST_CREDS);
     do_throw("Expected an error");
   } catch (err) {
     Assert.equal(418, err.code);
     Assert.equal("I am a Teapot", err.message);
   }
 
-  await deferredStop(server);
+  await promiseStopServer(server);
 });
 
 add_task(async function test_server_error_json() {
   let message = JSON.stringify({error: "Cannot get ye flask."});
   let method = "GET";
 
   let server = httpd_setup({"/foo": (request, response) => {
       response.setStatusLine(request.httpVersion, 400, "What wouldst thou deau?");
@@ -177,17 +177,17 @@ add_task(async function test_server_erro
 
   try {
     await client.request("/foo", method, TEST_CREDS);
     do_throw("Expected an error");
   } catch (err) {
     Assert.equal("Cannot get ye flask.", err.error);
   }
 
-  await deferredStop(server);
+  await promiseStopServer(server);
 });
 
 add_task(async function test_offset_after_request() {
   let message = "Ohai!";
   let method = "GET";
 
   let server = httpd_setup({"/foo": (request, response) => {
       response.setStatusLine(request.httpVersion, 200, "OK");
@@ -200,17 +200,17 @@ add_task(async function test_offset_afte
   client.now = () => { return now + HOUR_MS; };
 
   Assert.equal(client.localtimeOffsetMsec, 0);
 
   await client.request("/foo", method, TEST_CREDS);
   // Should be about an hour off
   Assert.ok(Math.abs(client.localtimeOffsetMsec + HOUR_MS) < SECOND_MS);
 
-  await deferredStop(server);
+  await promiseStopServer(server);
 });
 
 add_task(async function test_offset_in_hawk_header() {
   let message = "Ohai!";
   let method = "GET";
 
   let server = httpd_setup({
     "/first": function(request, response) {
@@ -243,17 +243,17 @@ add_task(async function test_offset_in_h
   Assert.equal(client.localtimeOffsetMsec, 0);
   await client.request("/first", method, TEST_CREDS);
 
   // After the first server response, our offset is updated to -12 hours.
   // We should be safely in the window, now.
   Assert.ok(Math.abs(client.localtimeOffsetMsec + 12 * HOUR_MS) < MINUTE_MS);
   await client.request("/second", method, TEST_CREDS);
 
-  await deferredStop(server);
+  await promiseStopServer(server);
 });
 
 add_task(async function test_2xx_success() {
   // Just to ensure that we're not biased toward 200 OK for success
   let credentials = {
     id: "eyJleHBpcmVzIjogMTM2NTAxMDg5OC4x",
     key: "qTZf4ZFpAMpMoeSsX3zVRjiqmNs=",
     algorithm: "sha256"
@@ -267,17 +267,17 @@ add_task(async function test_2xx_success
 
   let client = new HawkClient(server.baseURI);
 
   let response = await client.request("/foo", method, credentials);
 
   // Shouldn't be any content in a 202
   Assert.equal(response.body, "");
 
-  await deferredStop(server);
+  await promiseStopServer(server);
 });
 
 add_task(async function test_retry_request_on_fail() {
   let attempts = 0;
   let credentials = {
     id: "eyJleHBpcmVzIjogMTM2NTAxMDg5OC4x",
     key: "qTZf4ZFpAMpMoeSsX3zVRjiqmNs=",
     algorithm: "sha256"
@@ -318,17 +318,17 @@ add_task(async function test_retry_reque
 
   // We begin with no offset
   Assert.equal(client.localtimeOffsetMsec, 0);
 
   // Request will have bad timestamp; client will retry once
   let response = await client.request("/maybe", method, credentials);
   Assert.equal(response.body, "i love you!!!");
 
-  await deferredStop(server);
+  await promiseStopServer(server);
 });
 
 add_task(async function test_multiple_401_retry_once() {
   // Like test_retry_request_on_fail, but always return a 401
   // and ensure that the client only retries once.
   let attempts = 0;
   let credentials = {
     id: "eyJleHBpcmVzIjogMTM2NTAxMDg5OC4x",
@@ -364,17 +364,17 @@ add_task(async function test_multiple_40
   try {
     await client.request("/maybe", method, credentials);
     do_throw("Expected an error");
   } catch (err) {
     Assert.equal(err.code, 401);
   }
   Assert.equal(attempts, 2);
 
-  await deferredStop(server);
+  await promiseStopServer(server);
 });
 
 add_task(async function test_500_no_retry() {
   // If we get a 500 error, the client should not retry (as it would with a
   // 401)
   let credentials = {
     id: "eyJleHBpcmVzIjogMTM2NTAxMDg5OC4x",
     key: "qTZf4ZFpAMpMoeSsX3zVRjiqmNs=",
@@ -401,17 +401,17 @@ add_task(async function test_500_no_retr
   // Request will 500; no retries
   try {
     await client.request("/no-shutup", method, credentials);
     do_throw("Expected an error");
   } catch (err) {
     Assert.equal(err.code, 500);
   }
 
-  await deferredStop(server);
+  await promiseStopServer(server);
 });
 
 add_task(async function test_401_then_500() {
   // Like test_multiple_401_retry_once, but return a 500 to the
   // second request, ensuring that the promise is properly rejected
   // in client.request.
   let attempts = 0;
   let credentials = {
@@ -461,17 +461,17 @@ add_task(async function test_401_then_50
   // Request will have bad timestamp; client will retry once
   try {
     await client.request("/maybe", method, credentials);
   } catch (err) {
     Assert.equal(err.code, 500);
   }
   Assert.equal(attempts, 2);
 
-  await deferredStop(server);
+  await promiseStopServer(server);
 });
 
 add_task(async function throw_if_not_json_body() {
   let client = new HawkClient("https://example.com");
   try {
     await client.request("/bogus", "GET", {}, "I am not json");
     do_throw("Expected an error");
   } catch (err) {
@@ -483,18 +483,12 @@ add_task(async function throw_if_not_jso
 // Utility functions follow
 
 function getTimestampDelta(authHeader, now = Date.now()) {
   let tsMS = new Date(
       parseInt(/ts="(\d+)"/.exec(authHeader)[1], 10) * SECOND_MS);
   return Math.abs(tsMS - now);
 }
 
-function deferredStop(server) {
-  return new Promise(resolve => {
-    server.stop(resolve);
-  });
-}
-
 function run_test() {
   initTestLogging("Trace");
   run_next_test();
 }
--- a/services/common/tests/unit/test_tokenserverclient.js
+++ b/services/common/tests/unit/test_tokenserverclient.js
@@ -1,21 +1,17 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 ChromeUtils.import("resource://services-common/async.js");
 ChromeUtils.import("resource://services-common/tokenserverclient.js");
 
-function run_test() {
-  initTestLogging("Trace");
+initTestLogging("Trace");
 
-  run_next_test();
-}
-
-add_test(function test_working_bid_exchange() {
+add_task(async function test_working_bid_exchange() {
   _("Ensure that working BrowserID token exchange works as expected.");
 
   let service = "http://example.com/foo";
   let duration = 300;
 
   let server = httpd_setup({
     "/1.0/foo/1.0": function(request, response) {
       Assert.ok(request.hasHeader("accept"));
@@ -32,53 +28,46 @@ add_test(function test_working_bid_excha
         uid:          "uid",
         duration,
       });
       response.bodyOutputStream.write(body, body.length);
     }
   });
 
   let client = new TokenServerClient();
-  let cb = Async.makeSpinningCallback();
   let url = server.baseURI + "/1.0/foo/1.0";
-  client.getTokenFromBrowserIDAssertion(url, "assertion", cb);
-  let result = cb.wait();
+  let result = await client.getTokenFromBrowserIDAssertion(url, "assertion");
   Assert.equal("object", typeof(result));
   do_check_attribute_count(result, 6);
   Assert.equal(service, result.endpoint);
   Assert.equal("id", result.id);
   Assert.equal("key", result.key);
   Assert.equal("uid", result.uid);
   Assert.equal(duration, result.duration);
-  server.stop(run_next_test);
+  await promiseStopServer(server);
 });
 
-add_test(function test_invalid_arguments() {
+add_task(async function test_invalid_arguments() {
   _("Ensure invalid arguments to APIs are rejected.");
 
   let args = [
-    [null, "assertion", function() {}],
-    ["http://example.com/", null, function() {}],
-    ["http://example.com/", "assertion", null]
+    [null, "assertion"],
+    ["http://example.com/", null]
   ];
 
   for (let arg of args) {
-    try {
-      let client = new TokenServerClient();
-      client.getTokenFromBrowserIDAssertion(arg[0], arg[1], arg[2]);
-      do_throw("Should never get here.");
-    } catch (ex) {
+    let client = new TokenServerClient();
+    await Assert.rejects(client.getTokenFromBrowserIDAssertion(arg[0], arg[1]), ex => {
       Assert.ok(ex instanceof TokenServerClientError);
-    }
+      return true;
+    });
   }
-
-  run_next_test();
 });
 
-add_test(function test_conditions_required_response_handling() {
+add_task(async function test_conditions_required_response_handling() {
   _("Ensure that a conditions required response is handled properly.");
 
   let description = "Need to accept conditions";
   let tosURL = "http://example.com/tos";
 
   let server = httpd_setup({
     "/1.0/foo/1.0": function(request, response) {
       Assert.ok(!request.hasHeader("x-conditions-accepted"));
@@ -92,32 +81,30 @@ add_test(function test_conditions_requir
       });
       response.bodyOutputStream.write(body, body.length);
     }
   });
 
   let client = new TokenServerClient();
   let url = server.baseURI + "/1.0/foo/1.0";
 
-  function onResponse(error, token) {
+  await Assert.rejects(client.getTokenFromBrowserIDAssertion(url, "assertion"), error => {
     Assert.ok(error instanceof TokenServerClientServerError);
     Assert.equal(error.cause, "conditions-required");
     // Check a JSON.stringify works on our errors as our logging will try and use it.
     Assert.ok(JSON.stringify(error), "JSON.stringify worked");
-    Assert.equal(null, token);
 
     Assert.equal(error.urls.tos, tosURL);
+    return true;
+  });
 
-    server.stop(run_next_test);
-  }
-
-  client.getTokenFromBrowserIDAssertion(url, "assertion", onResponse);
+  await promiseStopServer(server);
 });
 
-add_test(function test_invalid_403_no_content_type() {
+add_task(async function test_invalid_403_no_content_type() {
   _("Ensure that a 403 without content-type is handled properly.");
 
   let server = httpd_setup({
     "/1.0/foo/1.0": function(request, response) {
       response.setStatusLine(request.httpVersion, 403, "Forbidden");
       // No Content-Type header by design.
 
       let body = JSON.stringify({
@@ -126,30 +113,28 @@ add_test(function test_invalid_403_no_co
       });
       response.bodyOutputStream.write(body, body.length);
     }
   });
 
   let client = new TokenServerClient();
   let url = server.baseURI + "/1.0/foo/1.0";
 
-  function onResponse(error, token) {
+  await Assert.rejects(client.getTokenFromBrowserIDAssertion(url, "assertion"), error => {
     Assert.ok(error instanceof TokenServerClientServerError);
     Assert.equal(error.cause, "malformed-response");
-    Assert.equal(null, token);
 
     Assert.equal(null, error.urls);
+    return true;
+  });
 
-    server.stop(run_next_test);
-  }
-
-  client.getTokenFromBrowserIDAssertion(url, "assertion", onResponse);
+  await promiseStopServer(server);
 });
 
-add_test(function test_invalid_403_bad_json() {
+add_task(async function test_invalid_403_bad_json() {
   _("Ensure that a 403 with JSON that isn't proper is handled properly.");
 
   let server = httpd_setup({
     "/1.0/foo/1.0": function(request, response) {
       response.setStatusLine(request.httpVersion, 403, "Forbidden");
       response.setHeader("Content-Type", "application/json; charset=utf-8");
 
       let body = JSON.stringify({
@@ -157,56 +142,52 @@ add_test(function test_invalid_403_bad_j
       });
       response.bodyOutputStream.write(body, body.length);
     }
   });
 
   let client = new TokenServerClient();
   let url = server.baseURI + "/1.0/foo/1.0";
 
-  function onResponse(error, token) {
+  await Assert.rejects(client.getTokenFromBrowserIDAssertion(url, "assertion"), error => {
     Assert.ok(error instanceof TokenServerClientServerError);
     Assert.equal(error.cause, "malformed-response");
-    Assert.equal(null, token);
     Assert.equal(null, error.urls);
+    return true;
+  });
 
-    server.stop(run_next_test);
-  }
-
-  client.getTokenFromBrowserIDAssertion(url, "assertion", onResponse);
+  await promiseStopServer(server);
 });
 
-add_test(function test_403_no_urls() {
+add_task(async function test_403_no_urls() {
   _("Ensure that a 403 without a urls field is handled properly.");
 
   let server = httpd_setup({
     "/1.0/foo/1.0": function(request, response) {
       response.setStatusLine(request.httpVersion, 403, "Forbidden");
       response.setHeader("Content-Type", "application/json; charset=utf-8");
 
       let body = "{}";
       response.bodyOutputStream.write(body, body.length);
     }
   });
 
   let client = new TokenServerClient();
   let url = server.baseURI + "/1.0/foo/1.0";
 
-  client.getTokenFromBrowserIDAssertion(url, "assertion",
-                                        function onResponse(error, result) {
+  await Assert.rejects(client.getTokenFromBrowserIDAssertion(url, "assertion"), error => {
     Assert.ok(error instanceof TokenServerClientServerError);
     Assert.equal(error.cause, "malformed-response");
-    Assert.equal(null, result);
+    return true;
+  });
 
-    server.stop(run_next_test);
-
-  });
+  await promiseStopServer(server);
 });
 
-add_test(function test_send_extra_headers() {
+add_task(async function test_send_extra_headers() {
   _("Ensures that the condition acceptance header is sent when asked.");
 
   let duration = 300;
   let server = httpd_setup({
     "/1.0/foo/1.0": function(request, response) {
       Assert.ok(request.hasHeader("x-foo"));
       Assert.equal(request.getHeader("x-foo"), "42");
 
@@ -225,181 +206,183 @@ add_test(function test_send_extra_header
       });
       response.bodyOutputStream.write(body, body.length);
     }
   });
 
   let client = new TokenServerClient();
   let url = server.baseURI + "/1.0/foo/1.0";
 
-  function onResponse(error, token) {
-    Assert.equal(null, error);
-
-    // Other tests validate other things.
-
-    server.stop(run_next_test);
-  }
-
   let extra = {
     "X-Foo": 42,
     "X-Bar": 17
   };
-  client.getTokenFromBrowserIDAssertion(url, "assertion", onResponse, extra);
+
+  await client.getTokenFromBrowserIDAssertion(url, "assertion", extra);
+  // Other tests validate other things.
+
+  await promiseStopServer(server);
 });
 
-add_test(function test_error_404_empty() {
+add_task(async function test_error_404_empty() {
   _("Ensure that 404 responses without proper response are handled properly.");
 
   let server = httpd_setup();
 
   let client = new TokenServerClient();
   let url = server.baseURI + "/foo";
-  client.getTokenFromBrowserIDAssertion(url, "assertion", function(error, r) {
+
+  await Assert.rejects(client.getTokenFromBrowserIDAssertion(url, "assertion"), error => {
     Assert.ok(error instanceof TokenServerClientServerError);
     Assert.equal(error.cause, "malformed-response");
 
     Assert.notEqual(null, error.response);
-    Assert.equal(null, r);
+    return true;
+  });
 
-    server.stop(run_next_test);
-  });
+  await promiseStopServer(server);
 });
 
-add_test(function test_error_404_proper_response() {
+add_task(async function test_error_404_proper_response() {
   _("Ensure that a Cornice error report for 404 is handled properly.");
 
   let server = httpd_setup({
     "/1.0/foo/1.0": function(request, response) {
       response.setStatusLine(request.httpVersion, 404, "Not Found");
       response.setHeader("Content-Type", "application/json; charset=utf-8");
 
       let body = JSON.stringify({
         status: 404,
         errors: [{description: "No service", location: "body", name: ""}],
       });
 
       response.bodyOutputStream.write(body, body.length);
     }
   });
 
-  function onResponse(error, token) {
+  let client = new TokenServerClient();
+  let url = server.baseURI + "/1.0/foo/1.0";
+
+  await Assert.rejects(client.getTokenFromBrowserIDAssertion(url, "assertion"), error => {
     Assert.ok(error instanceof TokenServerClientServerError);
     Assert.equal(error.cause, "unknown-service");
-    Assert.equal(null, token);
+    return true;
+  });
 
-    server.stop(run_next_test);
-  }
-
-  let client = new TokenServerClient();
-  let url = server.baseURI + "/1.0/foo/1.0";
-  client.getTokenFromBrowserIDAssertion(url, "assertion", onResponse);
+  await promiseStopServer(server);
 });
 
-add_test(function test_bad_json() {
+add_task(async function test_bad_json() {
   _("Ensure that malformed JSON is handled properly.");
 
   let server = httpd_setup({
     "/1.0/foo/1.0": function(request, response) {
       response.setStatusLine(request.httpVersion, 200, "OK");
       response.setHeader("Content-Type", "application/json");
 
       let body = '{"id": "id", baz}';
       response.bodyOutputStream.write(body, body.length);
     }
   });
 
   let client = new TokenServerClient();
   let url = server.baseURI + "/1.0/foo/1.0";
-  client.getTokenFromBrowserIDAssertion(url, "assertion", function(error, r) {
+
+  await Assert.rejects(client.getTokenFromBrowserIDAssertion(url, "assertion"), error => {
     Assert.notEqual(null, error);
     Assert.equal("TokenServerClientServerError", error.name);
     Assert.equal(error.cause, "malformed-response");
     Assert.notEqual(null, error.response);
-    Assert.equal(null, r);
+    return true;
+  });
 
-    server.stop(run_next_test);
-  });
+  await promiseStopServer(server);
 });
 
-add_test(function test_400_response() {
+add_task(async function test_400_response() {
   _("Ensure HTTP 400 is converted to malformed-request.");
 
   let server = httpd_setup({
     "/1.0/foo/1.0": function(request, response) {
       response.setStatusLine(request.httpVersion, 400, "Bad Request");
       response.setHeader("Content-Type", "application/json; charset=utf-8");
 
       let body = "{}"; // Actual content may not be used.
       response.bodyOutputStream.write(body, body.length);
     }
   });
 
   let client = new TokenServerClient();
   let url = server.baseURI + "/1.0/foo/1.0";
-  client.getTokenFromBrowserIDAssertion(url, "assertion", function(error, r) {
+
+  await Assert.rejects(client.getTokenFromBrowserIDAssertion(url, "assertion"), error => {
     Assert.notEqual(null, error);
     Assert.equal("TokenServerClientServerError", error.name);
     Assert.notEqual(null, error.response);
     Assert.equal(error.cause, "malformed-request");
+    return true;
+  });
 
-    server.stop(run_next_test);
-  });
+  await promiseStopServer(server);
 });
 
-add_test(function test_401_with_error_cause() {
+add_task(async function test_401_with_error_cause() {
   _("Ensure 401 cause is specified in body.status");
 
   let server = httpd_setup({
     "/1.0/foo/1.0": function(request, response) {
       response.setStatusLine(request.httpVersion, 401, "Unauthorized");
       response.setHeader("Content-Type", "application/json; charset=utf-8");
 
       let body = JSON.stringify({status: "no-soup-for-you"});
       response.bodyOutputStream.write(body, body.length);
     }
   });
 
   let client = new TokenServerClient();
   let url = server.baseURI + "/1.0/foo/1.0";
-  client.getTokenFromBrowserIDAssertion(url, "assertion", function(error, r) {
+
+  await Assert.rejects(client.getTokenFromBrowserIDAssertion(url, "assertion"), error => {
     Assert.notEqual(null, error);
     Assert.equal("TokenServerClientServerError", error.name);
     Assert.notEqual(null, error.response);
     Assert.equal(error.cause, "no-soup-for-you");
+    return true;
+  });
 
-    server.stop(run_next_test);
-  });
+  await promiseStopServer(server);
 });
 
-add_test(function test_unhandled_media_type() {
+add_task(async function test_unhandled_media_type() {
   _("Ensure that unhandled media types throw an error.");
 
   let server = httpd_setup({
     "/1.0/foo/1.0": function(request, response) {
       response.setStatusLine(request.httpVersion, 200, "OK");
       response.setHeader("Content-Type", "text/plain");
 
       let body = "hello, world";
       response.bodyOutputStream.write(body, body.length);
     }
   });
 
   let url = server.baseURI + "/1.0/foo/1.0";
   let client = new TokenServerClient();
-  client.getTokenFromBrowserIDAssertion(url, "assertion", function(error, r) {
+
+  await Assert.rejects(client.getTokenFromBrowserIDAssertion(url, "assertion"), error => {
     Assert.notEqual(null, error);
     Assert.equal("TokenServerClientServerError", error.name);
     Assert.notEqual(null, error.response);
-    Assert.equal(null, r);
+    return true;
+  });
 
-    server.stop(run_next_test);
-  });
+  await promiseStopServer(server);
 });
 
-add_test(function test_rich_media_types() {
+add_task(async function test_rich_media_types() {
   _("Ensure that extra tokens in the media type aren't rejected.");
 
   let duration = 300;
   let server = httpd_setup({
     "/foo": function(request, response) {
       response.setStatusLine(request.httpVersion, 200, "OK");
       response.setHeader("Content-Type", "application/json; foo=bar; bar=foo");
 
@@ -411,56 +394,12 @@ add_test(function test_rich_media_types(
         duration,
       });
       response.bodyOutputStream.write(body, body.length);
     }
   });
 
   let url = server.baseURI + "/foo";
   let client = new TokenServerClient();
-  client.getTokenFromBrowserIDAssertion(url, "assertion", function(error, r) {
-    Assert.equal(null, error);
 
-    server.stop(run_next_test);
-  });
+  await client.getTokenFromBrowserIDAssertion(url, "assertion");
+  await promiseStopServer(server);
 });
-
-add_test(function test_exception_during_callback() {
-  _("Ensure that exceptions thrown during callback handling are handled.");
-
-  let duration = 300;
-  let server = httpd_setup({
-    "/foo": function(request, response) {
-      response.setStatusLine(request.httpVersion, 200, "OK");
-      response.setHeader("Content-Type", "application/json");
-
-      let body = JSON.stringify({
-        id:           "id",
-        key:          "key",
-        api_endpoint: "foo",
-        uid:          "uid",
-        duration,
-      });
-      response.bodyOutputStream.write(body, body.length);
-    }
-  });
-
-  let url = server.baseURI + "/foo";
-  let client = new TokenServerClient();
-  let cb = Async.makeSpinningCallback();
-  let callbackCount = 0;
-
-  client.getTokenFromBrowserIDAssertion(url, "assertion", function(error, r) {
-    Assert.equal(null, error);
-
-    cb();
-
-    callbackCount += 1;
-    throw new Error("I am a bad function!");
-  });
-
-  cb.wait();
-  // This relies on some heavy event loop magic. The error in the main
-  // callback should already have been raised at this point.
-  Assert.equal(callbackCount, 1);
-
-  server.stop(run_next_test);
-});
--- a/services/common/tokenserverclient.js
+++ b/services/common/tokenserverclient.js
@@ -204,141 +204,103 @@ TokenServerClient.prototype = {
    *
    * Example Usage
    * -------------
    *
    *   let client = new TokenServerClient();
    *   let assertion = getBrowserIDAssertionFromSomewhere();
    *   let url = "https://token.services.mozilla.com/1.0/sync/2.0";
    *
-   *   client.getTokenFromBrowserIDAssertion(url, assertion,
-   *                                         function onResponse(error, result) {
-   *     if (error) {
-   *       if (error.cause == "conditions-required") {
-   *         promptConditionsAcceptance(error.urls, function onAccept() {
-   *           client.getTokenFromBrowserIDAssertion(url, assertion,
-   *           onResponse, true);
-   *         }
-   *         return;
-   *       }
-   *
-   *       // Do other error handling.
-   *       return;
-   *     }
-   *
-   *     let {
-   *       id: id, key: key, uid: uid, endpoint: endpoint, duration: duration
-   *     } = result;
+   *   try {
+   *     const result = await client.getTokenFromBrowserIDAssertion(url, assertion);
+   *     let {id, key, uid, endpoint, duration} = result;
    *     // Do stuff with data and carry on.
-   *   });
+   *   } catch (error) {
+   *     // Handle errors.
+   *   }
    *
    * @param  url
    *         (string) URL to fetch token from.
    * @param  assertion
    *         (string) BrowserID assertion to exchange token for.
-   * @param  cb
-   *         (function) Callback to be invoked with result of operation.
    * @param  conditionsAccepted
    *         (bool) Whether to send acceptance to service conditions.
    */
-  getTokenFromBrowserIDAssertion:
-    function getTokenFromBrowserIDAssertion(url, assertion, cb, addHeaders = {}) {
+  async getTokenFromBrowserIDAssertion(url, assertion, addHeaders = {}) {
     if (!url) {
       throw new TokenServerClientError("url argument is not valid.");
     }
 
     if (!assertion) {
       throw new TokenServerClientError("assertion argument is not valid.");
     }
 
-    if (!cb) {
-      throw new TokenServerClientError("cb argument is not valid.");
-    }
-
     this._log.debug("Beginning BID assertion exchange: " + url);
 
     let req = this.newRESTRequest(url);
     req.setHeader("Accept", "application/json");
     req.setHeader("Authorization", "BrowserID " + assertion);
 
     for (let header in addHeaders) {
       req.setHeader(header, addHeaders[header]);
     }
 
-    let client = this;
-    req.get(function onResponse(error) {
-      if (error) {
-        cb(new TokenServerClientNetworkError(error), null);
-        return;
-      }
-
-      let self = this;
-      function callCallback(error, result) {
-        if (!cb) {
-          self._log.warn("Callback already called! Did it throw?");
-          return;
-        }
+    let response = await new Promise((resolve, reject) => {
+      req.get(function(err) {
+        // Yes this is weird, the callback's |this| gets bound to the RESTRequest object.
+        err ? reject(new TokenServerClientNetworkError(err)) :
+              resolve(this.response);
+      });
+    });
 
-        try {
-          cb(error, result);
-        } catch (ex) {
-          self._log.warn("Exception when calling user-supplied callback", ex);
-        }
-
-        cb = null;
+    try {
+      return this._processTokenResponse(response);
+    } catch (ex) {
+      if (ex instanceof TokenServerClientServerError) {
+        throw ex;
       }
-
-      try {
-        client._processTokenResponse(this.response, callCallback);
-      } catch (ex) {
-        this._log.warn("Error processing token server response", ex);
-
-        let error = new TokenServerClientError(ex);
-        error.response = this.response;
-        callCallback(error, null);
-      }
-    });
+      this._log.warn("Error processing token server response", ex);
+      let error = new TokenServerClientError(ex);
+      error.response = response;
+      throw error;
+    }
   },
 
   /**
    * Handler to process token request responses.
    *
    * @param response
    *        RESTResponse from token HTTP request.
-   * @param cb
-   *        The original callback passed to the public API.
    */
-  _processTokenResponse: function processTokenResponse(response, cb) {
+  _processTokenResponse(response) {
     this._log.debug("Got token response: " + response.status);
 
     // Responses should *always* be JSON, even in the case of 4xx and 5xx
     // errors. If we don't see JSON, the server is likely very unhappy.
     let ct = response.headers["content-type"] || "";
     if (ct != "application/json" && !ct.startsWith("application/json;")) {
       this._log.warn("Did not receive JSON response. Misconfigured server?");
       this._log.debug("Content-Type: " + ct);
       this._log.debug("Body: " + response.body);
 
       let error = new TokenServerClientServerError("Non-JSON response.",
                                                    "malformed-response");
       error.response = response;
-      cb(error, null);
-      return;
+      throw error;
     }
 
     let result;
     try {
       result = JSON.parse(response.body);
     } catch (ex) {
       this._log.warn("Invalid JSON returned by server: " + response.body);
       let error = new TokenServerClientServerError("Malformed JSON.",
                                                    "malformed-response");
       error.response = response;
-      cb(error, null);
-      return;
+      throw error;
     }
 
     // Any response status can have X-Backoff or X-Weave-Backoff headers.
     this._maybeNotifyBackoff(response, "x-weave-backoff");
     this._maybeNotifyBackoff(response, "x-backoff");
 
     // The service shouldn't have any 3xx, so we don't need to handle those.
     if (response.status != 200) {
@@ -388,41 +350,39 @@ TokenServerClient.prototype = {
         error.message = "Unknown service.";
         error.cause = "unknown-service";
       }
 
       // A Retry-After header should theoretically only appear on a 503, but
       // we'll look for it on any error response.
       this._maybeNotifyBackoff(response, "retry-after");
 
-      cb(error, null);
-      return;
+      throw error;
     }
 
     for (let k of ["id", "key", "api_endpoint", "uid", "duration"]) {
       if (!(k in result)) {
         let error = new TokenServerClientServerError("Expected key not " +
                                                      " present in result: " +
                                                      k);
         error.cause = "malformed-response";
         error.response = response;
-        cb(error, null);
-        return;
+        throw error;
       }
     }
 
     this._log.debug("Successful token response");
-    cb(null, {
+    return {
       id:             result.id,
       key:            result.key,
       endpoint:       result.api_endpoint,
       uid:            result.uid,
       duration:       result.duration,
       hashed_fxa_uid: result.hashed_fxa_uid,
-    });
+    };
   },
 
   /*
    * The prefix used for all notifications sent by this module.  This
    * allows the handler of notifications to be sure they are handling
    * notifications for the service they expect.
    *
    * If not set, no notifications will be sent.
--- a/services/fxaccounts/tests/xpcshell/test_client.js
+++ b/services/fxaccounts/tests/xpcshell/test_client.js
@@ -24,22 +24,16 @@ var ACCOUNT_KEYS = {
 
   kA:           h("2021222324252627 28292a2b2c2d2e2f" +
                   "3031323334353637 38393a3b3c3d3e3f"),
 
   wrapKB:       h("4041424344454647 48494a4b4c4d4e4f" +
                   "5051525354555657 58595a5b5c5d5e5f"),
 };
 
-function deferredStop(server) {
-  return new Promise(resolve => {
-    server.stop(resolve);
-  });
-}
-
 add_task(async function test_authenticated_get_request() {
   let message = "{\"msg\": \"Great Success!\"}";
   let credentials = {
     id: "eyJleHBpcmVzIjogMTM2NTAxMDg5OC4x",
     key: "qTZf4ZFpAMpMoeSsX3zVRjiqmNs=",
     algorithm: "sha256"
   };
   let method = "GET";
@@ -52,17 +46,17 @@ add_task(async function test_authenticat
     }
   });
 
   let client = new FxAccountsClient(server.baseURI);
 
   let result = await client._request("/foo", method, credentials);
   Assert.equal("Great Success!", result.msg);
 
-  await deferredStop(server);
+  await promiseStopServer(server);
 });
 
 add_task(async function test_authenticated_post_request() {
   let credentials = {
     id: "eyJleHBpcmVzIjogMTM2NTAxMDg5OC4x",
     key: "qTZf4ZFpAMpMoeSsX3zVRjiqmNs=",
     algorithm: "sha256"
   };
@@ -77,17 +71,17 @@ add_task(async function test_authenticat
     }
   });
 
   let client = new FxAccountsClient(server.baseURI);
 
   let result = await client._request("/foo", method, credentials, {foo: "bar"});
   Assert.equal("bar", result.foo);
 
-  await deferredStop(server);
+  await promiseStopServer(server);
 });
 
 add_task(async function test_500_error() {
   let message = "<h1>Ooops!</h1>";
   let method = "GET";
 
   let server = httpd_setup({"/foo": function(request, response) {
       response.setStatusLine(request.httpVersion, 500, "Internal Server Error");
@@ -100,17 +94,17 @@ add_task(async function test_500_error()
   try {
     await client._request("/foo", method);
     do_throw("Expected to catch an exception");
   } catch (e) {
     Assert.equal(500, e.code);
     Assert.equal("Internal Server Error", e.message);
   }
 
-  await deferredStop(server);
+  await promiseStopServer(server);
 });
 
 add_task(async function test_backoffError() {
   let method = "GET";
   let server = httpd_setup({
     "/retryDelay": function(request, response) {
       response.setHeader("Retry-After", "30");
       response.setStatusLine(request.httpVersion, 429, "Client has sent too many requests");
@@ -147,17 +141,17 @@ add_task(async function test_backoffErro
     Assert.notEqual(client.backoffError, null);
   }
   // Once timer fires, client nulls error out and HTTP calls work again.
   client._clearBackoff();
   let result = await client._request("/duringDelayIShouldNotBeCalled", method);
   Assert.equal(client.backoffError, null);
   Assert.equal(result.working, "yes");
 
-  await deferredStop(server);
+  await promiseStopServer(server);
 });
 
 add_task(async function test_signUp() {
   let creationMessage_noKey = JSON.stringify({
     uid: "uid",
     sessionToken: "sessionToken"
   });
   let creationMessage_withKey = JSON.stringify({
@@ -232,17 +226,17 @@ add_task(async function test_signUp() {
   // Try to create an existing account.  Triggers error path.
   try {
     result = await client.signUp(unicodeUsername, unicodePassword);
     do_throw("Expected to catch an exception");
   } catch (expectedError) {
     Assert.equal(101, expectedError.errno);
   }
 
-  await deferredStop(server);
+  await promiseStopServer(server);
 });
 
 add_task(async function test_signIn() {
   let sessionMessage_noKey = JSON.stringify({
     sessionToken: FAKE_SESSION_TOKEN
   });
   let sessionMessage_withKey = JSON.stringify({
     sessionToken: FAKE_SESSION_TOKEN,
@@ -319,17 +313,17 @@ add_task(async function test_signIn() {
   // Trigger error path
   try {
     result = await client.signIn("yøü@bad.example.org", "nofear");
     do_throw("Expected to catch an exception");
   } catch (expectedError) {
     Assert.equal(102, expectedError.errno);
   }
 
-  await deferredStop(server);
+  await promiseStopServer(server);
 });
 
 add_task(async function test_signOut() {
   let signoutMessage = JSON.stringify({});
   let errorMessage = JSON.stringify({code: 400, errno: 102, error: "doesn't exist"});
   let signedOut = false;
 
   let server = httpd_setup({
@@ -355,17 +349,17 @@ add_task(async function test_signOut() {
   // Trigger error path
   try {
     result = await client.signOut("FakeSession");
     do_throw("Expected to catch an exception");
   } catch (expectedError) {
     Assert.equal(102, expectedError.errno);
   }
 
-  await deferredStop(server);
+  await promiseStopServer(server);
 });
 
 add_task(async function test_recoveryEmailStatus() {
   let emailStatus = JSON.stringify({verified: true});
   let errorMessage = JSON.stringify({code: 400, errno: 102, error: "doesn't exist"});
   let tries = 0;
 
   let server = httpd_setup({
@@ -393,17 +387,17 @@ add_task(async function test_recoveryEma
   // Trigger error path
   try {
     result = await client.recoveryEmailStatus("some bogus session");
     do_throw("Expected to catch an exception");
   } catch (expectedError) {
     Assert.equal(102, expectedError.errno);
   }
 
-  await deferredStop(server);
+  await promiseStopServer(server);
 });
 
 add_task(async function test_recoveryEmailStatusWithReason() {
   let emailStatus = JSON.stringify({verified: true});
   let server = httpd_setup({
     "/recovery_email/status": function(request, response) {
       Assert.ok(request.hasHeader("Authorization"));
       // if there is a query string then it will have a reason
@@ -414,17 +408,17 @@ add_task(async function test_recoveryEma
     },
   });
 
   let client = new FxAccountsClient(server.baseURI);
   let result = await client.recoveryEmailStatus(FAKE_SESSION_TOKEN, {
     reason: "push",
   });
   Assert.equal(result.verified, true);
-  await deferredStop(server);
+  await promiseStopServer(server);
 });
 
 add_task(async function test_resendVerificationEmail() {
   let emptyMessage = "{}";
   let errorMessage = JSON.stringify({code: 400, errno: 102, error: "doesn't exist"});
   let tries = 0;
 
   let server = httpd_setup({
@@ -450,17 +444,17 @@ add_task(async function test_resendVerif
   // Trigger error path
   try {
     result = await client.resendVerificationEmail("some bogus session");
     do_throw("Expected to catch an exception");
   } catch (expectedError) {
     Assert.equal(102, expectedError.errno);
   }
 
-  await deferredStop(server);
+  await promiseStopServer(server);
 });
 
 add_task(async function test_accountKeys() {
   // Four calls to accountKeys().  The first one should work correctly, and we
   // should get a valid bundle back, in exchange for our keyFetch token, from
   // which we correctly derive kA and wrapKB.  The subsequent three calls
   // should all trigger separate error paths.
   let responseMessage = JSON.stringify({bundle: ACCOUNT_KEYS.response});
@@ -531,17 +525,17 @@ add_task(async function test_accountKeys
   // Fourth try, pretend account doesn't exist
   try {
     result = await client.accountKeys(ACCOUNT_KEYS.keyFetch);
     do_throw("Expected to catch an exception");
   } catch (expectedError) {
     Assert.equal(102, expectedError.errno);
   }
 
-  await deferredStop(server);
+  await promiseStopServer(server);
 });
 
 add_task(async function test_signCertificate() {
   let certSignMessage = JSON.stringify({cert: {bar: "baz"}});
   let errorMessage = JSON.stringify({code: 400, errno: 102, error: "doesn't exist"});
   let tries = 0;
 
   let server = httpd_setup({
@@ -572,17 +566,17 @@ add_task(async function test_signCertifi
   // Account doesn't exist
   try {
     result = await client.signCertificate("bogus", JSON.stringify({foo: "bar"}), 600);
     do_throw("Expected to catch an exception");
   } catch (expectedError) {
     Assert.equal(102, expectedError.errno);
   }
 
-  await deferredStop(server);
+  await promiseStopServer(server);
 });
 
 add_task(async function test_accountExists() {
   let existsMessage = JSON.stringify({error: "wrong password", code: 400, errno: 103});
   let doesntExistMessage = JSON.stringify({error: "no such account", code: 400, errno: 102});
   let emptyMessage = "{}";
 
   let server = httpd_setup({
@@ -631,17 +625,17 @@ add_task(async function test_accountExis
 
   try {
     result = await client.accountExists("i.break.things@example.com");
     do_throw("Expected to catch an exception");
   } catch (unexpectedError) {
     Assert.equal(unexpectedError.code, 500);
   }
 
-  await deferredStop(server);
+  await promiseStopServer(server);
 });
 
 add_task(async function test_registerDevice() {
   const DEVICE_ID = "device id";
   const DEVICE_NAME = "device name";
   const DEVICE_TYPE = "device type";
   const ERROR_NAME = "test that the client promise rejects";
 
@@ -684,17 +678,17 @@ add_task(async function test_registerDev
 
   try {
     await client.registerDevice(FAKE_SESSION_TOKEN, ERROR_NAME, DEVICE_TYPE);
     do_throw("Expected to catch an exception");
   } catch (unexpectedError) {
     Assert.equal(unexpectedError.code, 500);
   }
 
-  await deferredStop(server);
+  await promiseStopServer(server);
 });
 
 add_task(async function test_updateDevice() {
   const DEVICE_ID = "some other id";
   const DEVICE_NAME = "some other name";
   const ERROR_ID = "test that the client promise rejects";
 
   const server = httpd_setup({
@@ -730,17 +724,17 @@ add_task(async function test_updateDevic
 
   try {
     await client.updateDevice(FAKE_SESSION_TOKEN, ERROR_ID, DEVICE_NAME);
     do_throw("Expected to catch an exception");
   } catch (unexpectedError) {
     Assert.equal(unexpectedError.code, 500);
   }
 
-  await deferredStop(server);
+  await promiseStopServer(server);
 });
 
 add_task(async function test_signOutAndDestroyDevice() {
   const DEVICE_ID = "device id";
   const ERROR_ID = "test that the client promise rejects";
   let emptyMessage = "{}";
 
   const server = httpd_setup({
@@ -772,17 +766,17 @@ add_task(async function test_signOutAndD
 
   try {
     await client.signOutAndDestroyDevice(FAKE_SESSION_TOKEN, ERROR_ID);
     do_throw("Expected to catch an exception");
   } catch (unexpectedError) {
     Assert.equal(unexpectedError.code, 500);
   }
 
-  await deferredStop(server);
+  await promiseStopServer(server);
 });
 
 add_task(async function test_getDeviceList() {
   let canReturnDevices;
 
   const server = httpd_setup({
     "/account/devices": function(request, response) {
       if (canReturnDevices) {
@@ -805,17 +799,17 @@ add_task(async function test_getDeviceLi
   try {
     canReturnDevices = false;
     await client.getDeviceList(FAKE_SESSION_TOKEN);
     do_throw("Expected to catch an exception");
   } catch (unexpectedError) {
     Assert.equal(unexpectedError.code, 500);
   }
 
-  await deferredStop(server);
+  await promiseStopServer(server);
 });
 
 add_task(async function test_client_metrics() {
   function writeResp(response, msg) {
     if (typeof msg === "object") {
       msg = JSON.stringify(msg);
     }
     response.bodyOutputStream.write(msg, msg.length);
@@ -838,17 +832,17 @@ add_task(async function test_client_metr
   let client = new FxAccountsClient(server.baseURI);
 
   await Assert.rejects(client.signOut(FAKE_SESSION_TOKEN, {
     service: "sync",
   }), function(err) {
     return err.errno == 111;
   });
 
-  await deferredStop(server);
+  await promiseStopServer(server);
 });
 
 add_task(async function test_email_case() {
   let canonicalEmail = "greta.garbo@gmail.com";
   let clientEmail = "Greta.Garbo@gmail.COM";
   let attempts = 0;
 
   function writeResp(response, msg) {
@@ -891,15 +885,15 @@ add_task(async function test_email_case(
   );
 
   let client = new FxAccountsClient(server.baseURI);
 
   let result = await client.signIn(clientEmail, "123456");
   Assert.equal(result.areWeHappy, "yes");
   Assert.equal(attempts, 2);
 
-  await deferredStop(server);
+  await promiseStopServer(server);
 });
 
 // turn formatted test vectors into normal hex strings
 function h(hexStr) {
   return hexStr.replace(/\s+/g, "");
 }
--- a/services/fxaccounts/tests/xpcshell/test_oauth_grant_client_server.js
+++ b/services/fxaccounts/tests/xpcshell/test_oauth_grant_client_server.js
@@ -40,22 +40,16 @@ function startServer() {
   activeTokens = new Set();
   let srv = new HttpServer();
   srv.registerPathHandler("/v1/authorization", authorize);
   srv.registerPathHandler("/v1/destroy", destroy);
   srv.start(-1);
   return srv;
 }
 
-function promiseStopServer(server) {
-  return new Promise(resolve => {
-    server.stop(resolve);
-  });
-}
-
 add_task(async function getAndRevokeToken() {
   Services.prefs.setBoolPref("identity.fxaccounts.allowHttp", true);
   let server = startServer();
   try {
     let clientOptions = {
       serverURL: "http://localhost:" + server.identity.primaryPort + "/v1",
       client_id: "abc123",
     };
--- a/services/sync/modules-testing/utils.js
+++ b/services/sync/modules-testing/utils.js
@@ -188,19 +188,19 @@ this.configureFxAccountIdentity = functi
     accountStatus() {
       return Promise.resolve(true);
     }
   };
   let mockFxAClient = new MockFxAccountsClient();
   fxa.internal._fxAccountsClient = mockFxAClient;
 
   let mockTSC = { // TokenServerClient
-    getTokenFromBrowserIDAssertion(uri, assertion, cb) {
+    async getTokenFromBrowserIDAssertion(uri, assertion) {
       config.fxaccount.token.uid = config.username;
-      cb(null, config.fxaccount.token);
+      return config.fxaccount.token;
     },
   };
   authService._fxaService = fxa;
   authService._tokenServerClient = mockTSC;
   // Set the "account" of the browserId manager to be the "email" of the
   // logged in user of the mockFXA service.
   authService._signedInUser = config.fxaccount.user;
   authService._account = config.fxaccount.user.email;
--- a/services/sync/modules/browserid_identity.js
+++ b/services/sync/modules/browserid_identity.js
@@ -603,30 +603,23 @@ this.BrowserIDManager.prototype = {
       return this._fxaService.getKeys().then(
         newUserData => {
           userData = newUserData;
           this._updateSignedInUser(userData); // throws if the user changed.
         }
       );
     };
 
-    let getToken = assertion => {
+    let getToken = async (assertion) => {
       log.debug("Getting a token");
-      let deferred = PromiseUtils.defer();
-      let cb = function(err, token) {
-        if (err) {
-          return deferred.reject(err);
-        }
-        log.debug("Successfully got a sync token");
-        return deferred.resolve(token);
-      };
-
       let headers = {"X-Client-State": userData.kXCS};
-      client.getTokenFromBrowserIDAssertion(tokenServerURI, assertion, cb, headers);
-      return deferred.promise;
+      // Exceptions will be handled by the caller.
+      const token = await client.getTokenFromBrowserIDAssertion(tokenServerURI, assertion, headers);
+      log.debug("Successfully got a sync token");
+      return token;
     };
 
     let getAssertion = () => {
       log.info("Getting an assertion from", tokenServerURI);
       let audience = Services.io.newURI(tokenServerURI).prePath;
       return fxa.getAssertion(audience);
     };
 
--- a/services/sync/tests/unit/head_helpers.js
+++ b/services/sync/tests/unit/head_helpers.js
@@ -482,19 +482,16 @@ function promiseOneObserver(topic, callb
     let observer = function(subject, data) {
       Svc.Obs.remove(topic, observer);
       resolve({ subject, data });
     };
     Svc.Obs.add(topic, observer);
   });
 }
 
-function promiseStopServer(server) {
-  return new Promise(resolve => server.stop(resolve));
-}
 // Avoid an issue where `client.name2` containing unicode characters causes
 // a number of tests to fail, due to them assuming that we do not need to utf-8
 // encode or decode data sent through the mocked server (see bug 1268912).
 // We stash away the original implementation so test_utils_misc.js can test it.
 Utils._orig_getDefaultDeviceName = Utils.getDefaultDeviceName;
 Utils.getDefaultDeviceName = function() {
   return "Test device name";
 };
--- a/services/sync/tests/unit/test_errorhandler_1.js
+++ b/services/sync/tests/unit/test_errorhandler_1.js
@@ -11,18 +11,17 @@ ChromeUtils.import("resource://services-
 ChromeUtils.import("resource://services-sync/util.js");
 ChromeUtils.import("resource://gre/modules/FileUtils.jsm");
 ChromeUtils.import("resource://gre/modules/PromiseUtils.jsm");
 
 var fakeServer = new SyncServer();
 fakeServer.start();
 
 registerCleanupFunction(function() {
-  return new Promise(resolve => {
-    fakeServer.stop(resolve);
+  return promiseStopServer(fakeServer).finally(() => {
     Svc.Prefs.resetBranch("");
   });
 });
 
 var fakeServerUrl = "http://localhost:" + fakeServer.port;
 
 const logsdir = FileUtils.getDir("ProfD", ["weave", "logs"], true);
 
--- a/services/sync/tests/unit/test_errorhandler_2.js
+++ b/services/sync/tests/unit/test_errorhandler_2.js
@@ -11,19 +11,17 @@ ChromeUtils.import("resource://services-
 ChromeUtils.import("resource://services-sync/util.js");
 ChromeUtils.import("resource://gre/modules/FileUtils.jsm");
 ChromeUtils.import("resource://gre/modules/PromiseUtils.jsm");
 
 var fakeServer = new SyncServer();
 fakeServer.start();
 
 registerCleanupFunction(function() {
-  return new Promise(resolve => {
-    fakeServer.stop(resolve);
-  });
+  return promiseStopServer(fakeServer);
 });
 
 var fakeServerUrl = "http://localhost:" + fakeServer.port;
 
 const logsdir = FileUtils.getDir("ProfD", ["weave", "logs"], true);
 
 function removeLogFiles() {
   let entries = logsdir.directoryEntries;
--- a/services/sync/tests/unit/test_fxa_node_reassignment.js
+++ b/services/sync/tests/unit/test_fxa_node_reassignment.js
@@ -66,30 +66,32 @@ function prepareServer(cbAfterTokenFetch
 
   // Set the token endpoint for the initial token request that's done implicitly
   // via configureIdentity.
   config.fxaccount.token.endpoint = server.baseURI + "1.1/johndoe/";
   // And future token fetches will do magic around numReassigns.
   let numReassigns = 0;
   return configureIdentity(config).then(() => {
     Service.identity._tokenServerClient = {
-      getTokenFromBrowserIDAssertion(uri, assertion, cb) {
-        // Build a new URL with trailing zeros for the SYNC_VERSION part - this
-        // will still be seen as equivalent by the test server, but different
-        // by sync itself.
-        numReassigns += 1;
-        let trailingZeros = new Array(numReassigns + 1).join("0");
-        let token = config.fxaccount.token;
-        token.endpoint = server.baseURI + "1.1" + trailingZeros + "/johndoe";
-        token.uid = config.username;
-        numTokenRequests += 1;
-        cb(null, token);
-        if (cbAfterTokenFetch) {
-          cbAfterTokenFetch();
-        }
+      getTokenFromBrowserIDAssertion(uri, assertion) {
+        return new Promise(res => {
+          // Build a new URL with trailing zeros for the SYNC_VERSION part - this
+          // will still be seen as equivalent by the test server, but different
+          // by sync itself.
+          numReassigns += 1;
+          let trailingZeros = new Array(numReassigns + 1).join("0");
+          let token = config.fxaccount.token;
+          token.endpoint = server.baseURI + "1.1" + trailingZeros + "/johndoe";
+          token.uid = config.username;
+          numTokenRequests += 1;
+          res(token);
+          if (cbAfterTokenFetch) {
+            cbAfterTokenFetch();
+          }
+        });
       },
     };
     return server;
   });
 }
 
 function getReassigned() {
   try {
--- a/services/sync/tests/unit/test_node_reassignment.js
+++ b/services/sync/tests/unit/test_node_reassignment.js
@@ -73,21 +73,19 @@ function getReassigned() {
  * setup, detach observers, etc.
  */
 async function syncAndExpectNodeReassignment(server, firstNotification, between,
                                        secondNotification, url) {
   let deferred = PromiseUtils.defer();
 
   let getTokenCount = 0;
   let mockTSC = { // TokenServerClient
-    getTokenFromBrowserIDAssertion(uri, assertion, cb) {
+    async getTokenFromBrowserIDAssertion(uri, assertion) {
       getTokenCount++;
-      cb(null, {
-        endpoint: server.baseURI + "1.1/johndoe/"
-      });
+      return {endpoint: server.baseURI + "1.1/johndoe/"};
     },
   };
   Service.identity._tokenServerClient = mockTSC;
 
   // Make sure that it works!
   await new Promise(res => {
     let request = new RESTRequest(url);
     request.get(function() {
@@ -275,21 +273,19 @@ add_task(async function test_loop_avoida
   let firstNotification  = "weave:service:login:error";
   let secondNotification = "weave:service:login:error";
   let thirdNotification  = "weave:service:sync:finish";
 
   let deferred = PromiseUtils.defer();
 
   let getTokenCount = 0;
   let mockTSC = { // TokenServerClient
-    getTokenFromBrowserIDAssertion(uri, assertion, cb) {
+    async getTokenFromBrowserIDAssertion(uri, assertion) {
       getTokenCount++;
-      cb(null, {
-        endpoint: server.baseURI + "1.1/johndoe/"
-      });
+      return {endpoint: server.baseURI + "1.1/johndoe/"};
     },
   };
   Service.identity._tokenServerClient = mockTSC;
 
   // Track the time. We want to make sure the duration between the first and
   // second sync is small, and then that the duration between second and third
   // is set to be large.
   let now;
@@ -371,21 +367,19 @@ add_task(async function test_loop_avoida
   let john   = server.user("johndoe");
 
   _("Enabling the Rotary engine.");
   let { engine, tracker } = await registerRotaryEngine();
   let deferred = PromiseUtils.defer();
 
   let getTokenCount = 0;
   let mockTSC = { // TokenServerClient
-    getTokenFromBrowserIDAssertion(uri, assertion, cb) {
+    getTokenFromBrowserIDAssertion(uri, assertion) {
       getTokenCount++;
-      cb(null, {
-        endpoint: server.baseURI + "1.1/johndoe/"
-      });
+      return {endpoint: server.baseURI + "1.1/johndoe/"};
     },
   };
   Service.identity._tokenServerClient = mockTSC;
 
   // We need the server to be correctly set up prior to experimenting. Do this
   // through a sync.
   let global = {syncID: Service.syncID,
                 storageVersion: STORAGE_VERSION,