Bug 1281932 - Fix intermittent u2f tests r=keeler draft
authorJ.C. Jones <jjones@mozilla.com>
Mon, 10 Oct 2016 17:06:31 -0700
changeset 423336 1255bd8ba17a141c3c8205a277c06c483540de90
parent 421656 da986c9f1f723af1e0c44f4ccd4cddd5fb6084e8
child 423390 12ab9ccb118fec94d1f6e283fa91a726ab306856
push id31885
push userjjones@mozilla.com
push dateTue, 11 Oct 2016 00:07:30 +0000
reviewerskeeler
bugs1281932
milestone52.0a1
Bug 1281932 - Fix intermittent u2f tests r=keeler This is reworking the U2F tests to do two things: 1) Don't run all the tests in one big frame; that makes it hard to tell what test is actually dying in Treeherder. 2) Fix the obvious possible test races with the async functions which could be causing the intermittent - Review updates per keeler - Change inappropriate uses of 'var' to 'let' in u2futil.js (kudos, keeler) - Rework frame_no_token.html to follow the same pattern as the others - Catch unexpected messages on the u2f testing harness - Update 2: Go back to a pre-set number of expected async tests. MozReview-Commit-ID: 6uLt5O1lUa3
dom/u2f/tests/frame_appid_facet.html
dom/u2f/tests/frame_appid_facet_insecure.html
dom/u2f/tests/frame_appid_facet_subdomain.html
dom/u2f/tests/frame_no_token.html
dom/u2f/tests/frame_register.html
dom/u2f/tests/frame_register_sign.html
dom/u2f/tests/mochitest.ini
dom/u2f/tests/test_appid_facet.html
dom/u2f/tests/test_appid_facet_insecure.html
dom/u2f/tests/test_appid_facet_subdomain.html
dom/u2f/tests/test_frame.html
dom/u2f/tests/test_frame_appid_facet.html
dom/u2f/tests/test_frame_appid_facet_insecure.html
dom/u2f/tests/test_frame_appid_facet_subdomain.html
dom/u2f/tests/test_frame_register.html
dom/u2f/tests/test_frame_register_sign.html
dom/u2f/tests/test_no_token.html
dom/u2f/tests/test_register.html
dom/u2f/tests/test_register_sign.html
dom/u2f/tests/u2futil.js
rename from dom/u2f/tests/test_frame_appid_facet.html
rename to dom/u2f/tests/frame_appid_facet.html
--- a/dom/u2f/tests/test_frame_appid_facet.html
+++ b/dom/u2f/tests/frame_appid_facet.html
@@ -8,51 +8,56 @@
 <script class="testbody" type="text/javascript">
 "use strict";
 
 local_is(window.location.origin, "https://example.com", "Is loaded correctly");
 
 var version = "U2F_V2";
 var challenge = new Uint8Array(16);
 
+local_expectThisManyTests(5);
+
 u2f.register(null, [{
   version: version,
   challenge: bytesToBase64UrlSafe(challenge),
 }], [], function(res){
   local_is(res.errorCode, 0, "Null AppID should work.");
+  local_completeTest();
 });
 
 u2f.register("", [{
   version: version,
   challenge: bytesToBase64UrlSafe(challenge),
 }], [], function(res){
   local_is(res.errorCode, 0, "Empty AppID should work.");
+  local_completeTest();
 });
 
 // Test: Correct TLD, but incorrect scheme
 u2f.register("http://example.com/appId", [{
   version: version,
   challenge: bytesToBase64UrlSafe(challenge),
 }], [], function(res){
   local_isnot(res.errorCode, 0, "HTTP scheme is disallowed");
+  local_completeTest();
 });
 
 // Test: Correct TLD, and also HTTPS
 u2f.register("https://example.com/appId", [{
   version: version,
   challenge: bytesToBase64UrlSafe(challenge),
 }], [], function(res){
   local_is(res.errorCode, 0, "HTTPS origin for example.com should work");
+  local_completeTest();
 });
 
 // Test: Dynamic origin
 u2f.register(window.location.origin + "/otherAppId", [{
   version: version,
   challenge: bytesToBase64UrlSafe(challenge),
 }], [], function(res){
   local_is(res.errorCode, 0, "Direct window origin should work");
+  local_completeTest();
 });
 
