Bug 1472828 [wpt PR 11758] - Network Error Logging: Add web platform tests, a=testonly
authorDouglas Creager <dcreager@chromium.org>
Wed, 22 Aug 2018 13:18:42 +0000
changeset 831096 3d9ba45c5fc51add474e6639b49d94dc6314a0c9
parent 831095 e02d5a5d5498376588d1162f36a8440e8f4ec76b
child 831097 17b39e682fe103ce049ea7b4950d18273b776a13
push id118868
push userbmo:zjz@zjz.name
push dateFri, 24 Aug 2018 07:04:39 +0000
reviewerstestonly
bugs1472828, 11758, 748549, 1106518, 584093
milestone63.0a1
Bug 1472828 [wpt PR 11758] - Network Error Logging: Add web platform tests, a=testonly Automatic update from web-platform-testsNetwork Error Logging: Add web platform tests This adds web-platform-tests for Network Error Logging. They should be suitable to upstream into the cross-platform WPT repo. Bug: 748549 Change-Id: I24098aeec8488c34a5f9239309264e9534dca6e4 Reviewed-on: https://chromium-review.googlesource.com/1106518 Reviewed-by: Peter Beverloo <peter@chromium.org> Reviewed-by: Philip J├Ągenstedt <foolip@chromium.org> Commit-Queue: Douglas Creager <dcreager@chromium.org> Cr-Commit-Position: refs/heads/master@{#584093} -- wpt-commits: d0b86dfcab07829407425bad98287757818f96c5 wpt-pr: 11758
testing/web-platform/meta/MANIFEST.json
testing/web-platform/tests/network-error-logging/META.yml
testing/web-platform/tests/network-error-logging/README.md
testing/web-platform/tests/network-error-logging/no-report-on-failed-cors-preflight.https.html
testing/web-platform/tests/network-error-logging/no-report-on-subdomain-404.https.html
testing/web-platform/tests/network-error-logging/no-report-on-subdomain-success.https.html
testing/web-platform/tests/network-error-logging/reports-are-not-observable.https.html
testing/web-platform/tests/network-error-logging/sends-report-on-404.https.html
testing/web-platform/tests/network-error-logging/sends-report-on-subdomain-dns-failure.https.html
testing/web-platform/tests/network-error-logging/sends-report-on-success-with-subdomain-policy.https.html
testing/web-platform/tests/network-error-logging/sends-report-on-success.https.html
testing/web-platform/tests/network-error-logging/support/clear-policy-pass.png
testing/web-platform/tests/network-error-logging/support/clear-policy-pass.png.sub.headers
testing/web-platform/tests/network-error-logging/support/lock.py
testing/web-platform/tests/network-error-logging/support/nel.sub.js
testing/web-platform/tests/network-error-logging/support/no-policy-pass.png
testing/web-platform/tests/network-error-logging/support/pass.png
testing/web-platform/tests/network-error-logging/support/pass.png.sub.headers
testing/web-platform/tests/network-error-logging/support/report.py
testing/web-platform/tests/network-error-logging/support/subdomains-pass.png
testing/web-platform/tests/network-error-logging/support/subdomains-pass.png.sub.headers
--- a/testing/web-platform/meta/MANIFEST.json
+++ b/testing/web-platform/meta/MANIFEST.json
@@ -291683,16 +291683,76 @@
      {}
     ]
    ],
    "netinfo/META.yml": [
     [
      {}
     ]
    ],
+   "network-error-logging/META.yml": [
+    [
+     {}
+    ]
+   ],
+   "network-error-logging/README.md": [
+    [
+     {}
+    ]
+   ],
+   "network-error-logging/support/clear-policy-pass.png": [
+    [
+     {}
+    ]
+   ],
+   "network-error-logging/support/clear-policy-pass.png.sub.headers": [
+    [
+     {}
+    ]
+   ],
+   "network-error-logging/support/lock.py": [
+    [
+     {}
+    ]
+   ],
+   "network-error-logging/support/nel.sub.js": [
+    [
+     {}
+    ]
+   ],
+   "network-error-logging/support/no-policy-pass.png": [
+    [
+     {}
+    ]
+   ],
+   "network-error-logging/support/pass.png": [
+    [
+     {}
+    ]
+   ],
+   "network-error-logging/support/pass.png.sub.headers": [
+    [
+     {}
+    ]
+   ],
+   "network-error-logging/support/report.py": [
+    [
+     {}
+    ]
+   ],
+   "network-error-logging/support/subdomains-pass.png": [
+    [
+     {}
+    ]
+   ],
+   "network-error-logging/support/subdomains-pass.png.sub.headers": [
+    [
+     {}
+    ]
+   ],
    "notifications/META.yml": [
     [
      {}
     ]
    ],
    "notifications/common.js": [
     [
      {}
@@ -368846,16 +368906,64 @@
     ]
    ],
    "netinfo/netinfo-basics.html": [
     [
      "/netinfo/netinfo-basics.html",
      {}
     ]
    ],
