Commit a18ad0a8 authored by Luca Greco's avatar Luca Greco
Browse files

Bug 1745763 - Implement DNR updateEnabledRulesets API method. r=robwu

parent 23077eb8
Loading
Loading
Loading
Loading
+9 −0
Original line number Original line Diff line number Diff line
@@ -1368,6 +1368,14 @@ function validateManifestEntry(extension) {
  }
  }
}
}


async function updateEnabledStaticRulesets(extension, updateRulesetOptions) {
  await ensureInitialized(extension);
  await lazy.ExtensionDNRStore.updateEnabledStaticRulesets(
    extension,
    updateRulesetOptions
  );
}

// exports used by the DNR API implementation.
// exports used by the DNR API implementation.
export const ExtensionDNR = {
export const ExtensionDNR = {
  RuleValidator,
  RuleValidator,
@@ -1375,6 +1383,7 @@ export const ExtensionDNR = {
  ensureInitialized,
  ensureInitialized,
  getMatchedRulesForRequest,
  getMatchedRulesForRequest,
  getRuleManager,
  getRuleManager,
  updateEnabledStaticRulesets,
  validateManifestEntry,
  validateManifestEntry,
  // TODO(Bug 1803370): consider allowing changing DNR limits through about:config prefs).
  // TODO(Bug 1803370): consider allowing changing DNR limits through about:config prefs).
  limits: {
  limits: {
+181 −2
Original line number Original line Diff line number Diff line
@@ -6,18 +6,24 @@ const { ExtensionParent } = ChromeUtils.import(
  "resource://gre/modules/ExtensionParent.jsm"
  "resource://gre/modules/ExtensionParent.jsm"
);
);


const { ExtensionUtils } = ChromeUtils.import(
  "resource://gre/modules/ExtensionUtils.jsm"
);

import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";


const lazy = {};
const lazy = {};


XPCOMUtils.defineLazyModuleGetters(lazy, {
XPCOMUtils.defineLazyModuleGetters(lazy, {
  Schemas: "resource://gre/modules/Schemas.jsm",
  Schemas: "resource://gre/modules/Schemas.jsm",
  PromiseUtils: "resource://gre/modules/PromiseUtils.jsm",
});
});


ChromeUtils.defineESModuleGetters(lazy, {
ChromeUtils.defineESModuleGetters(lazy, {
  ExtensionDNR: "resource://gre/modules/ExtensionDNR.sys.mjs",
  ExtensionDNR: "resource://gre/modules/ExtensionDNR.sys.mjs",
});
});


const { DefaultMap, ExtensionError } = ExtensionUtils;
const { StartupCache } = ExtensionParent;
const { StartupCache } = ExtensionParent;


// DNR Rules store subdirectory/file names and file extensions.
// DNR Rules store subdirectory/file names and file extensions.
@@ -79,6 +85,63 @@ class StoreData {
  }
  }
}
}


class Queue {
  #tasks = [];
  #runningTask = null;
  #closed = false;

  get hasPendingTasks() {
    return !!this.#runningTask || !!this.#tasks.length;
  }

  get isClosed() {
    return this.#closed;
  }

  async close() {
    if (this.#closed) {
      return;
    }
    const drainedQueuePromise = this.queueTask(() => {});
    this.#closed = true;
    return drainedQueuePromise;
  }

  queueTask(callback) {
    if (this.#closed) {
      throw new Error("Unexpected queueTask call on closed queue");
    }
    const deferred = lazy.PromiseUtils.defer();
    this.#tasks.push({ callback, deferred });
    // Run the queued task right away if there isn't one already running.
    if (!this.#runningTask) {
      this.#runNextTask();
    }
    return deferred.promise;
  }

  async #runNextTask() {
    if (!this.#tasks.length) {
      this.#runningTask = null;
      return;
    }

    this.#runningTask = this.#tasks.shift();
    const { callback, deferred } = this.#runningTask;
    try {
      let result = callback();
      if (result instanceof Promise) {
        result = await result;
      }
      deferred.resolve(result);
    } catch (err) {
      deferred.reject(err);
    }

    this.#runNextTask();
  }
}