-local_finished();
-
 </script>
 </body>
 </html>
rename from dom/u2f/tests/test_frame_appid_facet_insecure.html
rename to dom/u2f/tests/frame_appid_facet_insecure.html
--- a/dom/u2f/tests/test_frame_appid_facet_insecure.html
+++ b/dom/u2f/tests/frame_appid_facet_insecure.html
@@ -8,48 +8,53 @@
 <script class="testbody" type="text/javascript">
 "use strict";
 
 local_is(window.location.origin, "http://mochi.test:8888", "Is loaded correctly");
 
 var version = "U2F_V2";
 var challenge = new Uint8Array(16);
 
+local_expectThisManyTests(5);
+
 u2f.register(null, [{
   version: version,
   challenge: bytesToBase64UrlSafe(challenge),
 }], [], function(res){
   local_isnot(res.errorCode, 0, "Insecure origin disallowed for null AppID");
+  local_completeTest();
 });
 
 u2f.register("", [{
   version: version,
   challenge: bytesToBase64UrlSafe(challenge),
 }], [], function(res){
   local_isnot(res.errorCode, 0, "Insecure origin disallowed for empty AppID");
+  local_completeTest();
 });
 
 u2f.register("http://example.com/appId", [{
   version: version,
   challenge: bytesToBase64UrlSafe(challenge),
 }], [], function(res){
   local_isnot(res.errorCode, 0, "Insecure origin disallowed for HTTP AppID");
+  local_completeTest();
 });
 
 u2f.register("https://example.com/appId", [{
   version: version,
   challenge: bytesToBase64UrlSafe(challenge),
 }], [], function(res){
   local_isnot(res.errorCode, 0, "Insecure origin disallowed for HTTPS AppID from HTTP origin");
+  local_completeTest();
 });
 
 u2f.register(window.location.origin + "/otherAppId", [{
   version: version,
   challenge: bytesToBase64UrlSafe(challenge),
 }], [], function(res){
   local_isnot(res.errorCode, 0, "Insecure origin disallowed for HTTP origin");
+  local_completeTest();
 });
 
-local_finished();
-
 </script>
 </body>
 </html>
rename from dom/u2f/tests/test_frame_appid_facet_subdomain.html
rename to dom/u2f/tests/frame_appid_facet_subdomain.html
--- a/dom/u2f/tests/test_frame_appid_facet_subdomain.html
+++ b/dom/u2f/tests/frame_appid_facet_subdomain.html
@@ -8,53 +8,49 @@
 <script class="testbody" type="text/javascript">
 "use strict";
 
 var version = "U2F_V2";
 var challenge = new Uint8Array(16);
 
 local_is(window.location.origin, "https://test1.example.com", "Is loaded correctly");
 
+local_expectThisManyTests(4);
+
 // same domain check
 u2f.register("https://test1.example.com/appId", [{
   version: version,
   challenge: bytesToBase64UrlSafe(challenge),
 }], [], function(res){
   local_is(res.errorCode, 0, "AppID should work from a different path of this domain");
-  step2();
+  local_completeTest();
+});
+
+// same domain check, but wrong scheme
+u2f.register("http://test1.example.com/appId", [{
+  version: version,
+  challenge: bytesToBase64UrlSafe(challenge),
+}], [], function(res){
+  local_isnot(res.errorCode, 0, "AppID should not work when using a different scheme");
+  local_completeTest();
 });
 
-function step2() {
-  // same domain check, but wrong scheme
-  u2f.register("http://test1.example.com/appId", [{
-    version: version,
-    challenge: bytesToBase64UrlSafe(challenge),
-  }], [], function(res){
-    local_isnot(res.errorCode, 0, "AppID should not work when using a different scheme");
-    step3();
-  });
-}
+// eTLD+1 subdomain check
+u2f.register("https://example.com/appId", [{
+  version: version,
+  challenge: bytesToBase64UrlSafe(challenge),
+}], [], function(res){
+  local_isnot(res.errorCode, 0, "AppID should not work from another subdomain in this registered domain");
+  local_completeTest();
+});
 
