/* Any copyright is dedicated to the Public Domain.
 * http://creativecommons.org/publicdomain/zero/1.0/ */

"use strict";

const TEST_CROSS_SITE_DOMAIN = "https://example.net/";
const TEST_CROSS_SITE_PAGE =
  TEST_CROSS_SITE_DOMAIN + TEST_PATH + "file_empty.html";

add_setup(async function () {
  await SpecialPowers.pushPrefEnv({
    set: [
      ["dom.serviceWorkers.enabled", true],
      ["dom.serviceWorkers.testing.enabled", true],
    ],
  });

  Services.cookies.removeAll();
});

function registerSW(browser) {
  return SpecialPowers.spawn(browser, [], async _ => {
    let reg =
      await content.navigator.serviceWorker.register("serviceWorker.js");

    await ContentTaskUtils.waitForCondition(() => {
      return reg.active && reg.active.state === "activated";
    }, "The service worker is activated");

    ok(
      content.navigator.serviceWorker.controller,
      "The service worker controls the document successfully."
    );
  });
}

function fetchCookiesFromSW(browser) {
  return SpecialPowers.spawn(browser, [], async _ => {
    return new content.Promise(resolve => {
      content.navigator.serviceWorker.addEventListener("message", event => {
        resolve(event.data.content);
      });

      content.navigator.serviceWorker.controller.postMessage({
        action: "fetch",
        url: `cookies.sjs`,
      });
    });
  });
}

function setCookiesFromSW(browser, cookies) {
  let setCookieQuery = "";

  for (let cookie of cookies) {
    setCookieQuery += `Set-Cookie=${cookie}&`;
  }

  return SpecialPowers.spawn(browser, [setCookieQuery], async query => {
    return new content.Promise(resolve => {
      content.navigator.serviceWorker.addEventListener("message", event => {
        resolve(event.data.content);
      });

      content.navigator.serviceWorker.controller.postMessage({
        action: "fetch",
        url: `cookies.sjs?${query}`,
      });
    });
  });
}

function unregisterSW(browser) {
  return SpecialPowers.spawn(browser, [], async _ => {
    const regs = await content.navigator.serviceWorker.getRegistrations();
    for (const reg of regs) {
      await reg.unregister();
    }
  });
}

/**
 * Verify a first-party service worker can access both SameSite=None and
 * SameSite=Lax cookies set in the first-party context.
 */
add_task(async function testCookiesWithFirstPartyServiceWorker() {
  info("Open a tab");
  let tab = await BrowserTestUtils.openNewForegroundTab(
    gBrowser,
    TEST_TOP_PAGE
  );

  info("Writing cookies to the first-party context.");
  await SpecialPowers.spawn(tab.linkedBrowser, [], async _ => {
    const cookies = [
      "foo=bar; SameSite=None; Secure",
      "fooLax=barLax; SameSite=Lax; Secure",
    ];

    let query = "";

    for (let cookie of cookies) {
      query += `Set-Cookie=${cookie}&`;
    }

    await content.fetch(`cookies.sjs?${query}`);
  });

  info("Register a service worker and trigger a fetch request to get cookies.");
  await registerSW(tab.linkedBrowser);
  let cookieStr = await fetchCookiesFromSW(tab.linkedBrowser);

  is(cookieStr, "foo=bar; fooLax=barLax", "The cookies are expected");

  info("Set cookies from the service worker.");
  await setCookiesFromSW(tab.linkedBrowser, [
    "foo=barSW; SameSite=None; Secure",
    "fooLax=barLaxSW; SameSite=Lax; Secure",
  ]);

  info("Get cookies from the service worker.");
  cookieStr = await fetchCookiesFromSW(tab.linkedBrowser);

  is(cookieStr, "foo=barSW; fooLax=barLaxSW", "The cookies are expected");

  await unregisterSW(tab.linkedBrowser);
  BrowserTestUtils.removeTab(tab);
  Services.cookies.removeAll();
});

/**
 * Verify a cross-site service worker can only access cookies set in the
 * same cross-site context.
 */