+   "network-error-logging/no-report-on-failed-cors-preflight.https.html": [
+    [
+     "/network-error-logging/no-report-on-failed-cors-preflight.https.html",
+     {}
+    ]
+   ],
+   "network-error-logging/no-report-on-subdomain-404.https.html": [
+    [
+     "/network-error-logging/no-report-on-subdomain-404.https.html",
+     {}
+    ]
+   ],
+   "network-error-logging/no-report-on-subdomain-success.https.html": [
+    [
+     "/network-error-logging/no-report-on-subdomain-success.https.html",
+     {}
+    ]
+   ],
+   "network-error-logging/reports-are-not-observable.https.html": [
+    [
+     "/network-error-logging/reports-are-not-observable.https.html",
+     {}
+    ]
+   ],
+   "network-error-logging/sends-report-on-404.https.html": [
+    [
+     "/network-error-logging/sends-report-on-404.https.html",
+     {}
+    ]
+   ],
+   "network-error-logging/sends-report-on-subdomain-dns-failure.https.html": [
+    [
+     "/network-error-logging/sends-report-on-subdomain-dns-failure.https.html",
+     {}
+    ]
+   ],
+   "network-error-logging/sends-report-on-success-with-subdomain-policy.https.html": [
+    [
+     "/network-error-logging/sends-report-on-success-with-subdomain-policy.https.html",
+     {}
+    ]
+   ],
+   "network-error-logging/sends-report-on-success.https.html": [
+    [
+     "/network-error-logging/sends-report-on-success.https.html",
+     {}
+    ]
+   ],
    "notifications/constructor-basic.html": [
     [
      "/notifications/constructor-basic.html",
      {}
     ]
    ],
    "notifications/constructor-invalid.html": [
     [
@@ -610623,16 +610731,96 @@
   "netinfo/idlharness.any.js": [
    "4610508b69b1e30db574e04f2651b40cb8b646f2",
    "testharness"
   ],
   "netinfo/netinfo-basics.html": [
    "7d08e12280a881508c862ebaaeaa40a099cf8e35",
    "testharness"
   ],
+  "network-error-logging/META.yml": [
+   "bc063177d1d14febb1c4bdb86e70cfdce8ca0b5b",
+   "support"
+  ],
+  "network-error-logging/README.md": [
+   "7cf2c6fdceed95b3911deb69542a6820acda479d",
+   "support"
+  ],
+  "network-error-logging/no-report-on-failed-cors-preflight.https.html": [
+   "3a35651b4ef549a0510a83df923fd6b9d642b97c",
+   "testharness"
+  ],
+  "network-error-logging/no-report-on-subdomain-404.https.html": [
+   "462f99e842317cf36c4d7a76ad13af00b1692d15",
+   "testharness"
+  ],
+  "network-error-logging/no-report-on-subdomain-success.https.html": [
+   "5fd6d4fb41231c5ca5f345b890927c5d1b9411ab",
+   "testharness"
+  ],
+  "network-error-logging/reports-are-not-observable.https.html": [
+   "35ab4f3c23507617c4f26981339741d9b3c385be",
+   "testharness"
+  ],
+  "network-error-logging/sends-report-on-404.https.html": [
+   "38bdc014501e90f5a99bae1ac0d433191f557afb",
+   "testharness"
+  ],
+  "network-error-logging/sends-report-on-subdomain-dns-failure.https.html": [
+   "8913857af8acb01760589b6a7546a110a359f192",
+   "testharness"
+  ],
+  "network-error-logging/sends-report-on-success-with-subdomain-policy.https.html": [
+   "fce12cd3e96cf327222faea4bdaeba5835f4f4ce",
+   "testharness"
+  ],
+  "network-error-logging/sends-report-on-success.https.html": [
+   "68fddaa0c70b8dd0fd22194b351ba1157f836bdc",
+   "testharness"
+  ],
+  "network-error-logging/support/clear-policy-pass.png": [
+   "2fa1e0ac0663a65deae6602621521cc2844b93de",
+   "support"
+  ],
+  "network-error-logging/support/clear-policy-pass.png.sub.headers": [
+   "1085b8a987c56fd8b0412f4032f6957e58447ace",
+   "support"
+  ],
+  "network-error-logging/support/lock.py": [
+   "8c88250bde00b4a62cc99131fdaa09e97f716369",
+   "support"
+  ],
+  "network-error-logging/support/nel.sub.js": [
+   "c6b4783bd94c00579047627b6c2b137478ae1c2e",
+   "support"
+  ],
+  "network-error-logging/support/no-policy-pass.png": [
+   "2fa1e0ac0663a65deae6602621521cc2844b93de",
+   "support"
+  ],
+  "network-error-logging/support/pass.png": [
+   "2fa1e0ac0663a65deae6602621521cc2844b93de",
+   "support"
+  ],
+  "network-error-logging/support/pass.png.sub.headers": [
+   "70796e913ace97d4d1a21ac0b1c19f6fbb6d01fc",
+   "support"
+  ],
+  "network-error-logging/support/report.py": [
+   "7c05b51b9eb011f4d32bd5e774f6a0d3ead2cd9c",
+   "support"
+  ],
+  "network-error-logging/support/subdomains-pass.png": [
+   "2fa1e0ac0663a65deae6602621521cc2844b93de",
+   "support"
+  ],
+  "network-error-logging/support/subdomains-pass.png.sub.headers": [
+   "50124b8cfcdfd6efe91a0613ec34d3c8ca0a10dc",
+   "support"
+  ],
   "notifications/META.yml": [
    "2cb7972705c7b9ef00375dfa4258e92edb15fb21",
    "support"
   ],
   "notifications/body-basic-manual.html": [
    "a479917aeef9f3b777d152148a3b75cb8df7f494",
    "manual"
   ],
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/tests/network-error-logging/META.yml
@@ -0,0 +1,2 @@
+suggested_reviewers:
+  - dcreager
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/tests/network-error-logging/README.md
@@ -0,0 +1,74 @@
+# Network Error Logging
+
+The tests in this directory exercise the user agent's implementation of [Network
+Error Logging](https://w3c.github.io/network-error-logging/) and
+[Reporting](https://w3c.github.io/reporting/).
+
+## Collector
+
+Each test case generates a unique `reportID` that is used to distinguish the NEL
+reports generated by that test case.
+
+The [support/report.py][] file is a [Python file handler][] that can be used as
+a Reporting collector.  Its default operation is to save any reports that it
+receives into the [stash][].  If you pass in the optional `op` URL parameter,
+with a value of `retrieve_report`, it will instead return a list of all of the
+reports received for a particular `reportID`.
+
+[Python file handler]: https://wptserve.readthedocs.io/en/latest/handlers.html#python-file-handlers
+[stash]: https://wptserve.readthedocs.io/en/latest/stash.html
+[support/report.py]: support/report.py
+
+## Installing NEL policies
+
+NEL reports are only generated if the user agent has received a NEL policy for
+the origin of the request.  The current request counts; if its response contains
+a policy, that policy is used for the current request and all future requests,
+modulo the policy's `max_age` field.
+
+Most of the test cases will therefore make a request or two to install NEL
+policies, and then make another request that should or should not be covered by
+those policies.  It will then assert that NEL reports were or were not created,
+as required by the spec.
+
+The [support][] directory contains several images, each of which defines a
+particular "kind" of NEL policy (e.g., `include_subdomains` set vs unset, no
+policy at all, etc.).  The [support/nel.sub.js][] file contains helper
+JavaScript methods for requesting those images, so that the test cases
+themselves are more descriptive.
+
+[support]: support
+[support/nel.sub.js]: support/nel.sub.js
+
+## Avoiding spurious reports
+
+NEL policies apply to **all** future requests to the origin.  We therefore serve
+all of the test case's "infrastructure" (the test case itself,
+[support/report.py][] and [support/nel.sub.js][]) on a different origin than
+the requests that exercise the NEL implementation.  That ensures that we don't
+have to wade through NEL reports about the infrastructure when verifying the NEL
+reports about the requests that we care about.
+
+## Browser configuration
+
+You must configure your browser's Reporting implementation to upload reports for
+a request immediately.  The test cases do not currently have any timeouts; they
+assume that as soon as the Fetch API promise resolves, any NEL reports for the
+request have already been uploaded.
+
+## Test parallelism
+
+Because NEL policies are stored in a global cache in the user agent, we need to
+run the tests in this directory serially instead of in parallel.  We implement a
+simple spin-lock in [support/lock.py][] to ensure that only one test is allowed
+to perform any NEL-related requests at a time.
+
+[support/lock.py]: support/lock.py
+
+## CORS preflights
+
+Reporting uploads are subject to CORS preflights.  We want to test normal
+operation (when preflight requests succeed) as well as failures of the CORS
+preflight logic in the user agent.  To support this, our test collector is
+configured to always reject the CORS preflight for a single domain (www2), and
+to always grant the CORS preflight for all other test subdomains.
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/tests/network-error-logging/no-report-on-failed-cors-preflight.https.html
@@ -0,0 +1,26 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <title>
+    Test that NEL reports are not sent if the CORS preflight fails
+  </title>
+  <script src='/resources/testharness.js'></script>
+  <script src='/resources/testharnessreport.js'></script>
+  <script src='./support/nel.sub.js'></script>
+</head>
+<body>
+  <script>
+    nel_test(async t => {
+      // Make a request to a resource whose response headers include a NEL
+      // policy, but where the report uploader will reject the CORS preflight.
+      await fetchResourceWithBasicPolicy('www2');
+      // Because the CORS preflight is rejected, we should never receive a
+      // report about the request.
+      assert_false(await reportExists({
+        url: getURLForResourceWithBasicPolicy('www2'),
+        type: "network-error",
+      }));
+    });
+  </script>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/tests/network-error-logging/no-report-on-subdomain-404.https.html
@@ -0,0 +1,30 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <title>
+    Test that include_subdomains policies do NOT report HTTP errors
+  </title>
+  <script src='/resources/testharness.js'></script>
+  <script src='/resources/testharnessreport.js'></script>
+  <script src='./support/nel.sub.js'></script>
+</head>
+<body>
+  <script>
+    nel_test(async t => {
+      // Make a request to a resource whose response headers include an
+      // include_subdomains NEL policy.
+      await fetchResourceWithIncludeSubdomainsPolicy();
+      // Make a request to another resource on a subdomain of the above.  This
+      // resource doesn't exist, so the server should return a 404.
+      await fetchMissingResource('www');
+      // The include_subdomains policy that we just received should NOT cover
+      // the second request, since include_subdomains policies can only report
+      // on DNS errors.
+      assert_false(await reportExists({
+        url: getURLForMissingResource('www'),
+        type: "network-error",
+      }));
+    });
+  </script>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/tests/network-error-logging/no-report-on-subdomain-success.https.html
@@ -0,0 +1,30 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <title>
+    Test that include_subdomains policies do NOT report successful requests
+  </title>
+  <script src='/resources/testharness.js'></script>
+  <script src='/resources/testharnessreport.js'></script>
+  <script src='./support/nel.sub.js'></script>
+</head>
+<body>
+  <script>
+    nel_test(async t => {
+      // Make a request to a resource whose response headers include an
+      // include_subdomains NEL policy.
+      await fetchResourceWithIncludeSubdomainsPolicy();
+      // Make a request to another resource on a subdomain of the above, which
+      // does not define its own NEL policy.
+      await fetchResourceWithNoPolicy('www');
+      // The include_subdomains policy that we just received should NOT cover
+      // the second request, since include_subdomains policies can only report
+      // on DNS errors.
+      assert_false(await reportExists({
+        url: getURLForResourceWithNoPolicy('www'),
+        type: "network-error",
+      }));
+    });
+  </script>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/tests/network-error-logging/reports-are-not-observable.https.html
@@ -0,0 +1,29 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <title>
+    Test that NEL reports are not observable from JavaScript
+  </title>
+  <script src='/resources/testharness.js'></script>
+  <script src='/resources/testharnessreport.js'></script>
+  <script src='./support/nel.sub.js'></script>
+</head>
+<body>
+  <script>
+    nel_test(async t => {
+      // Register an observer for NEL reports.
+      var observer = new ReportingObserver((reports, _) => {
+        assert_unreached("NEL reports should not be observable");
+      }, {"types": ["network-error"]});
+      observer.observe();
+      // Make a request to a resource whose response headers include a NEL
+      // policy.  If NEL reports are observable, this will queue a task that
+      // calls the observer function above (which we don't want).
+      await fetchResourceWithBasicPolicy();
+      // Wait for one second to give any observer callback task a chance to
+      // fire.
+      await new Promise(resolve => t.step_timeout(resolve, 1000 /* msec */));
+    });
+  </script>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/tests/network-error-logging/sends-report-on-404.https.html
@@ -0,0 +1,42 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <title>
+    Test that NEL reports are sent for HTTP errors
+  </title>
+  <script src='/resources/testharness.js'></script>
+  <script src='/resources/testharnessreport.js'></script>
+  <script src='./support/nel.sub.js'></script>
+</head>
+<body>
+  <script>
+    nel_test(async t => {
+      // Make a request to a resource whose response headers include a NEL
+      // policy.
+      await fetchResourceWithBasicPolicy();
+      // Make a request to another resource on the same domain.  This resource
+      // doesn't exist, so the server should return a 404.
+      await fetchMissingResource();
+      // The 404 won't contain its own NEL policy, but the policy we received in
+      // the first request should cover the second request, too, since they're
+      // at the same origin, so the collector should have received a report
+      // about it.
+      assert_true(await reportExists({
+        url: getURLForMissingResource(),
+        user_agent: navigator.userAgent,
+        type: "network-error",
+        body: {
+          method: "GET",
+          sampling_fraction: 1.0,
+          status_code: 404,
+          phase: "application",
+          type: "http.error",
+        },
+        metadata: {
+          content_type: "application/reports+json",
+        },
+      }));
+    });
+  </script>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/tests/network-error-logging/sends-report-on-subdomain-dns-failure.https.html
@@ -0,0 +1,46 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <title>
+    Test that include_subdomains policies report DNS failures for subdomains
+  </title>
+  <script src='/resources/testharness.js'></script>
+  <script src='/resources/testharnessreport.js'></script>
+  <script src='./support/nel.sub.js'></script>
+</head>
+<body>
+  <script>
+    nel_test(async t => {
+      // Make a request to a resource whose response headers include an
+      // include_subdomains NEL policy.
+      await fetchResourceWithIncludeSubdomainsPolicy();
+      // Make a request to another resource on a nonexistent subdomain of the
+      // above.  Since the subdomain doesn't exist, the request should fail with
+      // a DNS error.
+      await fetchResourceWithNoPolicy('nonexistent').then((response) => {
+        assert_unreached("Request to nonexistent domain should fail");
+      }, (err) => {
+        // Silence the error, since it's expected.
+      });
+      // The include_subdomains policy that we just received should cover the
+      // second request, since include_subdomains policies can report on DNS
+      // errors, so the collector should have received a report about it.
+      assert_true(await reportExists({
+        url: getURLForResourceWithNoPolicy('nonexistent'),
+        user_agent: navigator.userAgent,
+        type: "network-error",
+        body: {
+          method: "GET",
+          sampling_fraction: 1.0,
+          status_code: 0,
+          phase: "dns",
+          type: "dns.name_not_resolved",
+        },
+        metadata: {
+          content_type: "application/reports+json",
+        },
+      }));
+    });
+  </script>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/tests/network-error-logging/sends-report-on-success-with-subdomain-policy.https.html
@@ -0,0 +1,40 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <title>
+    Test that NEL reports are sent for successful requests
+  </title>
+  <script src='/resources/testharness.js'></script>
+  <script src='/resources/testharnessreport.js'></script>
+  <script src='./support/nel.sub.js'></script>
+</head>
+<body>
+  <script>
+    nel_test(async t => {
+      // Make a request to a resource whose response headers include an
+      // include_subdomains NEL policy.
+      await fetchResourceWithIncludeSubdomainsPolicy();
+      // That policy should apply to the request that delivered it.  Even though
+      // the policy has include_subdomains set, it SHOULD generate a full,
+      // non-downgraded report about the request, since the request has the
+      // same origin as the policy.  (I.e., since the origins are the same, the
+      // include_subdomains setting is irrelevant.)
+      assert_true(await reportExists({
+        url: getURLForResourceWithIncludeSubdomainsPolicy(),
+        user_agent: navigator.userAgent,
+        type: "network-error",
+        body: {
+          method: "GET",
+          sampling_fraction: 1.0,
+          status_code: 200,
+          phase: "application",
+          type: "ok",
+        },
+        metadata: {
+          content_type: "application/reports+json",
+        },
+      }));
+    });
+  </script>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/tests/network-error-logging/sends-report-on-success.https.html
@@ -0,0 +1,37 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <title>
+    Test that NEL reports are sent for successful requests
+  </title>
+  <script src='/resources/testharness.js'></script>
+  <script src='/resources/testharnessreport.js'></script>
+  <script src='./support/nel.sub.js'></script>
+</head>
+<body>
+  <script>
+    nel_test(async t => {
+      // Make a request to a resource whose response headers include a NEL
+      // policy.
+      await fetchResourceWithBasicPolicy();
+      // That policy should apply to the request that delivered it, so the
+      // collector should have received a report about the request.
+      assert_true(await reportExists({
+        url: getURLForResourceWithBasicPolicy(),
+        user_agent: navigator.userAgent,
+        type: "network-error",
+        body: {
+          method: "GET",
+          sampling_fraction: 1.0,
+          status_code: 200,
+          phase: "application",
+          type: "ok",
+        },
+        metadata: {
+          content_type: "application/reports+json",
+        },
+      }));
+    });
+  </script>
+</body>
+</html>
new file mode 100644
index 0000000000000000000000000000000000000000..2fa1e0ac0663a65deae6602621521cc2844b93de
GIT binary patch
literal 1689
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
Vg^jxe1<?-yj`nVL%{HMK{{w#RF>?R_
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/tests/network-error-logging/support/clear-policy-pass.png.sub.headers
@@ -0,0 +1,6 @@
+Expires: Mon, 26 Jul 1997 05:00:00 GMT
+Cache-Control: no-store, no-cache, must-revalidate
+Cache-Control: post-check=0, pre-check=0, false
+Pragma: no-cache
+Report-To: { "group": "nel-group", "max_age": 0, "endpoints": [] }
+NEL: {"max_age": 0}
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/tests/network-error-logging/support/lock.py
@@ -0,0 +1,38 @@
+_LOCK_KEY = "network-error-logging:lock"
+_TIMEOUT = 5  # seconds
+
+def wait_for_lock(request):
+  t0 = time.time()
+  while time.time() - t0 < _TIMEOUT:
+    time.sleep(0.5)
+    value = request.server.stash.take(key=_LOCK_KEY)
+    if value is None:
+      return True
+  return False
+
+def lock(request, report_id):
+  with request.server.stash.lock:
+    # Loop until the lock is free
+    if not wait_for_lock(request):
+      return (503, [], "Cannot obtain lock")
+    request.server.stash.put(key=_LOCK_KEY, value=report_id)
+    return "Obtained lock for %s" % report_id
+
+def unlock(request, report_id):
+  with request.server.stash.lock:
+    lock_holder = request.server.stash.take(key=_LOCK_KEY)
+    if lock_holder != request_id:
+      # Return the lock holder to the stash
+      request.server.stash.put(key=_LOCK_KEY, value=lock_holder)
+      return (503, [], "Cannot release lock held by %s" % lock_holder)
+  return "Released lock for %s" % report_id
+
+def main(request, response):
+  op = request.GET.first("op")
+  report_id = request.GET.first("reportID")
+  if op == "lock":
+    return lock(request, report_id)
+  elif op == "unlock":
+    return unlock(request, report_id)
+  else:
+    return (400, [], "Invalid op")
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/tests/network-error-logging/support/nel.sub.js
@@ -0,0 +1,169 @@
+const reportID = "{{$id:uuid()}}";
+
+/*
+ * NEL tests have to run serially, since the user agent maintains a global cache
+ * of Reporting and NEL policies, and we don't want the policies for multiple
+ * tests to interfere with each other.  These functions (along with a Python
+ * handler in lock.py) implement a simple spin lock.
+ */
+
+function obtainNELLock() {
+  return fetch("/network-error-logging/support/lock.py?op=lock&reportID=" + reportID);
+}
+
+function releaseNELLock() {
+  return fetch("/network-error-logging/support/lock.py?op=unlock&reportID=" + reportID);
+}
+
+function nel_test(callback, name, properties) {
+  promise_test(async t => {
+    await obtainNELLock();
+    await clearReportingAndNELConfigurations();
+    await callback(t);
+    await releaseNELLock();
+  }, name, properties);
+}
+
+/*
+ * Helper functions for constructing domain names that contain NEL policies.
+ */
+function _monitoredDomain(subdomain) {
+  if (subdomain == "www") {
+    return "{{hosts[alt][www]}}"
+  } else if (subdomain == "www1") {
+    return "{{hosts[alt][www1]}}"
+  } else if (subdomain == "www2") {
+    return "{{hosts[alt][www2]}}"
+  } else if (subdomain == "nonexistent") {
+    return "{{hosts[alt][nonexistent]}}"
+  } else {
+    return "{{hosts[alt][]}}"
+  }
+}
+
+function _getNELResourceURL(subdomain, suffix) {
+  return "https://" + _monitoredDomain(subdomain) +
+    ":{{ports[https][0]}}/network-error-logging/support/" + suffix;
+}
+
+/*
+ * Fetches a resource whose headers define a basic NEL policy (i.e., with no
+ * include_subdomains flag).  We ensure that we request the resource from a
+ * different origin than is used for the main test case HTML file or for report
+ * uploads.  This minimizes the number of reports that are generated for this
+ * policy.
+ */
+
+function getURLForResourceWithBasicPolicy(subdomain) {
+  return _getNELResourceURL(subdomain, "pass.png?id="+reportID);
+}
+
+function fetchResourceWithBasicPolicy(subdomain) {
+  const url = getURLForResourceWithBasicPolicy(subdomain);
+  return fetch(url, {mode: "no-cors"});
+}
+
+/*
+ * Fetches a resource whose headers define an include_subdomains NEL policy.
+ */
+
+function getURLForResourceWithIncludeSubdomainsPolicy(subdomain) {
+  return _getNELResourceURL(subdomain, "subdomains-pass.png?id="+reportID);
+}
+
+function fetchResourceWithIncludeSubdomainsPolicy(subdomain) {
+  const url = getURLForResourceWithIncludeSubdomainsPolicy(subdomain);
+  return fetch(url, {mode: "no-cors"});
+}
+
+/*
+ * Fetches a resource whose headers do NOT define a NEL policy.  This may or may
+ * not generate a NEL report, depending on whether you've already successfully
+ * requested a resource from the same origin that included a NEL policy.
+ */
+
+function getURLForResourceWithNoPolicy(subdomain) {
+  return _getNELResourceURL(subdomain, "no-policy-pass.png");
+}
+
+function fetchResourceWithNoPolicy(subdomain) {
+  const url = getURLForResourceWithNoPolicy(subdomain);
+  return fetch(url, {mode: "no-cors"});
+}
+
+/*
+ * Fetches a resource that doesn't exist.  This may or may not generate a NEL
+ * report, depending on whether you've already successfully requested a resource
+ * from the same origin that included a NEL policy.
+ */
+
+function getURLForMissingResource(subdomain) {
+  return _getNELResourceURL(subdomain, "nonexistent.png");
+}
+
+function fetchMissingResource(subdomain) {
+  const url = getURLForMissingResource(subdomain);
+  return fetch(url, {mode: "no-cors"});
+}
+
+/*
+ * Fetches resources that clear out any existing Reporting or NEL configurations
+ * for all origins that any test case might use.
+ */
+
+function getURLForClearingConfiguration(subdomain) {
+  return _getNELResourceURL(subdomain, "clear-pass.png?id="+reportID);
+}
+
+async function clearReportingAndNELConfigurations(subdomain) {
+  await Promise.all([
+    fetch(getURLForClearingConfiguration(""), {mode: "no-cors"}),
+    fetch(getURLForClearingConfiguration("www"), {mode: "no-cors"}),
+    fetch(getURLForClearingConfiguration("www1"), {mode: "no-cors"}),
+    fetch(getURLForClearingConfiguration("www2"), {mode: "no-cors"}),
+  ]);
+  return;
+}
+
+/*
+ * Returns whether all of the fields in obj1 also exist in obj2 with the same
+ * values.  (Put another way, returns whether obj1 and obj2 are equal, ignoring
+ * any extra fields in obj2.)
+ */
+
+function _isSubsetOf(obj1, obj2) {
+  for (const prop in obj1) {
+    if (typeof obj1[prop] === 'object') {
+      if (typeof obj2[prop] !== 'object') {
+        return false;
+      }
+      if (!_isSubsetOf(obj1[prop], obj2[prop])) {
+        return false;
+      }
+    } else if (obj1[prop] != obj2[prop]) {
+      return false;
+    }
+  }
+  return true;
+}
+
+/*
+ * Verifies that a report was uploaded that contains all of the fields in
+ * expected.
+ */
+
+async function reportExists(expected) {
+  var timeout =
+    document.querySelector("meta[name=timeout][content=long]") ? 50 : 1;
+  var reportLocation =
+    "/network-error-logging/support/report.py?op=retrieve_report&timeout=" +
+    timeout + "&reportID=" + reportID;
+  const response = await fetch(reportLocation);
+  const json = await response.json();
+  for (const report of json) {
+    if (_isSubsetOf(expected, report)) {
+      return true;
+    }
+  }
+  return false;
+}
new file mode 100644
index 0000000000000000000000000000000000000000..2fa1e0ac0663a65deae6602621521cc2844b93de
GIT binary patch
literal 1689
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
Vg^jxe1<?-yj`nVL%{HMK{{w#RF>?R_
new file mode 100644
index 0000000000000000000000000000000000000000..2fa1e0ac0663a65deae6602621521cc2844b93de
GIT binary patch
literal 1689
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
Vg^jxe1<?-yj`nVL%{HMK{{w#RF>?R_
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/tests/network-error-logging/support/pass.png.sub.headers
@@ -0,0 +1,6 @@
+Expires: Mon, 26 Jul 1997 05:00:00 GMT
+Cache-Control: no-store, no-cache, must-revalidate
+Cache-Control: post-check=0, pre-check=0, false
+Pragma: no-cache
+Report-To: { "group": "nel-group", "max_age": 10886400, "endpoints": [{ "url": "https://{{hosts[][www]}}:{{ports[https][0]}}/network-error-logging/support/report.py?op=put&reportID={{GET[id]}}" }] }
+NEL: {"report_to": "nel-group", "max_age": 10886400, "success_fraction": 1.0}
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/tests/network-error-logging/support/report.py
@@ -0,0 +1,52 @@
+import time
+import json
+import re
+
+def retrieve_from_stash(request, key, timeout, default_value):
+  t0 = time.time()
+  while time.time() - t0 < timeout:
+    time.sleep(0.5)
+    value = request.server.stash.take(key=key)
+    if value is not None:
+      return json.dumps(value)
+
+  return default_value
+
+def main(request, response):
+  # Handle CORS preflight requests
+  if request.method == 'OPTIONS':
+    # Always reject preflights for one subdomain
+    if "www2" in request.headers["Origin"]:
+      return (400, [], "CORS preflight rejected for www2")
+    return [
+      ("Content-Type", "text/plain"),
+      ("Access-Control-Allow-Origin", "*"),
+      ("Access-Control-Allow-Methods", "post"),
+      ("Access-Control-Allow-Headers", "Content-Type"),
+    ], "CORS allowed"
+
+  op = request.GET.first("op");
+  key = request.GET.first("reportID")
+
+  if op == "retrieve_report":
+    try:
+      timeout = float(request.GET.first("timeout"))
+    except:
+      timeout = 0.5
+    return [("Content-Type", "application/json")], retrieve_from_stash(request, key, timeout, '[]')
+
+  # append new reports
+  new_reports = json.loads(request.body)
+  for report in new_reports:
+    report["metadata"] = {
+      "content_type": request.headers["Content-Type"],
+    }
+  with request.server.stash.lock:
+    reports = request.server.stash.take(key=key)
+    if reports is None:
+      reports = []
+    reports.extend(new_reports)
+    request.server.stash.put(key=key, value=reports)
+
+  # return acknowledgement report
+  return [("Content-Type", "text/plain")], "Recorded report"
new file mode 100644
index 0000000000000000000000000000000000000000..2fa1e0ac0663a65deae6602621521cc2844b93de
GIT binary patch
literal 1689
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
Vg^jxe1<?-yj`nVL%{HMK{{w#RF>?R_
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/tests/network-error-logging/support/subdomains-pass.png.sub.headers
@@ -0,0 +1,6 @@
+Expires: Mon, 26 Jul 1997 05:00:00 GMT
+Cache-Control: no-store, no-cache, must-revalidate
+Cache-Control: post-check=0, pre-check=0, false
+Pragma: no-cache
+Report-To: { "group": "nel-group", "max_age": 10886400, "include_subdomains": true, "endpoints": [{ "url": "https://{{hosts[][www]}}:{{ports[https][0]}}/network-error-logging/support/report.py?op=put&reportID={{GET[id]}}" }] }
+NEL: {"report_to": "nel-group", "max_age": 10886400, "include_subdomains": true, "success_fraction": 1.0}