summaryrefslogtreecommitdiffstats
path: root/browser/components/search/test/unit
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--browser/components/search/test/unit/corruptDB.sqlitebin0 -> 32772 bytes
-rw-r--r--browser/components/search/test/unit/test_domain_to_categories_store.js361
-rw-r--r--browser/components/search/test/unit/test_search_telemetry_categorization_sync.js75
-rw-r--r--browser/components/search/test/unit/test_search_telemetry_config_validation.js2
-rw-r--r--browser/components/search/test/unit/test_ui_schemas_valid.js31
-rw-r--r--browser/components/search/test/unit/xpcshell.toml11
6 files changed, 472 insertions, 8 deletions
diff --git a/browser/components/search/test/unit/corruptDB.sqlite b/browser/components/search/test/unit/corruptDB.sqlite
new file mode 100644
index 0000000000..b234246cac
--- /dev/null
+++ b/browser/components/search/test/unit/corruptDB.sqlite
Binary files differ
diff --git a/browser/components/search/test/unit/test_domain_to_categories_store.js b/browser/components/search/test/unit/test_domain_to_categories_store.js
new file mode 100644
index 0000000000..e3af0c8de5
--- /dev/null
+++ b/browser/components/search/test/unit/test_domain_to_categories_store.js
@@ -0,0 +1,361 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Ensure that the domain to categories store public methods work as expected
+ * and it handles all error cases as expected.
+ */
+
+ChromeUtils.defineESModuleGetters(this, {
+ CATEGORIZATION_SETTINGS: "resource:///modules/SearchSERPTelemetry.sys.mjs",
+ DomainToCategoriesStore: "resource:///modules/SearchSERPTelemetry.sys.mjs",
+ sinon: "resource://testing-common/Sinon.sys.mjs",
+ Sqlite: "resource://gre/modules/Sqlite.sys.mjs",
+});
+
+let store = new DomainToCategoriesStore();
+let defaultStorePath;
+let fileContents = [convertToBuffer({ foo: [0, 1] })];
+
+async function createCorruptedStore() {
+ info("Create a corrupted store.");
+ let storePath = PathUtils.join(
+ PathUtils.profileDir,
+ CATEGORIZATION_SETTINGS.STORE_FILE
+ );
+ let src = PathUtils.join(do_get_cwd().path, "corruptDB.sqlite");
+ await IOUtils.copy(src, storePath);
+ Assert.ok(await IOUtils.exists(storePath), "Store exists.");
+ return storePath;
+}
+
+function convertToBuffer(obj) {
+ return new TextEncoder().encode(JSON.stringify(obj)).buffer;
+}
+
+/**
+ * Deletes data from the store and removes any files that were generated due
+ * to them.
+ */
+async function cleanup() {
+ info("Clean up store.");
+
+ // In these tests, we sometimes use read-only files to test permission error
+ // handling. On Windows, we have to change it to writable to allow for their
+ // deletion so that subsequent tests aren't affected.
+ if (
+ (await IOUtils.exists(defaultStorePath)) &&
+ Services.appinfo.OS == "WINNT"
+ ) {
+ await IOUtils.setPermissions(defaultStorePath, 0o600);
+ }
+
+ await store.testDelete();
+ Assert.equal(store.empty, true, "Store should be empty.");
+ Assert.equal(await IOUtils.exists(defaultStorePath), false, "Store exists.");
+ Assert.equal(
+ await store.getVersion(),
+ 0,
+ "Version number should be 0 when store is empty."
+ );
+
+ await store.uninit();
+}
+
+async function createReadOnlyStore() {
+ info("Create a store that can't be read.");
+ let storePath = PathUtils.join(
+ PathUtils.profileDir,
+ CATEGORIZATION_SETTINGS.STORE_FILE
+ );
+
+ let conn = await Sqlite.openConnection({ path: storePath });
+ await conn.execute("CREATE TABLE test (id INTEGER PRIMARY KEY)");
+ await conn.close();
+
+ await changeStoreToReadOnly();
+}
+
+async function changeStoreToReadOnly() {
+ info("Change store to read only.");
+ let storePath = PathUtils.join(
+ PathUtils.profileDir,
+ CATEGORIZATION_SETTINGS.STORE_FILE
+ );
+ let stat = await IOUtils.stat(storePath);
+ await IOUtils.setPermissions(storePath, 0o444);
+ stat = await IOUtils.stat(storePath);
+ Assert.equal(stat.permissions, 0o444, "Permissions should be read only.");
+ Assert.ok(await IOUtils.exists(storePath), "Store exists.");
+}
+
+add_setup(async function () {
+ // We need a profile directory to create the store and open a connection.
+ do_get_profile();
+ defaultStorePath = PathUtils.join(
+ PathUtils.profileDir,
+ CATEGORIZATION_SETTINGS.STORE_FILE
+ );
+ registerCleanupFunction(async () => {
+ await cleanup();
+ });
+});
+
+// Ensure the test only function deletes the store.
+add_task(async function delete_store() {
+ let storePath = await createCorruptedStore();
+ await store.testDelete();
+ Assert.ok(!(await IOUtils.exists(storePath)), "Store doesn't exist.");
+});
+
+/**
+ * These tests check common no fail scenarios.
+ */
+
+add_task(async function init_insert_uninit() {
+ await store.init();
+ let result = await store.getCategories("foo");
+ Assert.deepEqual(result, [], "foo should not have a result.");
+ Assert.equal(
+ await store.getVersion(),
+ 0,
+ "Version number should not be set."
+ );
+ Assert.equal(store.empty, true, "Store should be empty.");
+
+ info("Try inserting after init.");
+ await store.insertFileContents(fileContents, 1);
+
+ result = await store.getCategories("foo");
+ Assert.deepEqual(result, [0, 1], "foo should have a matching result.");
+ Assert.equal(await store.getVersion(), 1, "Version number should be set.");
+ Assert.equal(store.empty, false, "Store should not be empty.");
+
+ info("Un-init store.");
+ await store.uninit();
+
+ result = await store.getCategories("foo");
+ Assert.deepEqual(result, [], "foo should be removed from store.");
+ Assert.equal(store.empty, true, "Store should be empty.");
+ Assert.equal(await store.getVersion(), 0, "Version should be reset.");
+
+ await cleanup();
+});
+
+add_task(async function insert_and_re_init() {
+ await store.init();
+ await store.insertFileContents(fileContents, 20240202);
+
+ let result = await store.getCategories("foo");
+ Assert.deepEqual(result, [0, 1], "foo should have a matching result.");
+ Assert.equal(
+ await store.getVersion(),
+ 20240202,
+ "Version number should be set."
+ );
+ Assert.equal(store.empty, false, "Is store empty.");
+
+ info("Simulate a restart.");
+ await store.uninit();
+ await store.init();
+
+ result = await store.getCategories("foo");
+ Assert.deepEqual(
+ result,
+ [0, 1],
+ "After restart, foo should still be in the store."
+ );
+ Assert.equal(
+ await store.getVersion(),
+ 20240202,
+ "Version number should still be in the store."
+ );
+ Assert.equal(store.empty, false, "Is store empty.");
+
+ await cleanup();
+});
+
+// Simulate consecutive updates.
+add_task(async function insert_multiple_times() {
+ await store.init();
+ let result = await store.getCategories("foo");
+ Assert.deepEqual(result, [], "foo should not have a result.");
+ Assert.equal(
+ await store.getVersion(),
+ 0,
+ "Version number should not be set."
+ );
+ Assert.equal(store.empty, true, "Is store empty.");
+
+ for (let i = 0; i < 3; ++i) {
+ info("Try inserting after init.");
+ await store.insertFileContents(fileContents, 1);
+
+ result = await store.getCategories("foo");
+ Assert.deepEqual(result, [0, 1], "foo should have a matching result.");
+ Assert.equal(store.empty, false, "Is store empty.");
+ Assert.equal(await store.getVersion(), 1, "Version number is set.");
+
+ await store.dropData();
+ result = await store.getCategories("foo");
+ Assert.deepEqual(
+ result,
+ [],
+ "After dropping data, foo should no longer have a matching result."
+ );
+ Assert.equal(await store.getVersion(), 0, "Version should be reset.");
+ Assert.equal(store.empty, true, "Is store empty.");
+ }
+
+ await cleanup();
+});
+
+/**
+ * The following tests check failures on store initialization.
+ */
+
+add_task(async function init_with_corrupted_store() {
+ await createCorruptedStore();
+
+ info("Initialize the store.");
+ await store.init();
+
+ info("Try inserting after the corrupted store was replaced.");
+ await store.insertFileContents(fileContents, 1);
+
+ let result = await store.getCategories("foo");
+ Assert.deepEqual(result, [0, 1], "foo should have a matching result.");
+ Assert.equal(await store.getVersion(), 1, "Version number is set.");
+ Assert.equal(store.empty, false, "Is store empty.");
+
+ await cleanup();
+});
+
+add_task(async function init_with_unfixable_store() {
+ let sandbox = sinon.createSandbox();
+ sandbox.stub(Sqlite, "openConnection").throws();
+
+ info("Initialize the store.");
+ await store.init();
+
+ info("Try inserting content even if the connection is impossible to fix.");
+ await store.dropData();
+ await store.insertFileContents(fileContents, 20240202);
+
+ let result = await store.getCategories("foo");
+ Assert.deepEqual(result, [], "foo should not have a result.");
+ Assert.equal(await store.getVersion(), 0, "Version should be reset.");
+ Assert.equal(store.empty, true, "Store should be empty.");
+
+ sandbox.restore();
+ await cleanup();
+});
+
+add_task(async function init_read_only_store() {
+ await createReadOnlyStore();
+ await store.init();
+
+ info("Insert contents into the store.");
+ await store.insertFileContents(fileContents, 20240202);
+ let result = await store.getCategories("foo");
+ Assert.deepEqual(result, [], "foo should not have a result.");
+ Assert.equal(
+ await store.getVersion(),
+ 0,
+ "Version number should not be set."
+ );
+ Assert.equal(store.empty, true, "Store should be empty.");
+
+ await cleanup();
+});
+
+add_task(async function init_close_to_shutdown() {
+ let sandbox = sinon.createSandbox();
+ sandbox.stub(Sqlite.shutdown, "addBlocker").throws(new Error());
+ await store.init();
+
+ let result = await store.getCategories("foo");
+ Assert.deepEqual(result, [], "foo should not have a result.");
+ Assert.equal(
+ await store.getVersion(),
+ 0,
+ "Version number should not be set."
+ );
+ Assert.equal(store.empty, true, "Store should be empty.");
+
+ sandbox.restore();
+ await cleanup();
+});
+
+/**
+ * The following tests check error handling when inserting data into the store.
+ */
+
+add_task(async function insert_broken_file() {
+ await store.init();
+
+ Assert.equal(
+ await store.getVersion(),
+ 0,
+ "Version number should not be set."
+ );
+
+ info("Try inserting one valid file and an invalid file.");
+ let contents = [...fileContents, new ArrayBuffer(0).buffer];
+ await store.insertFileContents(contents, 20240202);
+
+ let result = await store.getCategories("foo");
+ Assert.deepEqual(result, [], "foo should not have a result.");
+ Assert.equal(await store.getVersion(), 0, "Version should remain unset.");
+ Assert.equal(store.empty, true, "Store should remain empty.");
+
+ await cleanup();
+});
+
+add_task(async function insert_into_read_only_store() {
+ await createReadOnlyStore();
+ await store.init();
+
+ await store.dropData();
+ await store.insertFileContents(fileContents, 20240202);
+ let result = await store.getCategories("foo");
+ Assert.deepEqual(result, [], "foo should not have a result.");
+ Assert.equal(await store.getVersion(), 0, "Version should remain unset.");
+ Assert.equal(store.empty, true, "Store should remain empty.");
+
+ await cleanup();
+});
+
+// If the store becomes read only with content already inside of it,
+// the next time we try opening it, we'll encounter an error trying to write to
+// it. Since we are no longer able to manipulate it, the results should always
+// be empty.
+add_task(async function restart_with_read_only_store() {
+ await store.init();
+ await store.insertFileContents(fileContents, 20240202);
+
+ info("Check store has content.");
+ let result = await store.getCategories("foo");
+ Assert.deepEqual(result, [0, 1], "foo should have a matching result.");
+ Assert.equal(
+ await store.getVersion(),
+ 20240202,
+ "Version number should be set."
+ );
+ Assert.equal(store.empty, false, "Store should not be empty.");
+
+ await changeStoreToReadOnly();
+ await store.uninit();
+ await store.init();
+
+ result = await store.getCategories("foo");
+ Assert.deepEqual(
+ result,
+ [],
+ "foo should no longer have a matching value from the store."
+ );
+ Assert.equal(await store.getVersion(), 0, "Version number should be unset.");
+ Assert.equal(store.empty, true, "Store should be empty.");
+
+ await cleanup();
+});
diff --git a/browser/components/search/test/unit/test_search_telemetry_categorization_sync.js b/browser/components/search/test/unit/test_search_telemetry_categorization_sync.js
index 40d38efbba..2351347d77 100644
--- a/browser/components/search/test/unit/test_search_telemetry_categorization_sync.js
+++ b/browser/components/search/test/unit/test_search_telemetry_categorization_sync.js
@@ -9,6 +9,7 @@
ChromeUtils.defineESModuleGetters(this, {
RemoteSettings: "resource://services-settings/remote-settings.sys.mjs",
+ SearchSERPCategorization: "resource:///modules/SearchSERPTelemetry.sys.mjs",
SearchSERPDomainToCategoriesMap:
"resource:///modules/SearchSERPTelemetry.sys.mjs",
TELEMETRY_CATEGORIZATION_KEY:
@@ -158,7 +159,7 @@ add_task(async function test_initial_import() {
// Clean up.
await db.clear();
- SearchSERPDomainToCategoriesMap.uninit();
+ await SearchSERPDomainToCategoriesMap.uninit(true);
});
add_task(async function test_update_records() {
@@ -219,7 +220,7 @@ add_task(async function test_update_records() {
// Clean up.
await db.clear();
- SearchSERPDomainToCategoriesMap.uninit();
+ await SearchSERPDomainToCategoriesMap.uninit(true);
});
add_task(async function test_delayed_initial_import() {
@@ -273,7 +274,7 @@ add_task(async function test_delayed_initial_import() {
// Clean up.
await db.clear();
- SearchSERPDomainToCategoriesMap.uninit();
+ await SearchSERPDomainToCategoriesMap.uninit(true);
});
add_task(async function test_remove_record() {
@@ -332,7 +333,7 @@ add_task(async function test_remove_record() {
// Clean up.
await db.clear();
- SearchSERPDomainToCategoriesMap.uninit();
+ await SearchSERPDomainToCategoriesMap.uninit(true);
});
add_task(async function test_different_versions_coexisting() {
@@ -380,7 +381,7 @@ add_task(async function test_different_versions_coexisting() {
// Clean up.
await db.clear();
- SearchSERPDomainToCategoriesMap.uninit();
+ await SearchSERPDomainToCategoriesMap.uninit(true);
});
add_task(async function test_download_error() {
@@ -449,5 +450,67 @@ add_task(async function test_download_error() {
// Clean up.
await db.clear();
- SearchSERPDomainToCategoriesMap.uninit();
+ await SearchSERPDomainToCategoriesMap.uninit(true);
+});
+
+add_task(async function test_mock_restart() {
+ info("Create record containing domain_category_mappings_2a.json attachment.");
+ let record2a = await mockRecordWithCachedAttachment(RECORDS.record2a);
+ await db.create(record2a);
+
+ info("Create record containing domain_category_mappings_2b.json attachment.");
+ let record2b = await mockRecordWithCachedAttachment(RECORDS.record2b);
+ await db.create(record2b);
+
+ info("Add data to Remote Settings DB.");
+ await db.importChanges({}, Date.now());
+
+ info("Initialize search categorization mappings.");
+ let promise = waitForDomainToCategoriesUpdate();
+ await SearchSERPCategorization.init();
+ await promise;
+
+ Assert.deepEqual(
+ await SearchSERPDomainToCategoriesMap.get("example.com"),
+ [
+ {
+ category: 1,
+ score: 80,
+ },
+ ],
+ "Should have a record."
+ );
+
+ Assert.equal(
+ SearchSERPDomainToCategoriesMap.version,
+ 2,
+ "Version should be the latest."
+ );
+
+ info("Mock a restart by un-initializing the map.");
+ await SearchSERPCategorization.uninit();
+ promise = waitForDomainToCategoriesUpdate();
+ await SearchSERPCategorization.init();
+ await promise;
+
+ Assert.deepEqual(
+ await SearchSERPDomainToCategoriesMap.get("example.com"),
+ [
+ {
+ category: 1,
+ score: 80,
+ },
+ ],
+ "Should have a record."
+ );
+
+ Assert.equal(
+ SearchSERPDomainToCategoriesMap.version,
+ 2,
+ "Version should be the latest."
+ );
+
+ // Clean up.
+ await db.clear();
+ await SearchSERPDomainToCategoriesMap.uninit(true);
});
diff --git a/browser/components/search/test/unit/test_search_telemetry_config_validation.js b/browser/components/search/test/unit/test_search_telemetry_config_validation.js
index 8897b1e7c7..d14f7a3918 100644
--- a/browser/components/search/test/unit/test_search_telemetry_config_validation.js
+++ b/browser/components/search/test/unit/test_search_telemetry_config_validation.js
@@ -57,7 +57,7 @@ function disallowAdditionalProperties(section) {
add_task(async function test_search_telemetry_validates_to_schema() {
let schema = await IOUtils.readJSON(
- PathUtils.join(do_get_cwd().path, "search-telemetry-schema.json")
+ PathUtils.join(do_get_cwd().path, "search-telemetry-v2-schema.json")
);
disallowAdditionalProperties(schema);
diff --git a/browser/components/search/test/unit/test_ui_schemas_valid.js b/browser/components/search/test/unit/test_ui_schemas_valid.js
new file mode 100644
index 0000000000..3396f38238
--- /dev/null
+++ b/browser/components/search/test/unit/test_ui_schemas_valid.js
@@ -0,0 +1,31 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+let schemas = [
+ ["search-telemetry-v2-schema.json", "search-telemetry-v2-ui-schema.json"],
+];
+
+async function checkUISchemaValid(configSchema, uiSchema) {
+ for (let key of Object.keys(configSchema.properties)) {
+ Assert.ok(
+ uiSchema["ui:order"].includes(key),
+ `Should have ${key} listed at the top-level of the ui schema`
+ );
+ }
+}
+
+add_task(async function test_ui_schemas_valid() {
+ for (let [schema, uiSchema] of schemas) {
+ info(`Validating ${uiSchema} has every top-level from ${schema}`);
+ let schemaData = await IOUtils.readJSON(
+ PathUtils.join(do_get_cwd().path, schema)
+ );
+ let uiSchemaData = await IOUtils.readJSON(
+ PathUtils.join(do_get_cwd().path, uiSchema)
+ );
+
+ await checkUISchemaValid(schemaData, uiSchemaData);
+ }
+});
diff --git a/browser/components/search/test/unit/xpcshell.toml b/browser/components/search/test/unit/xpcshell.toml
index 423d218d19..24e1d78eb5 100644
--- a/browser/components/search/test/unit/xpcshell.toml
+++ b/browser/components/search/test/unit/xpcshell.toml
@@ -6,6 +6,9 @@ prefs = ["browser.search.log=true"]
skip-if = ["os == 'android'"] # bug 1730213
firefox-appdir = "browser"
+["test_domain_to_categories_store.js"]
+support-files = ["corruptDB.sqlite"]
+
["test_search_telemetry_categorization_logic.js"]
["test_search_telemetry_categorization_sync.js"]
@@ -14,7 +17,13 @@ prefs = ["browser.search.serpEventTelemetryCategorization.enabled=true"]
["test_search_telemetry_compare_urls.js"]
["test_search_telemetry_config_validation.js"]
-support-files = ["../../schema/search-telemetry-schema.json"]
+support-files = ["../../schema/search-telemetry-v2-schema.json"]
+
+["test_ui_schemas_valid.js"]
+support-files = [
+ "../../schema/search-telemetry-v2-schema.json",
+ "../../schema/search-telemetry-v2-ui-schema.json",
+]
["test_urlTelemetry.js"]