Create WPT for partitioned service workers

Add a test that verifies that services workers are partitioned and
cannot interact with each other when third-party partitioning is
enabled.

This CL verifies the enabled feature case by running the test in a
virtual test suite.

The test expectations are also updated to expect the test to fail in
the standard suite and pass in the virtual suite.

I manually verified this test by removing the window and iframe
cleanup/removals, running it in content_shell, and verifying that the
windows displayed the appropriate text demonstrating that the SW did
indeed get used.

Bug: 1246549
Change-Id: I22450e3abf496e4e011cc60ef7cfdf04acfdbcda
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/3183843
Reviewed-by: Mason Freed <masonf@chromium.org>
Reviewed-by: Ian Kilpatrick <ikilpatrick@chromium.org>
Reviewed-by: Mike Taylor <miketaylr@chromium.org>
Reviewed-by: Ben Kelly <wanderview@chromium.org>
Commit-Queue: Steven Bingler <bingler@chromium.org>
Cr-Commit-Position: refs/heads/main@{#932203}
diff --git a/service-workers/service-worker/partitioned-service-worker.tentative.https.html b/service-workers/service-worker/partitioned-service-worker.tentative.https.html
new file mode 100644
index 0000000..c006e09
--- /dev/null
+++ b/service-workers/service-worker/partitioned-service-worker.tentative.https.html
@@ -0,0 +1,138 @@
+<!DOCTYPE html>
+<meta charset="utf-8"/>
+<title>Service Worker: Partitioned Service Workers</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+
+
+<body>
+  The 3p iframe's postMessage:
+  <p id="iframe_response">No message received</p>
+
+<script>
+
+// The resolve function for the current pending event listener's promise.
+// It is nulled once the promise is resolved.
+var message_event_promise_resolve = null;
+
+function messageEventHandler(evt) {
+  if (message_event_promise_resolve) {
+    local_resolve = message_event_promise_resolve;
+    message_event_promise_resolve = null;
+    local_resolve(evt.data);
+  }
+}
+
+function makeMessagePromise() {
+  if (message_event_promise_resolve != null) {
+    // Do not create a new promise until the previous is settled.
+    return;
+  }
+
+  return new Promise(resolve => {
+    message_event_promise_resolve = resolve;
+  });
+}
+
+// Loads a url for the frame type and then returns a promise for
+// the data that was postMessage'd from the loaded frame.
+function loadAndReturnSwData(t, url, frame_type) {
+  if (frame_type !== 'iframe' && frame_type !== 'window') {
+    return;
+  }
+
+  const message_promise = makeMessagePromise();
+
+  // Create the iframe or window and then return the promise for data.
+  if ( frame_type === 'iframe' ) {
+    const frame = with_iframe(url, false);
+    t.add_cleanup(async () => {
+      const f = await frame;
+      f.remove();
+    });
+  }
+  else {
+    // 'window' case.
+    const w = window.open(url);
+    t.add_cleanup(() => w.close());
+  }
+
+  return message_promise;
+}
+
+promise_test(async t => {
+  const script = './resources/partitioned-storage-sw.js'
+  const scope = './resources/partitioned-'
+
+  // Add service worker to this 1P context.
+  const reg = await service_worker_unregister_and_register(t, script, scope);
+  t.add_cleanup(() => reg.unregister());
+  await wait_for_state(t, reg.installing, 'activated');
+
+  // Register the message listener.
+  self.addEventListener('message', messageEventHandler);
+  t.add_cleanup(() =>{
+    self.removeEventListener('message', messageEventHandler, false);
+  });
+
+  // Open an iframe that will create a promise within the SW.
+  // The query param is there to track which request the service worker is
+  // handling.
+  const wait_frame_url = new URL(
+    './resources/partitioned-waitUntilResolved.fakehtml?From1pFrame',
+    self.location);
+
+  const wait_frame_1p_data = await loadAndReturnSwData(t, wait_frame_url,
+                                                       'iframe');
+  assert_equals(wait_frame_1p_data.source, 'From1pFrame',
+    'The data for the 1p frame came from the wrong source');
+
+  // Now create a 3p iframe that will try to resolve the SW in a 3p context.
+  const third_party_url = new URL(
+    './resources/partitioned-service-worker-third-party-window.html',
+    get_host_info().HTTPS_NOTSAMESITE_ORIGIN + self.location.pathname);
+
+  // Create the 3p window (which will in turn create the iframe with the SW)
+  // and await on its data.
+  const frame_3p_data = await loadAndReturnSwData(t, third_party_url, 'window');
+  assert_equals(frame_3p_data.source, 'From3pFrame',
+    'The data for the 3p frame came from the wrong source');
+
+  // Print some debug info to the main frame.
+  document.getElementById("iframe_response").innerHTML =
+      "3p iframe's has_pending: " + frame_3p_data.has_pending + " source: " +
+      frame_3p_data.source + ". ";
+
+  // Now do the same for the 1p iframe.
+  const resolve_frame_url = new URL(
+    './resources/partitioned-resolve.fakehtml?From1pFrame', self.location);
+
+  const frame_1p_data = await loadAndReturnSwData(t, resolve_frame_url,
+                                                  'iframe');
+  assert_equals(frame_1p_data.source, 'From1pFrame',
+    'The data for the 1p frame came from the wrong source');
+  // Both the 1p frames should have been serviced by the same service worker ID.
+  // If this isn't the case then that means the SW could have been deactivated
+  // which invalidates the test.
+  assert_equals(frame_1p_data.ID, wait_frame_1p_data.ID,
+    'The 1p frames were serviced by different service workers.');
+
+  document.getElementById("iframe_response").innerHTML +=
+    "1p iframe's has_pending: " + frame_1p_data.has_pending + " source: " +
+    frame_1p_data.source;
+
+  // If partitioning is working correctly then only the 1p iframe should see
+  // (and resolve) its SW's promise. Additionally the two frames should see
+  // different IDs.
+  assert_true(frame_1p_data.has_pending,
+      'The 1p iframe saw a pending promise in the service worker.');
+  assert_false(frame_3p_data.has_pending,
+    'The 3p iframe saw a pending promise in the service worker.');
+  assert_not_equals(frame_1p_data.ID, frame_3p_data.ID,
+    'The frames were serviced by the same service worker thread.');
+}, 'Services workers under different top-level sites are partitioned.');
+
+</script>
+</body>
\ No newline at end of file
diff --git a/service-workers/service-worker/resources/partitioned-service-worker-third-party-iframe.html b/service-workers/service-worker/resources/partitioned-service-worker-third-party-iframe.html
new file mode 100644
index 0000000..56c04b6
--- /dev/null
+++ b/service-workers/service-worker/resources/partitioned-service-worker-third-party-iframe.html
@@ -0,0 +1,76 @@
+<!DOCTYPE html>
+<title>Service Worker: 3P iframe for partitioned service workers</title>
+<script src="/test-helpers.sub.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+
+
+<body>
+This iframe will register a service worker when it loads and then add its own
+iframe that will attempt to navigate to a url that service worker will intercept
+and use to resolve the service worker's internal Promise.
+<script>
+// We should keep track if we installed a worker or not. If we did then we need
+// to uninstall it. Otherwise we let the top level test uninstall it.
+var installed_a_worker = true;
+var reg;
+
+async function onLoad() {
+  const script = './partitioned-storage-sw.js'
+  const scope = './partitioned-'
+
+  reg = await navigator.serviceWorker.register(script, { scope: scope });
+  await new Promise(resolve => {
+    // Check if a worker is already activated.
+    var worker = reg.active;
+    // If so, just resolve.
+    if ( worker ) {
+       installed_a_worker = false;
+       resolve();
+       return;
+    }
+
+    //Otherwise check if one is waiting.
+    worker = reg.waiting;
+    // If not waiting, grab the installing worker.
+    if ( !worker ) {
+      worker = reg.installing;
+    }
+
+    // Resolve once it's activated.
+    worker.addEventListener('statechange', evt => {
+      if (worker.state === 'activated') {
+        resolve();
+      }
+    });
+  });
+
+  // When the SW's iframe finishes it'll post a message. This forwards it up to
+  // the window.
+  self.addEventListener('message', evt => {
+      window.parent.postMessage(evt.data, '*');
+  });
+
+  // Now try to resolve the SW's promise. If we're partitioned then there
+  // shouldn't be a promise to resolve.
+  const resolve_frame_url = new URL('./partitioned-resolve.fakehtml?From3pFrame', self.location);
+  const frame_resolve = await new Promise(resolve => {
+    var frame = document.createElement('iframe');
+    frame.src = resolve_frame_url;
+    frame.onload = function() { resolve(frame); };
+    document.body.appendChild(frame);
+  });
+}
+
+self.addEventListener('unload', async () => {
+  // If we didn't install a worker then that means the top level test did, and
+  // that test is therefore responsible for cleaning it up.
+  if ( !installed_a_worker ) {
+      return;
+  }
+
+    await reg.unregister();
+});
+
+self.addEventListener('load', onLoad);
+</script>
+</body>
\ No newline at end of file
diff --git a/service-workers/service-worker/resources/partitioned-service-worker-third-party-window.html b/service-workers/service-worker/resources/partitioned-service-worker-third-party-window.html
new file mode 100644
index 0000000..21a8380
--- /dev/null
+++ b/service-workers/service-worker/resources/partitioned-service-worker-third-party-window.html
@@ -0,0 +1,47 @@
+<!DOCTYPE html>
+<title>Service Worker: 3P window for partitioned service workers</title>
+<script src="test-helpers.sub.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+
+
+<body>
+This page should be opened as a third-party window. It then loads an iframe that
+registers a service worker that, in turn, attempts to resolve its internal
+Promise. Once that happens this window will postMessage back to the test to let
+it know that the resolution occured.
+
+If 3p partitioning is active then this Promise resolution won't
+resolve the promise in the test's (that originally opened this page) 1p
+service worker.
+
+<script>
+
+async function onLoad() {
+  const message_promise = new Promise(resolve => {
+    self.addEventListener('message', evt => {
+      resolve(evt.data);
+    });
+  });
+
+  const iframe_url = new URL(
+    './partitioned-service-worker-third-party-iframe.html',
+    get_host_info().HTTPS_ORIGIN + self.location.pathname);
+
+  var frame = document.createElement('iframe');
+  frame.src = iframe_url;
+  frame.style.position = 'absolute';
+  document.body.appendChild(frame);
+
+
+  await message_promise.then(data => {
+    // We're done, forward the message and clean up.
+    window.opener.postMessage(data, '*');
+
+    frame.remove();
+  });
+}
+
+self.addEventListener('load', onLoad);
+
+</script>
+</body>
\ No newline at end of file
diff --git a/service-workers/service-worker/resources/partitioned-storage-sw.js b/service-workers/service-worker/resources/partitioned-storage-sw.js
new file mode 100644
index 0000000..6abf10a
--- /dev/null
+++ b/service-workers/service-worker/resources/partitioned-storage-sw.js
@@ -0,0 +1,62 @@
+// Holds the promise that the "resolve.fakehtml" call attempts to resolve.
+// This is "the SW's promise" that other parts of the test refer to.
+var promise;
+// Stores the resolve funcution for the current promise.
+var pending_resolve_func = null;
+// Unique ID to determine which service worker is being used.
+const ID = Math.random();
+
+function callAndResetResolve() {
+  var local_resolve = pending_resolve_func;
+  pending_resolve_func = null;
+  local_resolve();
+}
+
+self.addEventListener('fetch', function(event) {
+  fetchEventHandler(event);
+})
+
+async function fetchEventHandler(event){
+  var request_url = new URL(event.request.url);
+  var url_search = request_url.search.substr(1);
+  request_url.search = "";
+  if ( request_url.href.endsWith('waitUntilResolved.fakehtml') ) {
+
+      if (pending_resolve_func != null) {
+        // Respond with an error if there is already a pending promise
+        event.respondWith(Response.error());
+        return;
+      }
+
+      // Create the new promise.
+      promise = new Promise(function(resolve) {
+        pending_resolve_func = resolve;
+      });
+      event.waitUntil(promise);
+
+      event.respondWith(new Response(`
+        <html>
+        Promise created by ${url_search}
+        <script>self.parent.postMessage({ ID:${ID}, source: "${url_search}"
+          });</script>
+        </html>
+        `, {headers: {'Content-Type': 'text/html'}}
+      ));
+
+  }
+  else if ( request_url.href.endsWith('resolve.fakehtml') ) {
+    var has_pending = !!pending_resolve_func;
+
+    event.respondWith(new Response(`
+      <html>
+      Promise settled for ${url_search}
+      <script>self.parent.postMessage({ ID:${ID}, has_pending: ${has_pending},
+        source: "${url_search}"  });</script>
+      </html>
+    `, {headers: {'Content-Type': 'text/html'}}));
+
+    if (has_pending) {
+      callAndResetResolve();
+    }
+  }
+}
\ No newline at end of file