summaryrefslogtreecommitdiffstats
path: root/dom/serviceworkers/test/browser_navigation_fetch_fault_handling.js
diff options
context:
space:
mode:
Diffstat (limited to 'dom/serviceworkers/test/browser_navigation_fetch_fault_handling.js')
-rw-r--r--dom/serviceworkers/test/browser_navigation_fetch_fault_handling.js276
1 files changed, 276 insertions, 0 deletions
diff --git a/dom/serviceworkers/test/browser_navigation_fetch_fault_handling.js b/dom/serviceworkers/test/browser_navigation_fetch_fault_handling.js
new file mode 100644
index 0000000000..a72fc68b69
--- /dev/null
+++ b/dom/serviceworkers/test/browser_navigation_fetch_fault_handling.js
@@ -0,0 +1,276 @@
+/**
+ * This test file tests our automatic recovery and any related mitigating
+ * heuristics that occur during intercepted navigation fetch request.
+ * Specifically, we should be resetting interception so that we go to the
+ * network in these cases and then potentially taking actions like unregistering
+ * the ServiceWorker and/or clearing QuotaManager-managed storage for the
+ * origin.
+ *
+ * See specific test permutations for specific details inline in the test.
+ *
+ * NOTE THAT CURRENTLY THIS TEST IS DISCUSSING MITIGATIONS THAT ARE NOT YET
+ * IMPLEMENTED, JUST PLANNED. These will be iterated on and added to the rest
+ * of the stack of patches on Bug 1503072.
+ *
+ * ## Test Mechanics
+ *
+ * ### Fetch Fault Injection
+ *
+ * We expose:
+ * - On nsIServiceWorkerInfo, the per-ServiceWorker XPCOM interface:
+ * - A mechanism for creating synthetic faults by setting the
+ * `nsIServiceWorkerInfo::testingInjectCancellation` attribute to a failing
+ * nsresult. The fault is applied at the beginning of the steps to dispatch
+ * the fetch event on the global.
+ * - A count of the number of times we experienced these navigation faults
+ * that had to be reset as `nsIServiceWorkerInfo::navigationFaultCount`.
+ * (This would also include real faults, but we only expect to see synthetic
+ * faults in this test.)
+ * - On nsIServiceWorkerRegistrationInfo, the per-registration XPCOM interface:
+ * - A readonly attribute that indicates how many times an origin storage
+ * usage check has been initiated.
+ *
+ * We also use:
+ * - `nsIServiceWorkerManager::addListener(nsIServiceWorkerManagerListener)`
+ * allows our test to listen for the unregistration of registrations. This
+ * allows us to be notified when unregistering or origin-clearing actions have
+ * been taken as a mitigation.
+ *
+ * ### General Test Approach
+ *
+ * For each test we:
+ * - Ensure/confirm the testing origin has no QuotaManager storage in use.
+ * - Install the ServiceWorker.
+ * - If we are testing the situation where we want to simulate the origin being
+ * near its quota limit, we also generate Cache API and IDB storage usage
+ * sufficient to put our origin over the threshold.
+ * - We run a quota check on the origin after doing this in order to make sure
+ * that we did this correctly and that we properly constrained the limit for
+ * the origin. We fail the test for test implementation reasons if we
+ * didn't accomplish this.
+ * - Verify a fetch navigation to the SW works without any fault injection,
+ * producing a result produced by the ServiceWorker.
+ * - Begin fault permutations in a loop, where for each pass of the loop:
+ * - We trigger a navigation which will result in an intercepted fetch
+ * which will fault. We wait until the navigation completes.
+ * - We verify that we got the request from the network.
+ * - We verify that the ServiceWorker's navigationFaultCount increased.
+ * - If this the count at which we expect a mitigation to take place, we wait
+ * for the registration to become unregistered AND:
+ * - We check whether the storage for the origin was cleared or not, which
+ * indicates which mitigation of the following happened:
+ * - Unregister the registration directly.
+ * - Clear the origin's data which will also unregister the registration
+ * as a side effect.
+ * - We check whether the registration indicates an origin quota check
+ * happened or not.
+ *
+ * ### Disk Usage Limits
+ *
+ * In order to avoid gratuitous disk I/O and related overheads, we limit QM
+ * ("temporary") storage to 10 MiB which ends up limiting group usage to 10 MiB.
+ * This lets us set a threshold situation where we claim that a SW needs at
+ * least 4 MiB of storage for installation/operation, meaning that any usage
+ * beyond 6 MiB in the group will constitute a need to clear the group or
+ * origin. We fill with the storage with 8 MiB of artificial usage to this end,
+ * storing 4 MiB in Cache API and 4 MiB in IDB.
+ **/
+
+// Because of the amount of I/O involved in this test, pernosco reproductions
+// may experience timeouts without a timeout multiplier.
+requestLongerTimeout(2);
+
+/* import-globals-from browser_head.js */
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/dom/serviceworkers/test/browser_head.js",
+ this
+);
+
+// The origin we run the tests on.
+const TEST_ORIGIN = "https://test1.example.org";
+// An origin in the same group that impacts the usage of the TEST_ORIGIN. Used
+// to verify heuristics related to group-clearing (where clearing the
+// TEST_ORIGIN itself would not be sufficient for us to mitigate quota limits
+// being reached.)
+const SAME_GROUP_ORIGIN = "https://test2.example.org";
+
+const TEST_SW_SETUP = {
+ origin: TEST_ORIGIN,
+ // Page with a body textContent of "NETWORK" and has utils.js loaded.
+ scope: "network_with_utils.html",
+ // SW that serves a body with a textContent of "SERVICEWORKER" and
+ // has utils.js loaded.
+ script: "sw_respondwith_serviceworker.js",
+};
+
+const TEST_STORAGE_SETUP = {
+ cacheBytes: 4 * 1024 * 1024, // 4 MiB
+ idbBytes: 4 * 1024 * 1024, // 4 MiB
+};
+
+const FAULTS_BEFORE_MITIGATION = 3;
+
+/**
+ * Core test iteration logic.
+ *
+ * Parameters:
+ * - name: Human readable name of the fault we're injecting.
+ * - useError: The nsresult failure code to inject into fetch.
+ * - errorPage: The "about" page that we expect errors to leave us on.
+ * - consumeQuotaOrigin: If truthy, the origin to place the storage usage in.
+ * If falsey, we won't fill storage.
+ */
+async function do_fault_injection_test({
+ name,
+ useError,
+ errorPage,
+ consumeQuotaOrigin,
+}) {
+ info(
+ `### testing: error: ${name} (${useError}) consumeQuotaOrigin: ${consumeQuotaOrigin}`
+ );
+
+ // ## Ensure/confirm the testing origins have no QuotaManager storage in use.
+ await clear_qm_origin_group_via_clearData(TEST_ORIGIN);
+
+ // ## Install the ServiceWorker
+ const reg = await install_sw(TEST_SW_SETUP);
+ const sw = reg.activeWorker;
+
+ // ## Generate quota usage if appropriate
+ if (consumeQuotaOrigin) {
+ await consume_storage(consumeQuotaOrigin, TEST_STORAGE_SETUP);
+ }
+
+ // ## Verify normal navigation is served by the SW.
+ info(`## Checking normal operation.`);
+ {
+ const debugTag = `err=${name}&fault=0`;
+ const docInfo = await navigate_and_get_body(TEST_SW_SETUP, debugTag);
+ is(
+ docInfo.body,
+ "SERVICEWORKER",
+ "navigation without injected fault originates from ServiceWorker"
+ );
+
+ is(
+ docInfo.controlled,
+ true,
+ "successfully intercepted navigation should be controlled"
+ );
+ }
+
+ // Make sure the test is listening on the ServiceWorker unregistration, since
+ // we expect it happens after navigation fault threshold reached.
+ const unregisteredPromise = waitForUnregister(reg.scope);
+
+ // Make sure the test is listening on the finish of quota checking, since we
+ // expect it happens after navigation fault threshold reached.
+ const quotaUsageCheckFinishPromise = waitForQuotaUsageCheckFinish(reg.scope);
+
+ // ## Inject faults in a loop until expected mitigation.
+ sw.testingInjectCancellation = useError;
+ for (let iFault = 0; iFault < FAULTS_BEFORE_MITIGATION; iFault++) {
+ info(`## Testing with injected fault number ${iFault + 1}`);
+ // We should never have triggered an origin quota usage check before the
+ // final fault injection.
+ is(reg.quotaUsageCheckCount, 0, "No quota usage check yet");
+
+ // Make sure our loads encode the specific
+ const debugTag = `err=${name}&fault=${iFault + 1}`;
+
+ const docInfo = await navigate_and_get_body(TEST_SW_SETUP, debugTag);
+ // We should always be receiving network fallback.
+ is(
+ docInfo.body,
+ "NETWORK",
+ "navigation with injected fault originates from network"
+ );
+
+ is(docInfo.controlled, false, "bypassed pages shouldn't be controlled");
+
+ // The fault count should have increased
+ is(
+ sw.navigationFaultCount,
+ iFault + 1,
+ "navigation fault increased (to expected value)"
+ );
+ }
+
+ await unregisteredPromise;
+ is(reg.unregistered, true, "registration should be unregistered");
+
+ //is(reg.quotaUsageCheckCount, 1, "Quota usage check must be started");
+ await quotaUsageCheckFinishPromise;
+
+ if (consumeQuotaOrigin) {
+ // Check that there is no longer any storage usaged by the origin in this
+ // case.
+ const originUsage = await get_qm_origin_usage(TEST_ORIGIN);
+ ok(
+ is_minimum_origin_usage(originUsage),
+ "origin usage should be mitigated"
+ );
+
+ if (consumeQuotaOrigin === SAME_GROUP_ORIGIN) {
+ const sameGroupUsage = await get_qm_origin_usage(SAME_GROUP_ORIGIN);
+ Assert.strictEqual(
+ sameGroupUsage,
+ 0,
+ "same group usage should be mitigated"
+ );
+ }
+ }
+}
+
+add_task(async function test_navigation_fetch_fault_handling() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["dom.serviceWorkers.enabled", true],
+ ["dom.serviceWorkers.exemptFromPerDomainMax", true],
+ ["dom.serviceWorkers.testing.enabled", true],
+ ["dom.serviceWorkers.mitigations.bypass_on_fault", true],
+ ["dom.serviceWorkers.mitigations.group_usage_headroom_kb", 5 * 1024],
+ ["dom.quotaManager.testing", true],
+ // We want the temporary global limit to be 10 MiB (the pref is in KiB).
+ // This will result in the group limit also being 10 MiB because on small
+ // disks we provide a group limit value of min(10 MiB, global limit).
+ ["dom.quotaManager.temporaryStorage.fixedLimit", 10 * 1024],
+ ],
+ });
+
+ // Need to reset the storages to make dom.quotaManager.temporaryStorage.fixedLimit
+ // works.
+ await qm_reset_storage();
+
+ const quotaOriginVariations = [
+ // Don't put us near the storage limit.
+ undefined,
+ // Put us near the storage limit in the SW origin itself.
+ TEST_ORIGIN,
+ // Put us near the storage limit in the SW origin's group but not the origin
+ // itself.
+ SAME_GROUP_ORIGIN,
+ ];
+
+ for (const consumeQuotaOrigin of quotaOriginVariations) {
+ await do_fault_injection_test({
+ name: "NS_ERROR_DOM_ABORT_ERR",
+ useError: 0x80530014, // Not in `Cr`.
+ // Abort errors manifest as about:blank pages.
+ errorPage: "about:blank",
+ consumeQuotaOrigin,
+ });
+
+ await do_fault_injection_test({
+ name: "NS_ERROR_INTERCEPTION_FAILED",
+ useError: 0x804b0064, // Not in `Cr`.
+ // Interception failures manifest as corrupt content pages.
+ errorPage: "about:neterror",
+ consumeQuotaOrigin,
+ });
+ }
+
+ // Cleanup: wipe the origin and group so all the ServiceWorkers go away.
+ await clear_qm_origin_group_via_clearData(TEST_ORIGIN);
+});