-function step3() {
-  // eTLD+1 subdomain check
-  u2f.register("https://example.com/appId", [{
-    version: version,
-    challenge: bytesToBase64UrlSafe(challenge),
-  }], [], function(res){
-    local_isnot(res.errorCode, 0, "AppID should not work from another subdomain in this registered domain");
-    step4();
-  });
-}
-
-function step4() {
-  // other domain check
-  u2f.register("https://mochi.test:8888/appId", [{
-    version: version,
-    challenge: bytesToBase64UrlSafe(challenge),
-  }], [], function(res){
-    local_isnot(res.errorCode, 0, "AppID should not work from other domains");
-    local_finished();
-  });
-}
+// other domain check
+u2f.register("https://mochi.test:8888/appId", [{
+  version: version,
+  challenge: bytesToBase64UrlSafe(challenge),
+}], [], function(res){
+  local_isnot(res.errorCode, 0, "AppID should not work from other domains");
+  local_completeTest();
+});
 
 </script>
 </body>
 </html>
--- a/dom/u2f/tests/frame_no_token.html
+++ b/dom/u2f/tests/frame_no_token.html
@@ -11,17 +11,20 @@
 var challenge = new Uint8Array(16);
 window.crypto.getRandomValues(challenge);
 
 var regRequest = {
   version: "U2F_V2",
   challenge: bytesToBase64UrlSafe(challenge),
 };
 