/**
/**
 * Class managing the rulesets persisted across browser sessions.
 * Class managing the rulesets persisted across browser sessions.
 *
 *
@@ -103,6 +166,8 @@ class RulesetsStore {
    this._dataPromises = new Map();
    this._dataPromises = new Map();
    // Map<extensionUUID, Promise<void>>
    // Map<extensionUUID, Promise<void>>
    this._savePromises = new Map();
    this._savePromises = new Map();
    // Map<extensionUUID, Queue>
    this._dataUpdateQueues = new DefaultMap(() => new Queue());
    // Map<extensionUUID, { close: Function }>
    // Map<extensionUUID, { close: Function }>
    this._shutdownHandlers = new Map();
    this._shutdownHandlers = new Map();
    // Promise to await on to ensure the store parent directory exist
    // Promise to await on to ensure the store parent directory exist
@@ -181,6 +246,27 @@ class RulesetsStore {
    return data?.staticRulesets;
    return data?.staticRulesets;
  }
  }


  /**
   * Update the enabled rulesets, queue changes to prevent races between calls
   * that may be triggered while an update is still in process.
   *
   * @param {Extension}     extension
   * @param {object}        params
   * @param {Array<string>} [params.disableRulesetIds=[]]
   * @param {Array<string>} [params.enableRulesetIds=[]]
   */
  async updateEnabledStaticRulesets(
    extension,
    { disableRulesetIds, enableRulesetIds }
  ) {
    return this._dataUpdateQueues.get(extension.uuid).queueTask(() => {
      return this.#updateEnabledStaticRulesets(extension, {
        disableRulesetIds,
        enableRulesetIds,
      });
    });
  }

  /**
  /**
   * Return the store file path for the given the extension's uuid.
   * Return the store file path for the given the extension's uuid.
   *
   *
@@ -227,7 +313,23 @@ class RulesetsStore {
    let shutdownHandler = this._shutdownHandlers.get(extensionUUID);
    let shutdownHandler = this._shutdownHandlers.get(extensionUUID);
    if (!shutdownHandler) {
    if (!shutdownHandler) {
      shutdownHandler = {
      shutdownHandler = {
        close: () => this.unloadData(extensionUUID),
        close: async () => {
          // Wait for the update tasks to have been executed, then unload the
          // data.
          const dataUpdateQueue = this._dataUpdateQueues.has(extensionUUID)
            ? this._dataUpdateQueues.get(extensionUUID)
            : undefined;
          if (dataUpdateQueue) {
            try {
              await dataUpdateQueue.close();
            } catch (err) {
              // Unexpected error on closing the update queue.
              Cu.reportError(err);
            }
            this._dataUpdateQueues.delete(extensionUUID);
          }
          this.unloadData(extensionUUID);
        },
      };
      };
      this._shutdownHandlers.set(extensionUUID, shutdownHandler);
      this._shutdownHandlers.set(extensionUUID, shutdownHandler);
    }
    }
@@ -247,6 +349,7 @@ class RulesetsStore {
      await savePromise;
      await savePromise;
      this._savePromises.delete(extensionUUID);
      this._savePromises.delete(extensionUUID);
    }
    }

    this._dataPromises.delete(extensionUUID);
    this._dataPromises.delete(extensionUUID);
    this._data.delete(extensionUUID);
    this._data.delete(extensionUUID);
  }
  }
@@ -588,6 +691,80 @@ class RulesetsStore {
      this._savePromises.delete(extensionUUID);
      this._savePromises.delete(extensionUUID);
    }
    }
  }
  }

  /**
   * Internal implementation for updating the enabled rulesets and enforcing
   * static rulesets and rules count limits.
   *
   * @param {Extension}     extension
   * @param {object}        params
   * @param {Array<string>} [params.disableRulesetIds=[]]
   * @param {Array<string>} [params.enableRulesetIds=[]]
   */
  async #updateEnabledStaticRulesets(
    extension,
    { disableRulesetIds, enableRulesetIds }
  ) {
    const ruleResources =
      extension.manifest.declarative_net_request?.rule_resources;
    if (!Array.isArray(ruleResources)) {
      return;
    }

    const enabledRulesets = await this.getEnabledStaticRulesets(extension);
    const updatedEnabledRulesets = new Map();
    let disableIds = new Set(disableRulesetIds);
    let enableIds = new Set(enableRulesetIds);

    // valiate the ruleset ids for existence (which will also reject calls
    // including the reserved _session and _dynamic, because static rulesets
    // id are validated as part of the manifest validation and they are not
    // allowed to start with '_').
    const existingIds = new Set(ruleResources.map(rs => rs.id));
    const errorOnInvalidRulesetIds = rsIdSet => {
      for (const rsId of rsIdSet) {
        if (!existingIds.has(rsId)) {
          throw new ExtensionError(`Invalid ruleset id: "${rsId}"`);
        }
      }
    };
    errorOnInvalidRulesetIds(disableIds);
    errorOnInvalidRulesetIds(enableIds);

    // Copy into the updatedEnabledRulesets Map any ruleset that is not
    // requested to be disabled or is enabled back in the same request.
    for (const [rulesetId, ruleset] of enabledRulesets) {
      if (!disableIds.has(rulesetId) || enableIds.has(rulesetId)) {
        updatedEnabledRulesets.set(rulesetId, ruleset);
        enableIds.delete(rulesetId);
      }
    }

    const { MAX_NUMBER_OF_ENABLED_STATIC_RULESETS } = lazy.ExtensionDNR.limits;

    const maxNewRulesetsCount =
      MAX_NUMBER_OF_ENABLED_STATIC_RULESETS - updatedEnabledRulesets.size;

    if (enableIds.size > maxNewRulesetsCount) {
      // Log an error for the developer.
      throw new ExtensionError(
        `updatedEnabledRulesets request is exceeding MAX_NUMBER_OF_ENABLED_STATIC_RULESETS`
      );
    }

    const newRulesets = await this.#getManifestStaticRulesets(
      extension,
      Array.from(enableIds)
    );

    for (const [rulesetId, ruleset] of newRulesets.entries()) {
      updatedEnabledRulesets.set(rulesetId, ruleset);
    }

    this._data.get(extension.uuid).staticRulesets = updatedEnabledRulesets;
    await this.save(extension);
    await this.updateRulesetManager(extension);
  }
}
}


