diff options
Diffstat (limited to '')
-rw-r--r-- | browser/components/search/test/unit/test_domain_to_categories_store.js | 361 |
1 files changed, 361 insertions, 0 deletions
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(); +}); |