add_task(async function testCookiesWithCrossSiteServiceWorker() {
  // Disable blocking third-party cookies.
  await SpecialPowers.pushPrefEnv({
    set: [["network.cookie.cookieBehavior.optInPartitioning", false]],
  });

  info("Open a cross-site tab");
  let crossSiteTab = await BrowserTestUtils.openNewForegroundTab(
    gBrowser,
    TEST_CROSS_SITE_PAGE
  );

  info("Writing cookies to the cross site in the first-party context.");
  await SpecialPowers.spawn(crossSiteTab.linkedBrowser, [], async _ => {
    const cookies = [
      "foo=bar; SameSite=None; Secure",
      "fooLax=barLax; SameSite=Lax; Secure",
    ];

    let query = "";

    for (let cookie of cookies) {
      query += `Set-Cookie=${cookie}&`;
    }

    await content.fetch(`cookies.sjs?${query}`);
  });

  info("Open a tab");
  let tab = await BrowserTestUtils.openNewForegroundTab(
    gBrowser,
    TEST_TOP_PAGE
  );

  info("Load a cross-site iframe");
  let crossSiteBc = await SpecialPowers.spawn(
    tab.linkedBrowser,
    [TEST_CROSS_SITE_PAGE],
    async url => {
      let ifr = content.document.createElement("iframe");

      await new content.Promise(resolve => {
        ifr.onload = resolve;
        content.document.body.appendChild(ifr);
        ifr.src = url;
      });

      return ifr.browsingContext;
    }
  );

  info("Write cookies in the cross-site iframe");
  await SpecialPowers.spawn(crossSiteBc, [], async _ => {
    const cookies = [
      "foo=crossBar; SameSite=None; Secure",
      "fooLax=crossBarLax; SameSite=Lax; Secure",
    ];

    let query = "";

    for (let cookie of cookies) {
      query += `Set-Cookie=${cookie}&`;
    }

    await content.fetch(`cookies.sjs?${query}`);
  });

  info(
    "Register a service worker and trigger a fetch request to get cookies in cross-site context."
  );
  await registerSW(crossSiteBc);
  let cookieStr = await fetchCookiesFromSW(crossSiteBc);

  is(
    cookieStr,
    "foo=crossBar",
    "Only the SameSite=None cookie set in the third-party iframe is available."
  );

  info("Set cookies from the third-party service worker.");
  await setCookiesFromSW(crossSiteBc, [
    "foo=crossBarSW; SameSite=None; Secure",
    "fooLax=crossBarLaxSW; SameSite=Lax; Secure",
  ]);

  info("Get cookies from the third-party service worker.");
  cookieStr = await fetchCookiesFromSW(crossSiteBc);

  is(
    cookieStr,
    "foo=crossBarSW",
    "Only the SameSite=None cookie set in the third-party service worker is available."
  );

  await unregisterSW(crossSiteBc);
  BrowserTestUtils.removeTab(crossSiteTab);
  BrowserTestUtils.removeTab(tab);
  Services.cookies.removeAll();
});

/**
 * Verify a cross-site service worker can only access partitioned cookies set in
 * the same cross-site context if third-party cookies are blocked.
 */