const store = new RulesetsStore();
const store = new RulesetsStore();
@@ -659,7 +836,9 @@ export const ExtensionDNRStore = {
    return store.clearOnUninstall(extensionUUID);
    return store.clearOnUninstall(extensionUUID);
  },
  },
  initExtension,
  initExtension,

  async updateEnabledStaticRulesets(extension, updateRulesetOptions) {
    await store.updateEnabledStaticRulesets(extension, updateRulesetOptions);
  },
  // Test-only helpers
  // Test-only helpers
  _getStoreForTesting() {
  _getStoreForTesting() {
    requireTestOnlyCallers();
    requireTestOnlyCallers();
+7 −0
Original line number Original line Diff line number Diff line
@@ -52,6 +52,13 @@ this.declarativeNetRequest = class extends ExtensionAPI {
          return ruleManager.enabledStaticRulesetIds;
          return ruleManager.enabledStaticRulesetIds;
        },
        },


        updateEnabledRulesets({ disableRulesetIds, enableRulesetIds }) {
          return ExtensionDNR.updateEnabledStaticRulesets(extension, {
            disableRulesetIds,
            enableRulesetIds,
          });
        },

        getSessionRules() {
        getSessionRules() {
          // ruleManager.getSessionRules() returns an array of Rule instances.
          // ruleManager.getSessionRules() returns an array of Rule instances.
          // When these are structurally cloned (to send them to the child),
          // When these are structurally cloned (to send them to the child),
+31 −0
Original line number Original line Diff line number Diff line
@@ -475,6 +475,37 @@
          }
          }
        ]
        ]
      },
      },
      {
        "name": "updateEnabledRulesets",
        "type": "function",
        "description": "Returns the ids for the current set of enabled static rulesets.",
        "async": "callback",
        "parameters": [
          {
            "name": "updateRulesetOptions",
            "type": "object",
            "properties": {
              "disableRulesetIds": {
                "type": "array",
                "items": { "type": "string" },
                "optional": true,
                "default": []
              },
              "enableRulesetIds": {
                "type": "array",
                "items": { "type": "string" },
                "optional": true,
                "default": []
              }
            }
          },
          {
            "name": "callback",
            "type": "function",
            "parameters": []
          }
        ]
      },
      {
      {
        "name": "getSessionRules",
        "name": "getSessionRules",
        "type": "function",
        "type": "function",
+248 −8
Original line number Original line Diff line number Diff line
@@ -27,6 +27,19 @@ function backgroundWithDNRAPICallHandlers() {
            )
            )
          );
          );
        break;
        break;
      case "updateEnabledRulesets":
        // Run (one or more than one concurrently) updateEnabledRulesets calls
        // and report back the results.
        result = await Promise.all(
          args.map(arg => {
            return browser.declarativeNetRequest
              .updateEnabledRulesets(arg)
              .catch(err => {
                return { rejectedWithErrorMessage: err.message };
              });
          })
        );
        break;
      default:
      default:
        browser.test.fail(`Unexpected test message: ${msg}`);
        browser.test.fail(`Unexpected test message: ${msg}`);
        return;
        return;