-u2f.register(window.location.origin, [regRequest], [], function (regResponse) {
-  parent.postMessage(regResponse.errorCode, '*');
+local_expectThisManyTests(1);
+
+u2f.register(window.location.origin, [regRequest], [], function (res) {
+  local_isnot(res.errorCode, 0, "The registration should be rejected.");
+  local_completeTest();
 });
 
 </script>
 
 </body>
 </html>
 
rename from dom/u2f/tests/test_frame_register.html
rename to dom/u2f/tests/frame_register.html
--- a/dom/u2f/tests/test_frame_register.html
+++ b/dom/u2f/tests/frame_register.html
@@ -8,67 +8,74 @@
 <script class="testbody" type="text/javascript">
 "use strict";
 
 var version = "U2F_V2";
 var challenge = new Uint8Array(16);
 
 local_is(window.location.origin, "https://example.com", "Is loaded correctly");
 
-// eTLD+1 check
+local_expectThisManyTests(7);
+
+// basic check
 u2f.register("https://example.com/appId", [{
   version: version,
   challenge: bytesToBase64UrlSafe(challenge),
 }], [], function(res){
-  local_is(res.errorCode, 0, "AppID should work from a subdomain");
+  local_is(res.errorCode, 0, "AppID should work from the domain");
+  local_completeTest();
 });
 
 u2f.register("https://example.net/appId", [{
   version: version,
   challenge: bytesToBase64UrlSafe(challenge),
 }], [], function(res){
   local_is(res.errorCode, 2, "AppID should not work from other domains");
+  local_completeTest();
 });
 
 u2f.register("", [], [], function(res){
   local_is(res.errorCode, 2, "Empty register requests");
+  local_completeTest();
 });
 
 local_doesThrow(function(){
   u2f.register("", null, [], null);
 }, "Non-array register requests");
 
 local_doesThrow(function(){
   u2f.register("", [], null, null);
 }, "Non-array sign requests");
 
 local_doesThrow(function(){
   u2f.register("", null, null, null);
 }, "Non-array for both arguments");
 
 u2f.register("", [{}], [], function(res){
   local_is(res.errorCode, 2, "Empty request");
+  local_completeTest();
 });
 
 u2f.register("https://example.net/appId", [{
     version: version,
   }], [], function(res){
-  local_is(res.errorCode, 2, "Missing challenge");
+    local_is(res.errorCode, 2, "Missing challenge");
+    local_completeTest();
 });
 
 u2f.register("https://example.net/appId", [{
     challenge: bytesToBase64UrlSafe(challenge),
   }], [], function(res){
-  local_is(res.errorCode, 2, "Missing version");
+   local_is(res.errorCode, 2, "Missing version");
+   local_completeTest();
 });
 
 u2f.register("https://example.net/appId", [{
     version: "a_version_00",
     challenge: bytesToBase64UrlSafe(challenge),
   }], [], function(res){
-  local_is(res.errorCode, 2, "Invalid version");
+    local_is(res.errorCode, 2, "Invalid version");
+    local_completeTest();
 });
 
-local_finished();
-
 </script>
 </body>
 </html>
rename from dom/u2f/tests/test_frame_register_sign.html
rename to dom/u2f/tests/frame_register_sign.html
--- a/dom/u2f/tests/mochitest.ini
+++ b/dom/u2f/tests/mochitest.ini
@@ -1,17 +1,21 @@
 [DEFAULT]
 support-files =
+  frame_appid_facet.html
+  frame_appid_facet_insecure.html
+  frame_appid_facet_subdomain.html
   frame_no_token.html
-  u2futil.js
-  test_frame_appid_facet.html
-  test_frame_register.html
-  test_frame_register_sign.html
-  test_frame_appid_facet_insecure.html
-  test_frame_appid_facet_subdomain.html
+  frame_register.html
+  frame_register_sign.html
+  pkijs/asn1.js
   pkijs/common.js
-  pkijs/asn1.js
   pkijs/x509_schema.js
   pkijs/x509_simpl.js
+  u2futil.js
 
 [test_util_methods.html]
 [test_no_token.html]
-[test_frame.html]
+[test_register.html]
+[test_register_sign.html]
+[test_appid_facet.html]
+[test_appid_facet_insecure.html]
+[test_appid_facet_subdomain.html]
new file mode 100644
--- /dev/null
+++ b/dom/u2f/tests/test_appid_facet.html
@@ -0,0 +1,34 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<head>
+  <title>Test for AppID / FacetID behavior for FIDO Universal Second Factor</title>
+  <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <script src="u2futil.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1231681">Mozilla Bug 1231681</a>
+
+<div id="framediv">
+  <iframe id="testing_frame"></iframe>
+</div>
+
+<pre id="log"></pre>
+
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+SpecialPowers.pushPrefEnv({"set": [["security.webauth.u2f", true],
+                                   ["security.webauth.u2f_enable_softtoken", true],
+                                   ["security.webauth.u2f_enable_usbtoken", false]]},
+function() {
+  // listen for messages from the test harness
+  window.addEventListener("message", handleEventMessage, false);
+  document.getElementById('testing_frame').src = "https://example.com/tests/dom/u2f/tests/frame_appid_facet.html";
+});
+
+</script>
+
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/dom/u2f/tests/test_appid_facet_insecure.html
@@ -0,0 +1,34 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<head>
+  <title>Test for AppID / FacetID behavior for FIDO Universal Second Factor</title>
+  <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <script src="u2futil.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1231681">Mozilla Bug 1231681</a>
+
+<div id="framediv">
+  <iframe id="testing_frame"></iframe>
+</div>
+
+<pre id="log"></pre>
+
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+SpecialPowers.pushPrefEnv({"set": [["security.webauth.u2f", true],
+                                   ["security.webauth.u2f_enable_softtoken", true],
+                                   ["security.webauth.u2f_enable_usbtoken", false]]},
+function() {
+  // listen for messages from the test harness
+  window.addEventListener("message", handleEventMessage, false);
+  document.getElementById('testing_frame').src = "http://mochi.test:8888/tests/dom/u2f/tests/frame_appid_facet_insecure.html";
+});
+
+</script>
+
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/dom/u2f/tests/test_appid_facet_subdomain.html
@@ -0,0 +1,34 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<head>
+  <title>Test for AppID / FacetID behavior for FIDO Universal Second Factor</title>
+  <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <script src="u2futil.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1231681">Mozilla Bug 1231681</a>
+
+<div id="framediv">
+  <iframe id="testing_frame"></iframe>
+</div>
+
+<pre id="log"></pre>
+
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+SpecialPowers.pushPrefEnv({"set": [["security.webauth.u2f", true],
+                                   ["security.webauth.u2f_enable_softtoken", true],
+                                   ["security.webauth.u2f_enable_usbtoken", false]]},
+function() {
+  // listen for messages from the test harness
+  window.addEventListener("message", handleEventMessage, false);
+  document.getElementById('testing_frame').src = "https://test1.example.com/tests/dom/u2f/tests/frame_appid_facet_subdomain.html";
+});
+
+</script>
+
+</body>
+</html>
deleted file mode 100644
--- a/dom/u2f/tests/test_frame.html
+++ /dev/null
@@ -1,64 +0,0 @@
-<!DOCTYPE html>
-<meta charset=utf-8>
-<head>
-  <title>Test for AppID / FacetID behavior for FIDO Universal Second Factor</title>
-  <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
-  <script src="u2futil.js"></script>
-
-  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
-</head>
-<body>
-<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1231681">Mozilla Bug 1231681</a>
-
-<div id="framediv">
-  <iframe id="testing_frame"></iframe>
-</div>
-
-<pre id="log"></pre>
-
-<script class="testbody" type="text/javascript">
-
-SpecialPowers.pushPrefEnv({"set": [["security.webauth.u2f", true],
-                                   ["security.webauth.u2f_enable_softtoken", true]]},
-function() {
-  var testList = [
-    "https://example.com/tests/dom/u2f/tests/test_frame_register.html",
-    "https://example.com/tests/dom/u2f/tests/test_frame_register_sign.html",
-    "http://mochi.test:8888/tests/dom/u2f/tests/test_frame_appid_facet_insecure.html",
-    "https://example.com/tests/dom/u2f/tests/test_frame_appid_facet.html",
-    "https://test1.example.com/tests/dom/u2f/tests/test_frame_appid_facet_subdomain.html"
-  ];
-
-  function log(msg) {
-    document.getElementById("log").textContent += "\n" + msg;
-  }
-
-  function nextTest() {
-    if (testList.length < 1) {
-      SimpleTest.finish();
-      return;
-    }
-
-    document.getElementById('testing_frame').src = testList.shift();
-  }
-
-  // listen for a messages from the mixed content test harness
-  function receiveMessage(event) {
-    if ("test" in event.data) {
-      var summary = event.data.test + ": " + event.data.msg;
-      log(event.data.status + ": " + summary);
-      ok(event.data.status, summary);
-    } else if ("done" in event.data) {
-      nextTest();
-    }
-  }
-
-  window.addEventListener("message", receiveMessage, false);
-  nextTest();
-});
-
-SimpleTest.waitForExplicitFinish();
-</script>
-
-  </body>
-</html>
--- a/dom/u2f/tests/test_no_token.html
+++ b/dom/u2f/tests/test_no_token.html
@@ -16,21 +16,17 @@
 <script class="testbody" type="text/javascript">
 
 SimpleTest.waitForExplicitFinish();
 
 SpecialPowers.pushPrefEnv({"set": [["security.webauth.u2f", true],
                                    ["security.webauth.u2f_enable_softtoken", false],
                                    ["security.webauth.u2f_enable_usbtoken", false]]},
 function() {
-  onmessage = function(event) {
-    //event.data is the response.errorCode
-    isnot(event.data, 0, "The registration should be rejected.");
-    SimpleTest.finish();
-  }
-
+  // listen for messages from the test harness
+  window.addEventListener("message", handleEventMessage, false);
   document.getElementById('testing_frame').src = 'frame_no_token.html';
 });
 
 </script>
 
 </body>
 </html>
new file mode 100644
--- /dev/null
+++ b/dom/u2f/tests/test_register.html
@@ -0,0 +1,34 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<head>
+  <title>Test for Register behavior for FIDO Universal Second Factor</title>
+  <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <script src="u2futil.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1231681">Mozilla Bug 1231681</a>
+
+<div id="framediv">
+  <iframe id="testing_frame"></iframe>
+</div>
+
+<pre id="log"></pre>
+
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+SpecialPowers.pushPrefEnv({"set": [["security.webauth.u2f", true],
+                                   ["security.webauth.u2f_enable_softtoken", true],
+                                   ["security.webauth.u2f_enable_usbtoken", false]]},
+function() {
+  // listen for messages from the test harness
+  window.addEventListener("message", handleEventMessage, false);
+  document.getElementById('testing_frame').src = "https://example.com/tests/dom/u2f/tests/frame_register.html";
+});
+
+</script>
+
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/dom/u2f/tests/test_register_sign.html
@@ -0,0 +1,34 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<head>
+  <title>Register and Sign Test for FIDO Universal Second Factor</title>
+  <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <script src="u2futil.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1231681">Mozilla Bug 1231681</a>
+
+<div id="framediv">
+  <iframe id="testing_frame"></iframe>
+</div>
+
+<pre id="log"></pre>
+
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+SpecialPowers.pushPrefEnv({"set": [["security.webauth.u2f", true],
+                                   ["security.webauth.u2f_enable_softtoken", true],
+                                   ["security.webauth.u2f_enable_usbtoken", false]]},
+function() {
+  // listen for messages from the test harness
+  window.addEventListener("message", handleEventMessage, false);
+  document.getElementById('testing_frame').src = "https://example.com/tests/dom/u2f/tests/frame_appid_facet.html";
+});
+
+</script>
+
+</body>
+</html>
--- a/dom/u2f/tests/u2futil.js
+++ b/dom/u2f/tests/u2futil.js
@@ -1,8 +1,32 @@
+// Used by local_addTest() / local_completeTest()
+var _countCompletions = 0;
+var _expectedCompletions = 0;
+
+function handleEventMessage(event) {
+  if ("test" in event.data) {
+    let summary = event.data.test + ": " + event.data.msg;
+    log(event.data.status + ": " + summary);
+    ok(event.data.status, summary);
+  } else if ("done" in event.data) {
+    SimpleTest.finish();
+  } else {
+    ok(false, "Unexpected message in the test harness: " + event.data)
+  }
+}
+
+function log(msg) {
+  console.log(msg)
+  let logBox = document.getElementById("log");
+  if (logBox) {
+    logBox.textContent += "\n" + msg;
+  }
+}
+
 function local_is(value, expected, message) {
   if (value === expected) {
     local_ok(true, message);
   } else {
     local_ok(false, message + " unexpectedly: " + value + " !== " + expected);
   }
 }
 
@@ -15,41 +39,59 @@ function local_isnot(value, expected, me
 }
 
 function local_ok(expression, message) {
   let body = {"test": this.location.pathname, "status":expression, "msg": message}
   parent.postMessage(body, "http://mochi.test:8888");
 }
 
 function local_doesThrow(fn, name) {
-  var gotException = false;
+  let gotException = false;
   try {
     fn();
   } catch (ex) { gotException = true; }
   local_ok(gotException, name);
 };
 
+function local_expectThisManyTests(count) {
+  if (_expectedCompletions > 0) {
+    local_ok(false, "Error: local_expectThisManyTests should only be called once.");
+  }
+  _expectedCompletions = count;
+}
+
+function local_completeTest() {
+  _countCompletions += 1
+  if (_countCompletions == _expectedCompletions) {
+    log("All tests completed.")
+    local_finished();
+  }
+  if (_countCompletions > _expectedCompletions) {
+    local_ok(false, "Error: local_completeTest called more than local_addTest.");
+  }
+}
+
 function local_finished() {
   parent.postMessage({"done":true}, "http://mochi.test:8888");
 }
 
 function string2buffer(str) {
   return (new Uint8Array(str.length)).map((x, i) => str.charCodeAt(i));
 }
 
 function buffer2string(buf) {
-  var str = "";
+  let str = "";
   buf.map(x => str += String.fromCharCode(x));
   return str;
 }
 
 function bytesToBase64(u8a){
-  var CHUNK_SZ = 0x8000;
-  var c = [];
-  for (var i = 0; i < u8a.length; i += CHUNK_SZ) {
+  let CHUNK_SZ = 0x8000;
+  let c = [];
+  for (let i = 0; i < u8a.length; i += CHUNK_SZ) {
     c.push(String.fromCharCode.apply(null, u8a.subarray(i, i + CHUNK_SZ)));
   }
   return window.btoa(c.join(""));
 }
 
 function base64ToBytes(b64encoded) {
   return new Uint8Array(window.atob(b64encoded).split("").map(function(c) {
     return c.charCodeAt(0);
@@ -108,55 +150,55 @@ function deriveAppAndChallengeParam(appI
     return {
       appParam: new Uint8Array(digests[0]),
       challengeParam: new Uint8Array(digests[1]),
     };
   });
 }
 
 function assembleSignedData(appParam, presenceAndCounter, challengeParam) {
-  var signedData = new Uint8Array(32 + 1 + 4 + 32);
+  let signedData = new Uint8Array(32 + 1 + 4 + 32);
   appParam.map((x, i) => signedData[0 + i] = x);
   presenceAndCounter.map((x, i) => signedData[32 + i] = x);
   challengeParam.map((x, i) => signedData[37 + i] = x);
   return signedData;
 }
 
 function assembleRegistrationSignedData(appParam, challengeParam, keyHandle, pubKey) {
-  var signedData = new Uint8Array(1 + 32 + 32 + keyHandle.length + 65);
+  let signedData = new Uint8Array(1 + 32 + 32 + keyHandle.length + 65);
   signedData[0] = 0x00;
   appParam.map((x, i) => signedData[1 + i] = x);
   challengeParam.map((x, i) => signedData[33 + i] = x);
   keyHandle.map((x, i) => signedData[65 + i] = x);
   pubKey.map((x, i) => signedData[65 + keyHandle.length + i] = x);
   return signedData;
 }
 
 function sanitizeSigArray(arr) {
   // ECDSA signature fields into WebCrypto must be exactly 32 bytes long, so
   // this method strips leading padding bytes, if added, and also appends
   // padding zeros, if needed.
   if (arr.length > 32) {
     arr = arr.slice(arr.length - 32)
   }
-  var ret = new Uint8Array(32);
+  let ret = new Uint8Array(32);
   ret.set(arr, ret.length - arr.length);
   return ret;
 }
 
 function verifySignature(key, data, derSig) {
-  var sigAsn1 = org.pkijs.fromBER(derSig.buffer);
-  var sigR = new Uint8Array(sigAsn1.result.value_block.value[0].value_block.value_hex);
-  var sigS = new Uint8Array(sigAsn1.result.value_block.value[1].value_block.value_hex);
+  let sigAsn1 = org.pkijs.fromBER(derSig.buffer);
+  let sigR = new Uint8Array(sigAsn1.result.value_block.value[0].value_block.value_hex);
+  let sigS = new Uint8Array(sigAsn1.result.value_block.value[1].value_block.value_hex);
 
   // The resulting R and S values from the ASN.1 Sequence must be fit into 32
   // bytes. Sometimes they have leading zeros, sometimes they're too short, it
   // all depends on what lib generated the signature.
-  var R = sanitizeSigArray(sigR);
-  var S = sanitizeSigArray(sigS);
+  let R = sanitizeSigArray(sigR);
+  let S = sanitizeSigArray(sigS);
 
-  var sigData = new Uint8Array(R.length + S.length);
+  let sigData = new Uint8Array(R.length + S.length);
   sigData.set(R);
   sigData.set(S, R.length);
 
-  var alg = {name: "ECDSA", hash: "SHA-256"};
+  let alg = {name: "ECDSA", hash: "SHA-256"};
   return crypto.subtle.verify(alg, key, sigData, data);
 }