Add basic CSP Early Hints WPTs

The test scenario is that:
- Navigate to the test page. The test page sends an Early Hints response
  to perform a cross origin preload with a CSP.
- The page sends the final response with a CSP.
- CSPs are configured to allow or disallow cross origin fetches, or
  may be absent.
- The test page checks whether the preload is allowed/disallowed.

Currently disallowed -> allowed cases are failing. Subsequent CLs would
resolve the failures.

Bug: 1305896
Change-Id: I3a9ca4a06fe0b1c48f5a93c8881750295510886c
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/3527873
Reviewed-by: Yutaka Hirano <yhirano@chromium.org>
Commit-Queue: Kenichi Ishibashi <bashi@chromium.org>
Cr-Commit-Position: refs/heads/main@{#982123}
diff --git a/loading/early-hints/csp-early-hints-absent-final-absent.h2.window.js b/loading/early-hints/csp-early-hints-absent-final-absent.h2.window.js
new file mode 100644
index 0000000..f61e268
--- /dev/null
+++ b/loading/early-hints/csp-early-hints-absent-final-absent.h2.window.js
@@ -0,0 +1,8 @@
+// META: script=/common/utils.js
+// META: script=resources/early-hints-helpers.sub.js
+
+test(() => {
+    const early_hints_policy = "absent";
+    const final_policy = "absent";
+    navigateToContentSecurityPolicyBasicTest(early_hints_policy, final_policy);
+});
diff --git a/loading/early-hints/csp-early-hints-absent-final-allowed.h2.window.js b/loading/early-hints/csp-early-hints-absent-final-allowed.h2.window.js
new file mode 100644
index 0000000..0e1762a
--- /dev/null
+++ b/loading/early-hints/csp-early-hints-absent-final-allowed.h2.window.js
@@ -0,0 +1,8 @@
+// META: script=/common/utils.js
+// META: script=resources/early-hints-helpers.sub.js
+
+test(() => {
+    const early_hints_policy = "absent";
+    const final_policy = "allowed";
+    navigateToContentSecurityPolicyBasicTest(early_hints_policy, final_policy);
+});
diff --git a/loading/early-hints/csp-early-hints-absent-final-disallowed.h2.window.js b/loading/early-hints/csp-early-hints-absent-final-disallowed.h2.window.js
new file mode 100644
index 0000000..3fcd89c
--- /dev/null
+++ b/loading/early-hints/csp-early-hints-absent-final-disallowed.h2.window.js
@@ -0,0 +1,8 @@
+// META: script=/common/utils.js
+// META: script=resources/early-hints-helpers.sub.js
+
+test(() => {
+    const early_hints_policy = "absent";
+    const final_policy = "disallowed";
+    navigateToContentSecurityPolicyBasicTest(early_hints_policy, final_policy);
+});
diff --git a/loading/early-hints/csp-early-hints-allowed-final-absent.h2.window.js b/loading/early-hints/csp-early-hints-allowed-final-absent.h2.window.js
new file mode 100644
index 0000000..15128ce
--- /dev/null
+++ b/loading/early-hints/csp-early-hints-allowed-final-absent.h2.window.js
@@ -0,0 +1,8 @@
+// META: script=/common/utils.js
+// META: script=resources/early-hints-helpers.sub.js
+
+test(() => {
+    const early_hints_policy = "allowed";
+    const final_policy = "absent";
+    navigateToContentSecurityPolicyBasicTest(early_hints_policy, final_policy);
+});
diff --git a/loading/early-hints/csp-early-hints-allowed-final-allowed.h2.window.js b/loading/early-hints/csp-early-hints-allowed-final-allowed.h2.window.js
new file mode 100644
index 0000000..ee51e78
--- /dev/null
+++ b/loading/early-hints/csp-early-hints-allowed-final-allowed.h2.window.js
@@ -0,0 +1,8 @@
+// META: script=/common/utils.js
+// META: script=resources/early-hints-helpers.sub.js
+
+test(() => {
+    const early_hints_policy = "allowed";
+    const final_policy = "allowed";
+    navigateToContentSecurityPolicyBasicTest(early_hints_policy, final_policy);
+});
diff --git a/loading/early-hints/csp-early-hints-allowed-final-disallowed.h2.window.js b/loading/early-hints/csp-early-hints-allowed-final-disallowed.h2.window.js
new file mode 100644
index 0000000..67b3933
--- /dev/null
+++ b/loading/early-hints/csp-early-hints-allowed-final-disallowed.h2.window.js
@@ -0,0 +1,8 @@
+// META: script=/common/utils.js
+// META: script=resources/early-hints-helpers.sub.js
+
+test(() => {
+    const early_hints_policy = "allowed";
+    const final_policy = "disallowed";
+    navigateToContentSecurityPolicyBasicTest(early_hints_policy, final_policy);
+});
diff --git a/loading/early-hints/csp-early-hints-disallowed-final-absent.h2.window.js b/loading/early-hints/csp-early-hints-disallowed-final-absent.h2.window.js
new file mode 100644
index 0000000..80b563d
--- /dev/null
+++ b/loading/early-hints/csp-early-hints-disallowed-final-absent.h2.window.js
@@ -0,0 +1,8 @@
+// META: script=/common/utils.js
+// META: script=resources/early-hints-helpers.sub.js
+
+test(() => {
+    const early_hints_policy = "disallowed";
+    const final_policy = "absent";
+    navigateToContentSecurityPolicyBasicTest(early_hints_policy, final_policy);
+});
diff --git a/loading/early-hints/csp-early-hints-disallowed-final-allowed.h2.window.js b/loading/early-hints/csp-early-hints-disallowed-final-allowed.h2.window.js
new file mode 100644
index 0000000..cfac9b0
--- /dev/null
+++ b/loading/early-hints/csp-early-hints-disallowed-final-allowed.h2.window.js
@@ -0,0 +1,8 @@
+// META: script=/common/utils.js
+// META: script=resources/early-hints-helpers.sub.js
+
+test(() => {
+    const early_hints_policy = "disallowed";
+    const final_policy = "allowed";
+    navigateToContentSecurityPolicyBasicTest(early_hints_policy, final_policy);
+});
diff --git a/loading/early-hints/csp-early-hints-disallowed-final-disallowed.h2.window.js b/loading/early-hints/csp-early-hints-disallowed-final-disallowed.h2.window.js
new file mode 100644
index 0000000..c8a7ca3
--- /dev/null
+++ b/loading/early-hints/csp-early-hints-disallowed-final-disallowed.h2.window.js
@@ -0,0 +1,8 @@
+// META: script=/common/utils.js
+// META: script=resources/early-hints-helpers.sub.js
+
+test(() => {
+    const early_hints_policy = "disallowed";
+    const final_policy = "disallowed";
+    navigateToContentSecurityPolicyBasicTest(early_hints_policy, final_policy);
+});
diff --git a/loading/early-hints/resources/csp-basic-loader.h2.py b/loading/early-hints/resources/csp-basic-loader.h2.py
new file mode 100644
index 0000000..080901e
--- /dev/null
+++ b/loading/early-hints/resources/csp-basic-loader.h2.py
@@ -0,0 +1,47 @@
+import os
+
+
+def _calculate_csp_value(policy, resource_origin):
+    if policy == "absent":
+        return None
+    elif policy == "allowed":
+        return "script-src 'self' 'unsafe-inline' {}".format(resource_origin)
+    elif policy == "disallowed":
+        return "script-src 'self' 'unsafe-inline'"
+    else:
+        return None
+
+
+def handle_headers(frame, request, response):
+    resource_origin = request.GET.first(b"resource-origin").decode()
+
+    # Send a 103 response.
+    resource_url = request.GET.first(b"resource-url").decode()
+    link_header_value = "<{}>; rel=preload; as=script".format(resource_url)
+    early_hints = [
+        (b":status", b"103"),
+        (b"link", link_header_value),
+    ]
+    early_hints_csp = _calculate_csp_value(
+        request.GET.first(b"early-hints-policy").decode(), resource_origin)
+    if early_hints_csp:
+        early_hints.append((b"content-security-policy", early_hints_csp))
+    response.writer.write_raw_header_frame(headers=early_hints,
+                                           end_headers=True)
+
+    # Send the final response header.
+    response.status = 200
+    response.headers["content-type"] = "text/html"
+    final_csp = _calculate_csp_value(
+        request.GET.first(b"final-policy").decode(), resource_origin)
+    if final_csp:
+        response.headers["content-security-policy"] = final_csp
+    response.write_status_headers()
+
+
+def main(request, response):
+    current_dir = os.path.dirname(os.path.realpath(__file__))
+    file_path = os.path.join(current_dir, "csp-basic.html")
+    with open(file_path, "r") as f:
+        test_content = f.read()
+    response.writer.write_data(item=test_content, last=True)
diff --git a/loading/early-hints/resources/csp-basic.html b/loading/early-hints/resources/csp-basic.html
new file mode 100644
index 0000000..0086711
--- /dev/null
+++ b/loading/early-hints/resources/csp-basic.html
@@ -0,0 +1,37 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="early-hints-helpers.sub.js"></script>
+<body>
+<script>
+const SEARCH_PARAMS = new URLSearchParams(window.location.search);
+const EARLY_HINTS_POLICY = SEARCH_PARAMS.get("early-hints-policy");
+const FINAL_POLICY = SEARCH_PARAMS.get("final-policy");
+
+function isResourceAllowed() {
+    return FINAL_POLICY === "absent" || FINAL_POLICY === "allowed";
+}
+
+function shouldEarlyHintsPreloadResource() {
+    return EARLY_HINTS_POLICY === "absent" || EARLY_HINTS_POLICY == "allowed";
+}
+
+promise_test(async (t) => {
+    const resource_url = SEARCH_PARAMS.get("resource-url");
+    if (isResourceAllowed()) {
+        await fetchScript(resource_url);
+        const early_hints_preloaded = isPreloadedByEarlyHints(resource_url);
+        const should_early_hints_preload = shouldEarlyHintsPreloadResource();
+        const description = "Early Hints " +
+            (early_hints_preloaded ? "preloaded" : "didn't preload") +
+            " the resource, should " +
+            (should_early_hints_preload ? "" : "not ") + "preload.";
+        assert_equals(early_hints_preloaded, should_early_hints_preload,
+            description);
+    } else {
+        await promise_rejects_js(t, Error, fetchScript(resource_url));
+    }
+}, `Early Hints CSP: Early Hints policy = ${EARLY_HINTS_POLICY}, final response policy = ${FINAL_POLICY}.`);
+</script>
+</body>
diff --git a/loading/early-hints/resources/early-hints-helpers.sub.js b/loading/early-hints/resources/early-hints-helpers.sub.js
index 5c1b219..33f80bc 100644
--- a/loading/early-hints/resources/early-hints-helpers.sub.js
+++ b/loading/early-hints/resources/early-hints-helpers.sub.js
@@ -49,11 +49,12 @@
  *
  * @param {string} url
  */
- async function fetchScript(url) {
-    return new Promise((resolve) => {
+async function fetchScript(url) {
+    return new Promise((resolve, reject) => {
         const el = document.createElement("script");
         el.src = url;
         el.onload = resolve;
+        el.onerror = _ => reject(new Error("Failed to fetch script"));
         document.body.appendChild(el);
     });
 }
@@ -66,6 +67,9 @@
  */
 function isPreloadedByEarlyHints(url) {
     const entries = performance.getEntriesByName(url);
+    if (entries.length === 0) {
+        return false;
+    }
     assert_equals(entries.length, 1);
     return entries[0].initiatorType === "early-hints";
 }
@@ -87,3 +91,28 @@
     const url = new URL(path, window.location);
     window.location.replace(url);
 }
+
+/**
+ * Navigate to the content security policy basic test. The test page sends an
+ * Early Hints response with a cross origin resource preload. CSP headers are
+ * configured based on the given policies. A policy should be one of the
+ * followings:
+ *   "absent" - Do not send Content-Security-Policy header
+ *   "allowed" - Set Content-Security-Policy to allow the cross origin preload
+ *   "disallowed" - Set Content-Security-Policy to disallow the cross origin  preload
+ *
+ * @param {string} early_hints_policy - The policy for the Early Hints response
+ * @param {string} final_policy - The policy for the final response
+ */
+function navigateToContentSecurityPolicyBasicTest(
+    early_hints_policy, final_policy) {
+    const params = new URLSearchParams();
+    params.set("resource-origin", CROSS_ORIGIN);
+    params.set("resource-url",
+        CROSS_ORIGIN_RESOURCES_URL + "/empty.js?" + token());
+    params.set("early-hints-policy", early_hints_policy);
+    params.set("final-policy", final_policy);
+
+    const url = "resources/csp-basic-loader.h2.py?" + params.toString();
+    window.location.replace(new URL(url, window.location));
+}