@@ -142,6 +155,18 @@ const assertDNRTestMatchOutcome = async (
  );
  );
};
};


const assertDNRGetEnabledRulesets = async (
  extensionTestWrapper,
  expectedRulesetIds
) => {
  extensionTestWrapper.sendMessage("getEnabledRulesets");
  Assert.deepEqual(
    await extensionTestWrapper.awaitMessage("getEnabledRulesets:done"),
    expectedRulesetIds,
    "Got the expected enabled ruleset ids from dnr.getEnabledRulesets API method"
  );
};

const assertDNRStoreData = async (
const assertDNRStoreData = async (
  dnrStore,
  dnrStore,
  extensionTestWrapper,
  extensionTestWrapper,
@@ -160,13 +185,6 @@ const assertDNRStoreData = async (
    return acc;
    return acc;
  }, new Map());
  }, new Map());


  extensionTestWrapper.sendMessage("getEnabledRulesets");
  Assert.deepEqual(
    await extensionTestWrapper.awaitMessage("getEnabledRulesets:done"),
    expectedRulesetIds,
    "Got the expected enabled ruleset ids from dnr.getEnabledRulesets API method"
  );

  ok(
  ok(
    dnrStore._dataPromises.has(extUUID),
    dnrStore._dataPromises.has(extUUID),
    "Got promise for the test extension DNR data being loaded"
    "Got promise for the test extension DNR data being loaded"
@@ -267,6 +285,8 @@ add_task(async function test_load_static_rules() {
  const dnrStore = ExtensionDNRStore._getStoreForTesting();
  const dnrStore = ExtensionDNRStore._getStoreForTesting();


  info("Verify DNRStore data for the test extension");
  info("Verify DNRStore data for the test extension");
  await assertDNRGetEnabledRulesets(extension, ["ruleset_1", "ruleset_2"]);

  await assertDNRStoreData(dnrStore, extension, {
  await assertDNRStoreData(dnrStore, extension, {
    ruleset_1: getSchemaNormalizedRules(extension, ruleset1Data),
    ruleset_1: getSchemaNormalizedRules(extension, ruleset1Data),
    ruleset_2: getSchemaNormalizedRules(extension, ruleset2Data),
    ruleset_2: getSchemaNormalizedRules(extension, ruleset2Data),
@@ -338,6 +358,8 @@ add_task(async function test_load_static_rules() {


  await addon.enable();
  await addon.enable();
  await extension.awaitMessage("bgpage:ready");
  await extension.awaitMessage("bgpage:ready");
  await assertDNRGetEnabledRulesets(extension, ["ruleset_1", "ruleset_2"]);

  await assertDNRStoreData(dnrStore, extension, {
  await assertDNRStoreData(dnrStore, extension, {
    ruleset_1: getSchemaNormalizedRules(extension, ruleset1Data),
    ruleset_1: getSchemaNormalizedRules(extension, ruleset1Data),
    ruleset_2: getSchemaNormalizedRules(extension, ruleset2Data),
    ruleset_2: getSchemaNormalizedRules(extension, ruleset2Data),
@@ -377,6 +399,7 @@ add_task(async function test_load_static_rules() {
    })
    })
  );
  );
  await extension.awaitMessage("bgpage:ready");
  await extension.awaitMessage("bgpage:ready");
  await assertDNRGetEnabledRulesets(extension, ["ruleset_2"]);
  await assertDNRStoreData(dnrStore, extension, {
  await assertDNRStoreData(dnrStore, extension, {
    ruleset_2: getSchemaNormalizedRules(extension, ruleset2Data),
    ruleset_2: getSchemaNormalizedRules(extension, ruleset2Data),
  });
  });
@@ -416,6 +439,7 @@ add_task(async function test_load_static_rules() {
    })
    })
  );
  );
  await extension.awaitMessage("bgpage:ready");
  await extension.awaitMessage("bgpage:ready");
  await assertDNRGetEnabledRulesets(extension, ["ruleset_1"]);
  await assertDNRStoreData(dnrStore, extension, {
  await assertDNRStoreData(dnrStore, extension, {
    ruleset_1: getSchemaNormalizedRules(extension, ruleset1Data),
    ruleset_1: getSchemaNormalizedRules(extension, ruleset1Data),
  });
  });
@@ -443,6 +467,7 @@ add_task(async function test_load_static_rules() {
    })
    })
  );
  );
  await extension.awaitMessage("bgpage:ready");
  await extension.awaitMessage("bgpage:ready");
  await assertDNRGetEnabledRulesets(extension, []);
  await assertDNRStoreData(dnrStore, extension, {});
  await assertDNRStoreData(dnrStore, extension, {});


  info("Verify matched rules using testMatchOutcome");
  info("Verify matched rules using testMatchOutcome");
@@ -672,6 +697,137 @@ add_task(async function test_ruleset_validation() {
  }
  }
});
});