add_task(async function testPartitionedCookiesWithCrossSiteServiceWorker() {
  // Enable blocking third-party cookies.
  await SpecialPowers.pushPrefEnv({
    set: [
      ["network.cookie.cookieBehavior.optInPartitioning", true],
      ["network.cookie.CHIPS.enabled", true],
    ],
  });

  info("Open a tab");
  let tab = await BrowserTestUtils.openNewForegroundTab(
    gBrowser,
    TEST_TOP_PAGE
  );

  info("Load a cross-site iframe");
  let crossSiteBc = await SpecialPowers.spawn(
    tab.linkedBrowser,
    [TEST_CROSS_SITE_PAGE],
    async url => {
      let ifr = content.document.createElement("iframe");

      await new content.Promise(resolve => {
        ifr.onload = resolve;
        content.document.body.appendChild(ifr);
        ifr.src = url;
      });

      return ifr.browsingContext;
    }
  );

  info("Write cookies in the cross-site iframe");
  await SpecialPowers.spawn(crossSiteBc, [], async _ => {
    const cookies = [
      "foo=crossBar; SameSite=None; Secure",
      "fooLax=crossBarLax; SameSite=Lax; Secure",
      "fooPartitioned=crossBar; SameSite=None; Secure; Partitioned;",
      "fooLaxPartitioned=crossBarLax; SameSite=Lax; Secure; Partitioned;",
    ];

    let query = "";

    for (let cookie of cookies) {
      query += `Set-Cookie=${cookie}&`;
    }

    await content.fetch(`cookies.sjs?${query}`);
  });

  info(
    "Register a service worker and trigger a fetch request to get cookies in cross-site context."
  );
  await registerSW(crossSiteBc);
  let cookieStr = await fetchCookiesFromSW(crossSiteBc);

  is(
    cookieStr,
    "fooPartitioned=crossBar",
    "Only the SameSite=None partitioned cookie set in the third-party iframe is available."
  );

  info("Set cookies from the third-party service worker.");
  await setCookiesFromSW(crossSiteBc, [
    "foo=crossBarSW; SameSite=None; Secure",
    "fooLax=crossBarLaxSW; SameSite=Lax; Secure",
    "fooPartitioned=crossBarSW; SameSite=None; Secure; Partitioned;",
    "fooLaxPartitioned=crossBarLaxSW; SameSite=Lax; Secure; Partitioned;",
  ]);

  info("Get cookies from the third-party service worker.");
  cookieStr = await fetchCookiesFromSW(crossSiteBc);

  is(
    cookieStr,
    "fooPartitioned=crossBarSW",
    "Only the SameSite=None partitioned cookie set in the third-party service worker is available."
  );

  await unregisterSW(crossSiteBc);
  BrowserTestUtils.removeTab(tab);
  Services.cookies.removeAll();
});

/**
 * Verify a ABA service worker can only access cookies set in the ABA context.
 */
add_task(async function testCookiesWithABAServiceWorker() {
  // Disable blocking third-party cookies.
  await SpecialPowers.pushPrefEnv({
    set: [["network.cookie.cookieBehavior.optInPartitioning", false]],
  });

  info("Open a tab");
  let tab = await BrowserTestUtils.openNewForegroundTab(
    gBrowser,
    TEST_TOP_PAGE
  );

  info("Writing cookies to the first-party context.");
  await SpecialPowers.spawn(tab.linkedBrowser, [], async _ => {
    const cookies = [
      "foo=bar; SameSite=None; Secure",
      "fooLax=barLax; SameSite=Lax; Secure",
    ];

    let query = "";

    for (let cookie of cookies) {
      query += `Set-Cookie=${cookie}&`;
    }

    await content.fetch(`cookies.sjs?${query}`);
  });

  info("Load a ABA iframe");
  let crossSiteBc = await SpecialPowers.spawn(
    tab.linkedBrowser,
    [TEST_CROSS_SITE_PAGE],
    async url => {
      let ifr = content.document.createElement("iframe");

      await new content.Promise(resolve => {
        ifr.onload = resolve;
        content.document.body.appendChild(ifr);
        ifr.src = url;
      });

      return ifr.browsingContext;
    }
  );

  let ABABc = await SpecialPowers.spawn(
    crossSiteBc,
    [TEST_TOP_PAGE],
    async url => {
      let ifr = content.document.createElement("iframe");

      await new content.Promise(resolve => {
        ifr.onload = resolve;
        content.document.body.appendChild(ifr);
        ifr.src = url;
      });

      return ifr.browsingContext;
    }
  );

  info(
    "Register a service worker and trigger a fetch request to get cookies in the ABA context."
  );

  await registerSW(ABABc);
  let cookieStr = await fetchCookiesFromSW(ABABc);
  is(cookieStr, "", "No cookie should be available in ABA context.");

  info("Set cookies in the ABA iframe");
  await SpecialPowers.spawn(ABABc, [], async _ => {
    const cookies = [
      "fooABA=barABA; SameSite=None; Secure",
      "fooABALax=BarABALax; SameSite=Lax; Secure",
    ];

    let query = "";

    for (let cookie of cookies) {
      query += `Set-Cookie=${cookie}&`;
    }

    await content.fetch(`cookies.sjs?${query}`);
  });

  info("Get cookies in the ABA service worker.");
  cookieStr = await fetchCookiesFromSW(ABABc);

  is(
    cookieStr,
    "fooABA=barABA",
    "Only the SameSite=None cookie set in ABA iframe is available."
  );

  info("Set cookies from the service worker in ABA context");
  await setCookiesFromSW(ABABc, [
    "fooABA=barABASW; SameSite=None; Secure",
    "fooABALax=BarABALaxSW; SameSite=Lax; Secure",
  ]);

  info("Get cookies from the service worker in the ABA context.");
  cookieStr = await fetchCookiesFromSW(ABABc);

  is(
    cookieStr,
    "fooABA=barABASW",
    "Only the SameSite=None cookie set in ABA service worker is available."
  );

  await unregisterSW(ABABc);
  BrowserTestUtils.removeTab(tab);
  Services.cookies.removeAll();
});

