[COOP access reporting] Preliminary WPT tests.

Add some basic WPT tests about the COOP access reporting feature.
No web browsers actually implement this. As a result, chrome do not
pass them yet.
The tests aren't complete yet, they will evolve along the specification
and the implementations.

Bug: 922191
Change-Id: Ie4675e5fb5ec0f839ca1527c15fafbb456925a0d
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2228884
Reviewed-by: Pâris Meuleman <pmeuleman@chromium.org>
Commit-Queue: Arthur Sonzogni <arthursonzogni@chromium.org>
Cr-Commit-Position: refs/heads/master@{#775020}
diff --git a/html/cross-origin-opener-policy/access-reporting/META.yml b/html/cross-origin-opener-policy/access-reporting/META.yml
new file mode 100644
index 0000000..0db2820
--- /dev/null
+++ b/html/cross-origin-opener-policy/access-reporting/META.yml
@@ -0,0 +1,6 @@
+suggested_reviewers:
+  - ArthurSonzogni
+  - ParisMeuleman
+  - camillelamy
+  - hemeryar
+  - mikewest
diff --git a/html/cross-origin-opener-policy/access-reporting/openee-accessed_openee-coop-ro.https.html b/html/cross-origin-opener-policy/access-reporting/openee-accessed_openee-coop-ro.https.html
new file mode 100644
index 0000000..2f4ea87
--- /dev/null
+++ b/html/cross-origin-opener-policy/access-reporting/openee-accessed_openee-coop-ro.https.html
@@ -0,0 +1,84 @@
+<title>
+  COOP reports are sent when the openee used COOP-RO+COEP and then its opener
+  tries to access it.
+</title>
+<meta name=timeout content=long>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src=/common/get-host-info.sub.js></script>
+<script src="/common/utils.js"></script>
+<script src="./resources/dispatcher.js"></script>
+<script>
+
+const directory = "/html/cross-origin-opener-policy/access-reporting";
+const executor_path = directory + "/resources/executor.html?pipe=";
+const cross_origin = get_host_info().HTTPS_REMOTE_ORIGIN;
+const coep_header = '|header(Cross-Origin-Embedder-Policy,require-corp)';
+
+let operation = [
+//[test name           , operation                   ] ,
+  ["Call blur"         , w => w.blur()               ] ,
+  ["Call foo"          , w => w.foo()                ] ,
+  ["Call location"     , w => w.location()           ] ,
+  ["Call opener"       , w => w.opener()             ] ,
+  ["Call postMessage"  , w => w.postMessage()        ] ,
+  ["Call window"       , w => w.window()             ] ,
+  ["Read blur"         , w => w.blur                 ] ,
+  ["Read foo"          , w => w.foo                  ] ,
+  ["Read location"     , w => w.location             ] ,
+  ["Read opener"       , w => w.opener               ] ,
+  ["Read postMessage"  , w => w.postMessage          ] ,
+  ["Read window"       , w => w.window               ] ,
+  ["Write blur"        , w => w.blur = "test"        ] ,
+  ["Write foo"         , w => w.foo = "test"         ] ,
+  ["Write location"    , w => w.location = "test"    ] ,
+  ["Write opener"      , w => w.opener = "test"      ] ,
+  ["Write postMessage" , w => w.postMessage = "test" ] ,
+  ["Write window"      , w => w.window = "test"      ] ,
+];
+
+operation.forEach(([test, op]) => {
+  promise_test(async t => {
+    const report_token = token();
+    const executor_token = token();
+    const callback_token = token();
+
+    const reportTo = reportToHeaders(report_token);
+    const openee_url = cross_origin + executor_path +
+      reportTo.header + reportTo.coopReportOnlySameOrigin + coep_header +
+      `&uuid=${executor_token}`;
+    const openee = window.open(openee_url);
+    t.add_cleanup(() => send(executor_token, "window.close()"))
+
+    // 1. Make sure the new document to be loaded.
+    send(executor_token, `
+      send("${callback_token}", "Ready");
+    `);
+    let reply = await receive(callback_token);
+    assert_equals(reply, "Ready");
+
+    // 2. Skip the first report about the opener breakage.
+    let report_1 = await receive(report_token);
+    assert_not_equals(report_1, "timeout");
+    report_1 = JSON.parse(report_1);
+    assert_equals(report_1.length, 1);
+    assert_equals(report_1[0].type, "coop");
+    assert_equals(report_1[0].body["violation-type"], "navigation-to-document");
+    assert_equals(report_1[0].body["disposition"], "reporting");
+
+    // 3. Try to access the openee. A report is sent, because of COOP-RO+COEP.
+    try {op(openee)} catch(e) {}
+
+    // 4. A COOP access reports must be sent as a result of (3).
+    let report_2 = await receive(report_token);
+    assert_not_equals(report_1, "timeout");
+    report_2 = JSON.parse(report_2);
+    assert_equals(report_2.length, 1);
+    assert_equals(report_2[0].type, "coop");
+    assert_equals(report_2[0].body["violation-type"], "access");
+    assert_equals(report_2[0].body["disposition"], "reporting");
+    assert_equals(report_2[0].body["effective-policy"], "same-origin-plus-coep");
+  }, `${test}`);
+});
+
+</script>
diff --git a/html/cross-origin-opener-policy/access-reporting/openee-accessed_openee-coop.https.html b/html/cross-origin-opener-policy/access-reporting/openee-accessed_openee-coop.https.html
new file mode 100644
index 0000000..07fd43b
--- /dev/null
+++ b/html/cross-origin-opener-policy/access-reporting/openee-accessed_openee-coop.https.html
@@ -0,0 +1,84 @@
+<title>
+  COOP reports are sent when the openee used COOP+COEP and then its opener
+  tries to access it.
+</title>
+<meta name=timeout content=long>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src=/common/get-host-info.sub.js></script>
+<script src="/common/utils.js"></script>
+<script src="./resources/dispatcher.js"></script>
+<script>
+
+const directory = "/html/cross-origin-opener-policy/access-reporting";
+const executor_path = directory + "/resources/executor.html?pipe=";
+const cross_origin = get_host_info().HTTPS_REMOTE_ORIGIN;
+const coep_header = '|header(Cross-Origin-Embedder-Policy,require-corp)';
+
+let operation = [
+//[test name           , operation                   ] ,
+  ["Call blur"         , w => w.blur()               ] ,
+  ["Call foo"          , w => w.foo()                ] ,
+  ["Call location"     , w => w.location()           ] ,
+  ["Call opener"       , w => w.opener()             ] ,
+  ["Call postMessage"  , w => w.postMessage()        ] ,
+  ["Call window"       , w => w.window()             ] ,
+  ["Read blur"         , w => w.blur                 ] ,
+  ["Read foo"          , w => w.foo                  ] ,
+  ["Read location"     , w => w.location             ] ,
+  ["Read opener"       , w => w.opener               ] ,
+  ["Read postMessage"  , w => w.postMessage          ] ,
+  ["Read window"       , w => w.window               ] ,
+  ["Write blur"        , w => w.blur = "test"        ] ,
+  ["Write foo"         , w => w.foo = "test"         ] ,
+  ["Write location"    , w => w.location = "test"    ] ,
+  ["Write opener"      , w => w.opener = "test"      ] ,
+  ["Write postMessage" , w => w.postMessage = "test" ] ,
+  ["Write window"      , w => w.window = "test"      ] ,
+];
+
+operation.forEach(([test, op]) => {
+  promise_test(async t => {
+    const report_token = token();
+    const executor_token = token();
+    const callback_token = token();
+
+    const reportTo = reportToHeaders(report_token);
+    const openee_url = cross_origin + executor_path +
+      reportTo.header + reportTo.coopSameOrigin + coep_header +
+      `&uuid=${executor_token}`;
+    const openee = window.open(openee_url);
+    t.add_cleanup(() => send(executor_token, "window.close()"))
+
+    // 1. Make sure the new document to be loaded.
+    send(executor_token, `
+      send("${callback_token}", "Ready");
+    `);
+    let reply = await receive(callback_token);
+    assert_equals(reply, "Ready");
+
+    // 2. Skip the first report about the opener breakage.
+    let report_1 = await receive(report_token);
+    assert_not_equals(report_1, "timeout");
+    report_1 = JSON.parse(report_1);
+    assert_equals(report_1.length, 1);
+    assert_equals(report_1[0].type, "coop");
+    assert_equals(report_1[0].body["violation-type"], "navigation-to-document");
+    assert_equals(report_1[0].body["disposition"], "enforce");
+
+    // 3. Try to access the openee. This shouldn't work because of COOP+COEP.
+    try {op(openee)} catch(e) {}
+
+    // 4. A COOP access reports must be sent as a result of (3).
+    let report_2 = await receive(report_token);
+    assert_not_equals(report_2, "timeout");
+    report_2 = JSON.parse(report_2);
+    assert_equals(report_2.length, 1);
+    assert_equals(report_2[0].type, "coop");
+    assert_equals(report_2[0].body["violation-type"], "access");
+    assert_equals(report_2[0].body["disposition"], "enforce");
+    assert_equals(report_2[0].body["effective-policy"], "same-origin-plus-coep");
+  }, `${test}`);
+});
+
+</script>
diff --git a/html/cross-origin-opener-policy/access-reporting/opener-accessed_openee-coop-ro.https.html b/html/cross-origin-opener-policy/access-reporting/opener-accessed_openee-coop-ro.https.html
new file mode 100644
index 0000000..c942b9c
--- /dev/null
+++ b/html/cross-origin-opener-policy/access-reporting/opener-accessed_openee-coop-ro.https.html
@@ -0,0 +1,83 @@
+<title>
+  COOP reports are sent when the openee used COOP-RO+COEP and then tries to
+  access its opener.
+</title>
+<meta name=timeout content=long>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src=/common/get-host-info.sub.js></script>
+<script src="/common/utils.js"></script>
+<script src="./resources/dispatcher.js"></script>
+<script>
+
+const directory = "/html/cross-origin-opener-policy/access-reporting";
+const executor_path = directory + "/resources/executor.html?pipe=";
+const cross_origin = get_host_info().HTTPS_REMOTE_ORIGIN;
+const coep_header = '|header(Cross-Origin-Embedder-Policy,require-corp)';
+
+let operation = [
+//[test name           , operation                     ]   ,
+  ["Call blur"         , "opener.blur()"               ] ,
+  ["Call foo"          , "opener.foo()"                ] ,
+  ["Call location"     , "opener.location()"           ] ,
+  ["Call opener"       , "opener.opener()"             ] ,
+  ["Call postMessage"  , "opener.postMessage()"        ] ,
+  ["Call window"       , "opener.window()"             ] ,
+  ["Read blur"         , "opener.blur"                 ] ,
+  ["Read foo"          , "opener.foo"                  ] ,
+  ["Read location"     , "opener.location"             ] ,
+  ["Read opener"       , "opener.opener"               ] ,
+  ["Read postMessage"  , "opener.postMessage"          ] ,
+  ["Read window"       , "opener.window"               ] ,
+  ["Write blur"        , "opener.blur = 'test'"        ]  ,
+  ["Write foo"         , "opener.foo = 'test'"         ]  ,
+  ["Write location"    , "opener.location = 'test'"    ]  ,
+  ["Write opener"      , "opener.opener = 'test'"      ]  ,
+  ["Write postMessage" , "opener.postMessage = 'test'" ]  ,
+  ["Write window"      , "opener.window = 'test'"      ]  ,
+];
+
+operation.forEach(([test, op]) => {
+  promise_test(async t => {
+    const report_token = token();
+    const executor_token = token();
+    const callback_token = token();
+
+    const reportTo = reportToHeaders(report_token);
+    const openee_url = cross_origin + executor_path +
+      reportTo.header + reportTo.coopReportOnlySameOrigin + coep_header +
+      `&uuid=${executor_token}`;
+    const openee = window.open(openee_url);
+    t.add_cleanup(() => send(executor_token, "window.close()"))
+
+    // 1. Skip the first report about the opener breakage.
+    let report_1 = await receive(report_token);
+    assert_not_equals(report_1, "timeout");
+    report_1 = JSON.parse(report_1);
+    assert_equals(report_1.length, 1);
+    assert_equals(report_1[0].type, "coop");
+    assert_equals(report_1[0].body["violation-type"], "navigation-to-document");
+    assert_equals(report_1[0].body["disposition"], "reporting");
+
+    // 3. Try to access the opener. A report is sent, because of COOP-RO+COEP.
+    send(executor_token, `
+      try {${op}} catch(e) {}
+      send("${callback_token}", "Done");
+    `);
+    let reply = await receive(callback_token);
+    assert_equals(reply, "Done");
+
+    // 4. A COOP access reports must be sent as a result of (3).
+    let report_2 = await receive(report_token);
+    assert_not_equals(report_2, "timeout");
+    report_2 = JSON.parse(report_2);
+
+    assert_equals(report_2.length, 1);
+    assert_equals(report_2[0].type, "coop");
+    assert_equals(report_2[0].body["violation-type"], "access");
+    assert_equals(report_2[0].body["disposition"], "reporting");
+    assert_equals(report_2[0].body["effective-policy"], "same-origin-plus-coep");
+  }, `${test}`);
+});
+
+</script>
diff --git a/html/cross-origin-opener-policy/access-reporting/opener-accessed_openee-coop.https.html b/html/cross-origin-opener-policy/access-reporting/opener-accessed_openee-coop.https.html
new file mode 100644
index 0000000..921fdf7
--- /dev/null
+++ b/html/cross-origin-opener-policy/access-reporting/opener-accessed_openee-coop.https.html
@@ -0,0 +1,83 @@
+<title>
+  COOP reports are sent when the openee used COOP+COEP and then tries to
+  access its opener.
+</title>
+<meta name=timeout content=long>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src=/common/get-host-info.sub.js></script>
+<script src="/common/utils.js"></script>
+<script src="./resources/dispatcher.js"></script>
+<script>
+
+const directory = "/html/cross-origin-opener-policy/access-reporting";
+const executor_path = directory + "/resources/executor.html?pipe=";
+const cross_origin = get_host_info().HTTPS_REMOTE_ORIGIN;
+const coep_header = '|header(Cross-Origin-Embedder-Policy,require-corp)';
+
+let operation = [
+//[test name           , operation                     ]   ,
+  ["Call blur"         , "opener.blur()"               ] ,
+  ["Call foo"          , "opener.foo()"                ] ,
+  ["Call location"     , "opener.location()"           ] ,
+  ["Call opener"       , "opener.opener()"             ] ,
+  ["Call postMessage"  , "opener.postMessage()"        ] ,
+  ["Call window"       , "opener.window()"             ] ,
+  ["Read blur"         , "opener.blur"                 ] ,
+  ["Read foo"          , "opener.foo"                  ] ,
+  ["Read location"     , "opener.location"             ] ,
+  ["Read opener"       , "opener.opener"               ] ,
+  ["Read postMessage"  , "opener.postMessage"          ] ,
+  ["Read window"       , "opener.window"               ] ,
+  ["Write blur"        , "opener.blur = 'test'"        ]  ,
+  ["Write foo"         , "opener.foo = 'test'"         ]  ,
+  ["Write location"    , "opener.location = 'test'"    ]  ,
+  ["Write opener"      , "opener.opener = 'test'"      ]  ,
+  ["Write postMessage" , "opener.postMessage = 'test'" ]  ,
+  ["Write window"      , "opener.window = 'test'"      ]  ,
+];
+
+operation.forEach(([test, op]) => {
+  promise_test(async t => {
+    const report_token = token();
+    const executor_token = token();
+    const callback_token = token();
+
+    const reportTo = reportToHeaders(report_token);
+    const openee_url = cross_origin + executor_path +
+      reportTo.header + reportTo.coopSameOrigin + coep_header +
+      `&uuid=${executor_token}`;
+    const openee = window.open(openee_url);
+    t.add_cleanup(() => send(executor_token, "window.close()"));
+
+    // 1. Skip the first report about the opener breakage.
+    let report_1 = await receive(report_token);
+    assert_not_equals(report_1, "timeout");
+    report_1 = JSON.parse(report_1);
+    assert_equals(report_1.length, 1);
+    assert_equals(report_1[0].type, "coop");
+    assert_equals(report_1[0].body["violation-type"], "navigation-to-document");
+    assert_equals(report_1[0].body["disposition"], "enforce");
+
+    // 3. Try to access the opener. A report is sent, because of COOP-RO+COEP.
+    send(executor_token, `
+      try {${op}} catch(e) {}
+      send("${callback_token}", "Done");
+    `);
+    let reply = await receive(callback_token);
+    assert_equals(reply, "Done");
+
+    // 4. A COOP access reports must be sent as a result of (3).
+    let report_2 = await receive(report_token);
+    assert_not_equals(report_2, "timeout");
+    report_2 = JSON.parse(report_2);
+
+    assert_equals(report_2.length, 1);
+    assert_equals(report_2[0].type, "coop");
+    assert_equals(report_2[0].body["violation-type"], "access");
+    assert_equals(report_2[0].body["disposition"], "enforce");
+    assert_equals(report_2[0].body["effective-policy"], "same-origin-plus-coep");
+  }, `${test}`);
+});
+
+</script>
diff --git a/html/cross-origin-opener-policy/access-reporting/resources/dispatcher.js b/html/cross-origin-opener-policy/access-reporting/resources/dispatcher.js
new file mode 100644
index 0000000..d0184d5
--- /dev/null
+++ b/html/cross-origin-opener-policy/access-reporting/resources/dispatcher.js
@@ -0,0 +1,55 @@
+// Define an universal message passing API.
+//
+// In particular, this works:
+// - cross-origin and
+// - cross-browsing-context-group.
+//
+// It can also be used to receive reports.
+
+const dispatcher_path =
+    '/html/cross-origin-opener-policy/access-reporting/resources/dispatcher.py';
+const dispatcher_url = new URL(dispatcher_path, location.href).href;
+
+const send = function(uuid, message) {
+  fetch(dispatcher_url + `?uuid=${uuid}`, {
+    method: 'POST',
+    body: message
+  });
+}
+
+const receive = async function(uuid) {
+  const timeout = 3000;
+  const retry_delay = 100;
+  for(let i = 0; i * retry_delay < timeout; ++i) {
+    let response = await fetch(dispatcher_url + `?uuid=${uuid}`);
+    let data = await response.text();
+    if (data != 'not ready')
+      return data;
+    await new Promise(r => step_timeout(r, retry_delay));
+  }
+  return "timeout";
+}
+
+// Build a set of headers to tests the reporting API. This defines a set of
+// matching 'Report-To', 'Cross-Origin-Opener-Policy' and
+// 'Cross-Origin-Opener-Policy-Report-Only' headers.
+const reportToHeaders = function(uuid) {
+  const report_endpoint_url = dispatcher_path + `?uuid=${uuid}`;
+  let reportToJSON = {
+    'group': `${uuid}`,
+    'max_age': 3600,
+    'endpoints': [
+      {'url': report_endpoint_url.toString()},
+    ]
+  };
+  reportToJSON = JSON.stringify(reportToJSON)
+                     .replace(/,/g, '\\,')
+                     .replace(/\(/g, '\\\(')
+                     .replace(/\)/g, '\\\)=');
+
+  return {
+    header: `|header(report-to,${reportToJSON})`,
+    coopSameOrigin: `|header(Cross-Origin-Opener-Policy, same-origin%3Breport-to="${uuid}")`,
+    coopReportOnlySameOrigin: `|header(Cross-Origin-Opener-Policy-Report-Only, same-origin%3Breport-to="${uuid}")`,
+  };
+};
diff --git a/html/cross-origin-opener-policy/access-reporting/resources/dispatcher.py b/html/cross-origin-opener-policy/access-reporting/resources/dispatcher.py
new file mode 100644
index 0000000..67b8cc3
--- /dev/null
+++ b/html/cross-origin-opener-policy/access-reporting/resources/dispatcher.py
@@ -0,0 +1,22 @@
+# A server used to store and retrieve arbitrary data.
+# This is used by: ./dispatcher.js
+import json
+
+def main(request, response):
+    response.headers.set('Access-Control-Allow-Origin', '*')
+    response.headers.set('Access-Control-Allow-Methods', 'OPTIONS, GET, POST')
+    response.headers.set('Access-Control-Allow-Headers', 'Content-Type')
+    response.headers.set('Cache-Control', 'no-cache, no-store, must-revalidate');
+    if request.method == 'OPTIONS': # CORS preflight
+        return ''
+
+    uuid = request.GET['uuid']
+
+    if request.method == 'POST':
+        return request.server.stash.put(uuid, request.body)
+    else:
+        body = request.server.stash.take(uuid)
+        if body is None:
+            return 'not ready'
+        else:
+            return body
diff --git a/html/cross-origin-opener-policy/access-reporting/resources/executor.html b/html/cross-origin-opener-policy/access-reporting/resources/executor.html
new file mode 100644
index 0000000..3b10da0
--- /dev/null
+++ b/html/cross-origin-opener-policy/access-reporting/resources/executor.html
@@ -0,0 +1,12 @@
+<script src=/resources/testharness.js></script>
+<script src="./dispatcher.js"></script>
+<script>
+  const params = new URLSearchParams(window.location.search);
+  const uuid = params.get('uuid');
+
+  let executeOrders = async function() {
+    while(true)
+      eval(await receive(uuid));
+  };
+  executeOrders();
+</script>