add_task(async function test_updateEnabledRuleset_id_validation() {
  const rule_resources = [
    {
      id: "ruleset_1",
      enabled: true,
      path: "ruleset_1.json",
    },
    {
      id: "ruleset_2",
      enabled: false,
      path: "ruleset_2.json",
    },
  ];

  const ruleset1Data = [
    getDNRRule({
      action: { type: "allow" },
      condition: { resourceTypes: ["main_frame"] },
    }),
  ];
  const ruleset2Data = [
    getDNRRule({
      action: { type: "block" },
      condition: { resourceTypes: ["main_frame", "script"] },
    }),
  ];

  const files = {
    "ruleset_1.json": JSON.stringify(ruleset1Data),
    "ruleset_2.json": JSON.stringify(ruleset2Data),
  };

  let extension = ExtensionTestUtils.loadExtension(
    getDNRExtension({ rule_resources, files })
  );

  await extension.startup();
  await extension.awaitMessage("bgpage:ready");

  await assertDNRGetEnabledRulesets(extension, ["ruleset_1"]);

  const dnrStore = ExtensionDNRStore._getStoreForTesting();
  await assertDNRStoreData(dnrStore, extension, {
    ruleset_1: getSchemaNormalizedRules(extension, ruleset1Data),
  });

  const invalidStaticRulesetIds = [
    // The following two are reserved for session and dynamic rules.
    "_session",
    "_dynamic",
    "ruleset_non_existing",
  ];

  for (const invalidRSId of invalidStaticRulesetIds) {
    extension.sendMessage(
      "updateEnabledRulesets",
      // Only in rulesets to be disabled.
      { disableRulesetIds: [invalidRSId] },
      // Only in rulesets to be enabled.
      { enableRulesetIds: [invalidRSId] },
      // In both rulesets to be enabled and disabled.
      { disableRulesetIds: [invalidRSId], enableRulesetIds: [invalidRSId] },
      // Along with existing rulesets (and expected the existing rulesets
      // to stay unchanged due to the invalid ruleset ids.)
      {
        disableRulesetIds: [invalidRSId, "ruleset_1"],
        enableRulesetIds: [invalidRSId, "ruleset_2"],
      }
    );
    const [
      resInDisable,
      resInEnable,
      resInEnableAndDisable,
      resInSameRequestAsValid,
    ] = await extension.awaitMessage("updateEnabledRulesets:done");
    await Assert.rejects(
      Promise.reject(resInDisable?.rejectedWithErrorMessage),
      new RegExp(`Invalid ruleset id: "${invalidRSId}"`),
      `Got the expected rejection on invalid ruleset id "${invalidRSId}" in disableRulesetIds`
    );
    await Assert.rejects(
      Promise.reject(resInEnable?.rejectedWithErrorMessage),
      new RegExp(`Invalid ruleset id: "${invalidRSId}"`),
      `Got the expected rejection on invalid ruleset id "${invalidRSId}" in enableRulesetIds`
    );
    await Assert.rejects(
      Promise.reject(resInEnableAndDisable?.rejectedWithErrorMessage),
      new RegExp(`Invalid ruleset id: "${invalidRSId}"`),
      `Got the expected rejection on invalid ruleset id "${invalidRSId}" in both enable/disableRulesetIds`
    );
    await Assert.rejects(
      Promise.reject(resInSameRequestAsValid?.rejectedWithErrorMessage),
      new RegExp(`Invalid ruleset id: "${invalidRSId}"`),
      `Got the expected rejection on invalid ruleset id "${invalidRSId}" along with valid ruleset ids`
    );
  }

  // Confirm that the expected rulesets didn't change neither.
  await assertDNRGetEnabledRulesets(extension, ["ruleset_1"]);
  await assertDNRStoreData(dnrStore, extension, {
    ruleset_1: getSchemaNormalizedRules(extension, ruleset1Data),
  });

  // - List the same ruleset ids more than ones is expected to work and
  //   to be resulting in the same set of rules being enabled
  // - Disabling and Enabling the same ruleset id should result in the
  //   ruleset being enabled.
  await extension.sendMessage("updateEnabledRulesets", {
    disableRulesetIds: [
      "ruleset_1",
      "ruleset_1",
      "ruleset_2",
      "ruleset_2",
      "ruleset_2",
    ],
    enableRulesetIds: ["ruleset_2", "ruleset_2"],
  });
  Assert.deepEqual(
    await extension.awaitMessage("updateEnabledRulesets:done"),
    [undefined],
    "Expect the updateEnabledRulesets to result successfully"
  );

  await assertDNRGetEnabledRulesets(extension, ["ruleset_2"]);
  await assertDNRStoreData(dnrStore, extension, {
    ruleset_2: getSchemaNormalizedRules(extension, ruleset2Data),
  });

  await extension.unload();
});