/**
 * Verify a ABA service worker can only access partitioned cookies set in the
 * ABA context if third-party cookies are blocked.
 */
add_task(async function testCookiesWithABAServiceWorker() {
  // Disable blocking third-party cookies.
  await SpecialPowers.pushPrefEnv({
    set: [
      ["network.cookie.cookieBehavior.optInPartitioning", true],
      ["network.cookie.CHIPS.enabled", true],
    ],
  });

  info("Open a tab");
  let tab = await BrowserTestUtils.openNewForegroundTab(
    gBrowser,
    TEST_TOP_PAGE
  );

  info("Load a ABA iframe");
  let crossSiteBc = await SpecialPowers.spawn(
    tab.linkedBrowser,
    [TEST_CROSS_SITE_PAGE],
    async url => {
      let ifr = content.document.createElement("iframe");

      await new content.Promise(resolve => {
        ifr.onload = resolve;
        content.document.body.appendChild(ifr);
        ifr.src = url;
      });

      return ifr.browsingContext;
    }
  );

  let ABABc = await SpecialPowers.spawn(
    crossSiteBc,
    [TEST_TOP_PAGE],
    async url => {
      let ifr = content.document.createElement("iframe");

      await new content.Promise(resolve => {
        ifr.onload = resolve;
        content.document.body.appendChild(ifr);
        ifr.src = url;
      });

      return ifr.browsingContext;
    }
  );

  info("Set cookies in the ABA iframe");
  await SpecialPowers.spawn(ABABc, [], async _ => {
    const cookies = [
      "fooABA=barABA; SameSite=None; Secure",
      "fooABALax=BarABALax; SameSite=Lax; Secure",
      "fooABAPartitioned=barABA; SameSite=None; Secure; Partitioned;",
      "fooABALaxPartitioned=BarABALax; SameSite=Lax; Secure; Partitioned;",
    ];

    let query = "";

    for (let cookie of cookies) {
      query += `Set-Cookie=${cookie}&`;
    }

    await content.fetch(`cookies.sjs?${query}`);
  });

  info(
    "Register a service worker and trigger a fetch request to get cookies in the ABA context."
  );

  await registerSW(ABABc);

  info("Get cookies in the ABA service worker.");
  let cookieStr = await fetchCookiesFromSW(ABABc);

  is(
    cookieStr,
    "fooABAPartitioned=barABA",
    "Only the SameSite=None partitioned cookie set in ABA iframe is available."
  );

  info("Set cookies from the service worker in ABA context");
  await setCookiesFromSW(ABABc, [
    "fooABA=barABASW; SameSite=None; Secure",
    "fooABALax=BarABALaxSW; SameSite=Lax; Secure",
    "fooABAPartitioned=barABASW; SameSite=None; Secure; Partitioned;",
    "fooABALaxPartitioned=BarABALaxSW; SameSite=Lax; Secure; Partitioned;",
  ]);

  info("Get cookies from the service worker in the ABA context.");
  cookieStr = await fetchCookiesFromSW(ABABc);

  is(
    cookieStr,
    "fooABAPartitioned=barABASW",
    "Only the SameSite=None partitioned cookie set in ABA service worker is available."
  );

  await unregisterSW(ABABc);
  BrowserTestUtils.removeTab(tab);
  Services.cookies.removeAll();
});