add_task(async function test_static_rulesets_limits() {
add_task(async function test_static_rulesets_limits() {
  const dnrStore = ExtensionDNRStore._getStoreForTesting();
  const dnrStore = ExtensionDNRStore._getStoreForTesting();


@@ -719,13 +875,14 @@ add_task(async function test_static_rulesets_limits() {
    })
    })
  );
  );


  const expectedEnabledRulesets = {};

  const { messages } = await AddonTestUtils.promiseConsoleOutput(async () => {
  const { messages } = await AddonTestUtils.promiseConsoleOutput(async () => {
    ExtensionTestUtils.failOnSchemaWarnings(false);
    ExtensionTestUtils.failOnSchemaWarnings(false);
    await extension.startup();
    await extension.startup();
    ExtensionTestUtils.failOnSchemaWarnings(true);
    ExtensionTestUtils.failOnSchemaWarnings(true);
    await extension.awaitMessage("bgpage:ready");
    await extension.awaitMessage("bgpage:ready");


    const expectedEnabledRulesets = {};
    for (let i = 0; i < MAX_NUMBER_OF_ENABLED_STATIC_RULESETS; i++) {
    for (let i = 0; i < MAX_NUMBER_OF_ENABLED_STATIC_RULESETS; i++) {
      expectedEnabledRulesets[`ruleset_${i}`] = getSchemaNormalizedRules(
      expectedEnabledRulesets[`ruleset_${i}`] = getSchemaNormalizedRules(
        extension,
        extension,
@@ -733,6 +890,10 @@ add_task(async function test_static_rulesets_limits() {
      );
      );
    }
    }


    await assertDNRGetEnabledRulesets(
      extension,
      Array.from(Object.keys(expectedEnabledRulesets))
    );
    await assertDNRStoreData(dnrStore, extension, expectedEnabledRulesets);
    await assertDNRStoreData(dnrStore, extension, expectedEnabledRulesets);
  });
  });


@@ -753,5 +914,84 @@ add_task(async function test_static_rulesets_limits() {
    ],
    ],
  });
  });


  info(
    "Verify updateEnabledRulesets reject when the request is exceeding the enabled rulesets count limit"
  );
  extension.sendMessage("updateEnabledRulesets", {
    disableRulesetIds: ["ruleset_0"],
    enableRulesetIds: ["ruleset_10", "ruleset_11"],
  });

  await Assert.rejects(
    extension.awaitMessage("updateEnabledRulesets:done").then(results => {
      if (results[0].rejectedWithErrorMessage) {
        return Promise.reject(new Error(results[0].rejectedWithErrorMessage));
      }
      return results[0];
    }),
    /updatedEnabledRulesets request is exceeding MAX_NUMBER_OF_ENABLED_STATIC_RULESETS/,
    "Expected rejection on updateEnabledRulesets exceeting enabled rulesets count limit"
  );

  // Confirm that the expected rulesets didn't change neither.
  await assertDNRGetEnabledRulesets(
    extension,
    Array.from(Object.keys(expectedEnabledRulesets))
  );
  await assertDNRStoreData(dnrStore, extension, expectedEnabledRulesets);

  info(
    "Verify updateEnabledRulesets applies the expected changes when resolves successfully"
  );
  extension.sendMessage(
    "updateEnabledRulesets",
    {
      disableRulesetIds: ["ruleset_0"],
      enableRulesetIds: ["ruleset_10"],
    },
    {
      disableRulesetIds: ["ruleset_10"],
      enableRulesetIds: ["ruleset_11"],
    }
  );
  await extension.awaitMessage("updateEnabledRulesets:done");

  // Expect ruleset_0 disabled, ruleset_10 to be enabled but then disabled by the
  // second update queued after the first one, and ruleset_11 to be enabled.
  delete expectedEnabledRulesets.ruleset_0;
  expectedEnabledRulesets.ruleset_11 = getSchemaNormalizedRules(
    extension,
    rules
  );

  await assertDNRGetEnabledRulesets(
    extension,
    Array.from(Object.keys(expectedEnabledRulesets))
  );
  await assertDNRStoreData(dnrStore, extension, expectedEnabledRulesets);

  // Ensure all changes were stored and reloaded from disk store and the
  // DNR store update queue can accept new updates.
  info("Verify static rules load and updates after extension is restarted");
  await AddonTestUtils.promiseRestartManager();
  await extension.awaitStartup();
  await extension.awaitMessage("bgpage:ready");
  await assertDNRGetEnabledRulesets(
    extension,
    Array.from(Object.keys(expectedEnabledRulesets))
  );
  await assertDNRStoreData(dnrStore, extension, expectedEnabledRulesets);

  extension.sendMessage("updateEnabledRulesets", {
    disableRulesetIds: ["ruleset_11"],
  });
  await extension.awaitMessage("updateEnabledRulesets:done");
  delete expectedEnabledRulesets.ruleset_11;
  await assertDNRGetEnabledRulesets(
    extension,
    Array.from(Object.keys(expectedEnabledRulesets))
  );
  await assertDNRStoreData(dnrStore, extension, expectedEnabledRulesets);

  await extension.unload();
  await extension.unload();
});
});