diff options
Diffstat (limited to 'toolkit/components/satchel/test/unit')
24 files changed, 1922 insertions, 0 deletions
diff --git a/toolkit/components/satchel/test/unit/asyncformhistory_expire.sqlite b/toolkit/components/satchel/test/unit/asyncformhistory_expire.sqlite Binary files differnew file mode 100644 index 0000000000..07b43c2096 --- /dev/null +++ b/toolkit/components/satchel/test/unit/asyncformhistory_expire.sqlite diff --git a/toolkit/components/satchel/test/unit/formhistory_1000.sqlite b/toolkit/components/satchel/test/unit/formhistory_1000.sqlite Binary files differnew file mode 100644 index 0000000000..5eeab074fd --- /dev/null +++ b/toolkit/components/satchel/test/unit/formhistory_1000.sqlite diff --git a/toolkit/components/satchel/test/unit/formhistory_CORRUPT.sqlite b/toolkit/components/satchel/test/unit/formhistory_CORRUPT.sqlite new file mode 100644 index 0000000000..5f7498bfc2 --- /dev/null +++ b/toolkit/components/satchel/test/unit/formhistory_CORRUPT.sqlite @@ -0,0 +1 @@ +BACON diff --git a/toolkit/components/satchel/test/unit/formhistory_apitest.sqlite b/toolkit/components/satchel/test/unit/formhistory_apitest.sqlite Binary files differnew file mode 100644 index 0000000000..00daf03c27 --- /dev/null +++ b/toolkit/components/satchel/test/unit/formhistory_apitest.sqlite diff --git a/toolkit/components/satchel/test/unit/formhistory_autocomplete.sqlite b/toolkit/components/satchel/test/unit/formhistory_autocomplete.sqlite Binary files differnew file mode 100644 index 0000000000..724cff73f6 --- /dev/null +++ b/toolkit/components/satchel/test/unit/formhistory_autocomplete.sqlite diff --git a/toolkit/components/satchel/test/unit/formhistory_v3.sqlite b/toolkit/components/satchel/test/unit/formhistory_v3.sqlite Binary files differnew file mode 100644 index 0000000000..e0e8fe2468 --- /dev/null +++ b/toolkit/components/satchel/test/unit/formhistory_v3.sqlite diff --git a/toolkit/components/satchel/test/unit/formhistory_v3v4.sqlite b/toolkit/components/satchel/test/unit/formhistory_v3v4.sqlite Binary files differnew file mode 100644 index 0000000000..8eab177e97 --- /dev/null +++ b/toolkit/components/satchel/test/unit/formhistory_v3v4.sqlite diff --git a/toolkit/components/satchel/test/unit/formhistory_v999a.sqlite b/toolkit/components/satchel/test/unit/formhistory_v999a.sqlite Binary files differnew file mode 100644 index 0000000000..216bce4a34 --- /dev/null +++ b/toolkit/components/satchel/test/unit/formhistory_v999a.sqlite diff --git a/toolkit/components/satchel/test/unit/formhistory_v999b.sqlite b/toolkit/components/satchel/test/unit/formhistory_v999b.sqlite Binary files differnew file mode 100644 index 0000000000..fe400d04a1 --- /dev/null +++ b/toolkit/components/satchel/test/unit/formhistory_v999b.sqlite diff --git a/toolkit/components/satchel/test/unit/head_satchel.js b/toolkit/components/satchel/test/unit/head_satchel.js new file mode 100644 index 0000000000..3ff06b89fe --- /dev/null +++ b/toolkit/components/satchel/test/unit/head_satchel.js @@ -0,0 +1,188 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const CURRENT_SCHEMA = 5; +const PR_HOURS = 60 * 60 * 1000000; + +var { XPCOMUtils } = ChromeUtils.importESModule( + "resource://gre/modules/XPCOMUtils.sys.mjs" +); + +ChromeUtils.defineESModuleGetters(this, { + FormHistory: "resource://gre/modules/FormHistory.sys.mjs", + FormHistoryTestUtils: + "resource://testing-common/FormHistoryTestUtils.sys.mjs", + Sqlite: "resource://gre/modules/Sqlite.sys.mjs", +}); + +do_get_profile(); + +// Send the profile-after-change notification to the form history component to ensure +// that it has been initialized. +var formHistoryStartup = Cc[ + "@mozilla.org/satchel/form-history-startup;1" +].getService(Ci.nsIObserver); +formHistoryStartup.observe(null, "profile-after-change", null); + +async function getDBVersion(dbfile) { + let dbConnection = await Sqlite.openConnection({ path: dbfile.path }); + let version = await dbConnection.getSchemaVersion(); + await dbConnection.close(); + + return version; +} + +async function getDBSchemaVersion(path) { + let db = await Sqlite.openConnection({ path }); + try { + return await db.getSchemaVersion(); + } finally { + await db.close(); + } +} + +function getFormHistoryDBVersion() { + let profileDir = do_get_profile(); + // Cleanup from any previous tests or failures. + let dbFile = profileDir.clone(); + dbFile.append("formhistory.sqlite"); + return getDBVersion(dbFile); +} + +const isGUID = /[A-Za-z0-9\+\/]{16}/; + +// Find form history entries. +function searchEntries(terms, params, iter) { + FormHistory.search(terms, params).then( + results => iter.next(results), + error => do_throw("Error occurred searching form history: " + error) + ); +} + +// Count the number of entries with the given name and value, and call then(number) +// when done. If name or value is null, then the value of that field does not matter. +function countEntries(name, value, then) { + let obj = {}; + if (name !== null) { + obj.fieldname = name; + } + if (value !== null) { + obj.value = value; + } + + FormHistory.count(obj).then( + count => { + then(count); + }, + error => { + do_throw("Error occurred searching form history: " + error); + } + ); +} + +// Perform a single form history update and call then() when done. +function updateEntry(op, name, value, then) { + let obj = { op }; + if (name !== null) { + obj.fieldname = name; + } + if (value !== null) { + obj.value = value; + } + updateFormHistory(obj, then); +} + +// Add a single form history entry with the current time and call then() when done. +function addEntry(name, value, then) { + let now = Date.now() * 1000; + updateFormHistory( + { + op: "add", + fieldname: name, + value, + timesUsed: 1, + firstUsed: now, + lastUsed: now, + }, + then + ); +} + +function promiseCountEntries(name, value, checkFn = () => {}) { + return new Promise(resolve => { + countEntries(name, value, function (result) { + checkFn(result); + resolve(result); + }); + }); +} + +function promiseUpdateEntry(op, name, value) { + return new Promise(res => { + updateEntry(op, name, value, res); + }); +} + +function promiseAddEntry(name, value) { + return new Promise(res => { + addEntry(name, value, res); + }); +} + +// Wrapper around FormHistory.update which handles errors. Calls then() when done. +function updateFormHistory(changes, then) { + FormHistory.update(changes).then(then, error => { + do_throw("Error occurred updating form history: " + error); + }); +} + +function promiseUpdate(change) { + return FormHistory.update(change); +} + +/** + * Logs info to the console in the standard way (includes the filename). + * + * @param {string} aMessage + * The message to log to the console. + */ +function do_log_info(aMessage) { + print("TEST-INFO | " + _TEST_FILE + " | " + aMessage); +} + +/** + * Copies a test file into the profile folder. + * + * @param {string} aFilename + * The name of the file to copy. + * @param {string} aDestFilename + * The name of the file to copy. + * @param {object} [options] + * @param {object} [options.overwriteExisting] + * Whether to overwrite an existing file. + * @returns {string} path to the copied file. + */ +async function copyToProfile( + aFilename, + aDestFilename, + { overwriteExisting = false } = {} +) { + let curDir = Services.dirsvc.get("CurWorkD", Ci.nsIFile).path; + let srcPath = PathUtils.join(curDir, aFilename); + Assert.ok(await IOUtils.exists(srcPath), "Database file found"); + + // Ensure that our file doesn't exist already. + let destPath = PathUtils.join(PathUtils.profileDir, aDestFilename); + let exists = await IOUtils.exists(destPath); + if (exists) { + if (overwriteExisting) { + await IOUtils.remove(destPath); + } else { + throw new Error("The file should not exist"); + } + } + await IOUtils.copy(srcPath, destPath); + info(`Copied ${aFilename} to ${destPath}`); + return destPath; +} diff --git a/toolkit/components/satchel/test/unit/test_async_expire.js b/toolkit/components/satchel/test/unit/test_async_expire.js new file mode 100644 index 0000000000..b6f25b81dd --- /dev/null +++ b/toolkit/components/satchel/test/unit/test_async_expire.js @@ -0,0 +1,134 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +ChromeUtils.defineESModuleGetters(this, { + TestUtils: "resource://testing-common/TestUtils.sys.mjs", +}); + +function promiseExpiration() { + let promise = TestUtils.topicObserved( + "satchel-storage-changed", + (subject, data) => { + return data == "formhistory-expireoldentries"; + } + ); + + // We can't easily fake a "daily idle" event, so for testing purposes form + // history listens for another notification to trigger an immediate + // expiration. + Services.obs.notifyObservers(null, "formhistory-expire-now"); + + return promise; +} + +add_task(async function () { + // ===== test init ===== + let testfile = do_get_file("asyncformhistory_expire.sqlite"); + let profileDir = do_get_profile(); + + // Cleanup from any previous tests or failures. + let dbFile = profileDir.clone(); + dbFile.append("formhistory.sqlite"); + if (dbFile.exists()) { + dbFile.remove(false); + } + + testfile.copyTo(profileDir, "formhistory.sqlite"); + Assert.ok(dbFile.exists()); + + // We're going to clear this at the end, so it better have the default value now. + Assert.ok(!Services.prefs.prefHasUserValue("browser.formfill.expire_days")); + + // Sanity check initial state + Assert.equal(508, await promiseCountEntries(null, null)); + Assert.ok((await promiseCountEntries("name-A", "value-A")) > 0); // lastUsed == distant past + Assert.ok((await promiseCountEntries("name-B", "value-B")) > 0); // lastUsed == distant future + + Assert.equal(CURRENT_SCHEMA, await getDBVersion(dbFile)); + + // Add a new entry + Assert.equal(0, await promiseCountEntries("name-C", "value-C")); + await promiseAddEntry("name-C", "value-C"); + Assert.equal(1, await promiseCountEntries("name-C", "value-C")); + + // Update some existing entries to have ages relative to when the test runs. + let now = 1000 * Date.now(); + let updateLastUsed = (results, age) => { + let lastUsed = now - age * 24 * PR_HOURS; + + let changes = []; + for (let result of results) { + changes.push({ op: "update", lastUsed, guid: result.guid }); + } + + return changes; + }; + + let results = await FormHistory.search(["guid"], { lastUsed: 181 }); + await promiseUpdate(updateLastUsed(results, 181)); + + results = await FormHistory.search(["guid"], { lastUsed: 179 }); + await promiseUpdate(updateLastUsed(results, 179)); + + results = await FormHistory.search(["guid"], { lastUsed: 31 }); + await promiseUpdate(updateLastUsed(results, 31)); + + results = await FormHistory.search(["guid"], { lastUsed: 29 }); + await promiseUpdate(updateLastUsed(results, 29)); + + results = await FormHistory.search(["guid"], { lastUsed: 9999 }); + await promiseUpdate(updateLastUsed(results, 11)); + + results = await FormHistory.search(["guid"], { lastUsed: 9 }); + await promiseUpdate(updateLastUsed(results, 9)); + + Assert.ok((await promiseCountEntries("name-A", "value-A")) > 0); + Assert.ok((await promiseCountEntries("181DaysOld", "foo")) > 0); + Assert.ok((await promiseCountEntries("179DaysOld", "foo")) > 0); + Assert.equal(509, await promiseCountEntries(null, null)); + + // 2 entries are expected to expire. + await promiseExpiration(); + + Assert.equal(0, await promiseCountEntries("name-A", "value-A")); + Assert.equal(0, await promiseCountEntries("181DaysOld", "foo")); + Assert.ok((await promiseCountEntries("179DaysOld", "foo")) > 0); + Assert.equal(507, await promiseCountEntries(null, null)); + + // And again. No change expected. + await promiseExpiration(); + + Assert.equal(507, await promiseCountEntries(null, null)); + + // Set formfill pref to 30 days. + Services.prefs.setIntPref("browser.formfill.expire_days", 30); + + Assert.ok((await promiseCountEntries("179DaysOld", "foo")) > 0); + Assert.ok((await promiseCountEntries("bar", "31days")) > 0); + Assert.ok((await promiseCountEntries("bar", "29days")) > 0); + Assert.equal(507, await promiseCountEntries(null, null)); + + await promiseExpiration(); + + Assert.equal(0, await promiseCountEntries("179DaysOld", "foo")); + Assert.equal(0, await promiseCountEntries("bar", "31days")); + Assert.ok((await promiseCountEntries("bar", "29days")) > 0); + Assert.equal(505, await promiseCountEntries(null, null)); + + // Set override pref to 10 days and expire. This expires a large batch of + // entries, and should trigger a VACCUM to reduce file size. + Services.prefs.setIntPref("browser.formfill.expire_days", 10); + + Assert.ok((await promiseCountEntries("bar", "29days")) > 0); + Assert.ok((await promiseCountEntries("9DaysOld", "foo")) > 0); + Assert.equal(505, await promiseCountEntries(null, null)); + + await promiseExpiration(); + + Assert.equal(0, await promiseCountEntries("bar", "29days")); + Assert.ok((await promiseCountEntries("9DaysOld", "foo")) > 0); + Assert.ok((await promiseCountEntries("name-B", "value-B")) > 0); + Assert.ok((await promiseCountEntries("name-C", "value-C")) > 0); + Assert.equal(3, await promiseCountEntries(null, null)); +}); diff --git a/toolkit/components/satchel/test/unit/test_autocomplete.js b/toolkit/components/satchel/test/unit/test_autocomplete.js new file mode 100644 index 0000000000..13f66eb0f2 --- /dev/null +++ b/toolkit/components/satchel/test/unit/test_autocomplete.js @@ -0,0 +1,388 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +var fac; + +var numRecords, timeGroupingSize, now; + +const DEFAULT_EXPIRE_DAYS = 180; + +function padLeft(number, length) { + let str = number + ""; + while (str.length < length) { + str = "0" + str; + } + return str; +} + +function getFormExpiryDays() { + if (Services.prefs.prefHasUserValue("browser.formfill.expire_days")) { + return Services.prefs.getIntPref("browser.formfill.expire_days"); + } + return DEFAULT_EXPIRE_DAYS; +} + +function run_test() { + // ===== test init ===== + let testfile = do_get_file("formhistory_autocomplete.sqlite"); + let profileDir = Services.dirsvc.get("ProfD", Ci.nsIFile); + + // Cleanup from any previous tests or failures. + let destFile = profileDir.clone(); + destFile.append("formhistory.sqlite"); + if (destFile.exists()) { + destFile.remove(false); + } + + testfile.copyTo(profileDir, "formhistory.sqlite"); + + fac = Cc["@mozilla.org/satchel/form-autocomplete;1"].getService( + Ci.nsIFormAutoComplete + ); + + timeGroupingSize = + Services.prefs.getIntPref("browser.formfill.timeGroupingSize") * + 1000 * + 1000; + + run_next_test(); +} + +add_test(function test0() { + let maxTimeGroupings = Services.prefs.getIntPref( + "browser.formfill.maxTimeGroupings" + ); + let bucketSize = Services.prefs.getIntPref("browser.formfill.bucketSize"); + + // ===== Tests with constant timesUsed and varying lastUsed date ===== + // insert 2 records per bucket to check alphabetical sort within + now = 1000 * Date.now(); + numRecords = Math.ceil(maxTimeGroupings / bucketSize) * 2; + + let changes = []; + for (let i = 0; i < numRecords; i += 2) { + let useDate = now - (i / 2) * bucketSize * timeGroupingSize; + + changes.push({ + op: "add", + fieldname: "field1", + value: "value" + padLeft(numRecords - 1 - i, 2), + timesUsed: 1, + firstUsed: useDate, + lastUsed: useDate, + }); + changes.push({ + op: "add", + fieldname: "field1", + value: "value" + padLeft(numRecords - 2 - i, 2), + timesUsed: 1, + firstUsed: useDate, + lastUsed: useDate, + }); + } + + updateFormHistory(changes, run_next_test); +}); + +add_test(function test1() { + do_log_info("Check initial state is as expected"); + + countEntries(null, null, function () { + countEntries("field1", null, function (count) { + Assert.ok(count > 0); + run_next_test(); + }); + }); +}); + +add_test(function test2() { + do_log_info("Check search contains all entries"); + + fac.autoCompleteSearchAsync("field1", "", null, null, false, { + onSearchCompletion(aResults) { + Assert.equal(numRecords, aResults.matchCount); + run_next_test(); + }, + }); +}); + +add_test(function test3() { + do_log_info("Check search result ordering with empty search term"); + + let lastFound = numRecords; + fac.autoCompleteSearchAsync("field1", "", null, null, false, { + onSearchCompletion(aResults) { + for (let i = 0; i < numRecords; i += 2) { + Assert.equal( + parseInt(aResults.getValueAt(i + 1).substr(5), 10), + --lastFound + ); + Assert.equal( + parseInt(aResults.getValueAt(i).substr(5), 10), + --lastFound + ); + } + run_next_test(); + }, + }); +}); + +add_test(function test4() { + do_log_info('Check search result ordering with "v"'); + + let lastFound = numRecords; + fac.autoCompleteSearchAsync("field1", "v", null, null, false, { + onSearchCompletion(aResults) { + for (let i = 0; i < numRecords; i += 2) { + Assert.equal( + parseInt(aResults.getValueAt(i + 1).substr(5), 10), + --lastFound + ); + Assert.equal( + parseInt(aResults.getValueAt(i).substr(5), 10), + --lastFound + ); + } + run_next_test(); + }, + }); +}); + +const timesUsedSamples = 20; + +add_test(function test5() { + do_log_info("Begin tests with constant use dates and varying timesUsed"); + + let changes = []; + for (let i = 0; i < timesUsedSamples; i++) { + let timesUsed = timesUsedSamples - i; + let change = { + op: "add", + fieldname: "field2", + value: "value" + (timesUsedSamples - 1 - i), + timesUsed: timesUsed * timeGroupingSize, + firstUsed: now, + lastUsed: now, + }; + changes.push(change); + } + updateFormHistory(changes, run_next_test); +}); + +add_test(function test6() { + do_log_info("Check search result ordering with empty search term"); + + let lastFound = timesUsedSamples; + fac.autoCompleteSearchAsync("field2", "", null, null, false, { + onSearchCompletion(aResults) { + for (let i = 0; i < timesUsedSamples; i++) { + Assert.equal( + parseInt(aResults.getValueAt(i).substr(5), 10), + --lastFound + ); + } + run_next_test(); + }, + }); +}); + +add_test(function test7() { + do_log_info('Check search result ordering with "v"'); + + let lastFound = timesUsedSamples; + fac.autoCompleteSearchAsync("field2", "v", null, null, false, { + onSearchCompletion(aResults) { + for (let i = 0; i < timesUsedSamples; i++) { + Assert.equal( + parseInt(aResults.getValueAt(i).substr(5), 10), + --lastFound + ); + } + run_next_test(); + }, + }); +}); + +add_test(function test8() { + do_log_info( + 'Check that "senior citizen" entries get a bonus (browser.formfill.agedBonus)' + ); + + let agedDate = + 1000 * (Date.now() - getFormExpiryDays() * 24 * 60 * 60 * 1000); + + let changes = []; + changes.push({ + op: "add", + fieldname: "field3", + value: "old but not senior", + timesUsed: 100, + firstUsed: agedDate + 60 * 1000 * 1000, + lastUsed: now, + }); + changes.push({ + op: "add", + fieldname: "field3", + value: "senior citizen", + timesUsed: 100, + firstUsed: agedDate - 60 * 1000 * 1000, + lastUsed: now, + }); + updateFormHistory(changes, run_next_test); +}); + +add_test(function test9() { + fac.autoCompleteSearchAsync("field3", "", null, null, false, { + onSearchCompletion(aResults) { + Assert.equal(aResults.getValueAt(0), "senior citizen"); + Assert.equal(aResults.getValueAt(1), "old but not senior"); + run_next_test(); + }, + }); +}); + +add_test(function test10() { + do_log_info("Check entries that are really old or in the future"); + + let changes = []; + changes.push({ + op: "add", + fieldname: "field4", + value: "date of 0", + timesUsed: 1, + firstUsed: 0, + lastUsed: 0, + }); + changes.push({ + op: "add", + fieldname: "field4", + value: "in the future 1", + timesUsed: 1, + firstUsed: 0, + lastUsed: now * 2, + }); + changes.push({ + op: "add", + fieldname: "field4", + value: "in the future 2", + timesUsed: 1, + firstUsed: now * 2, + lastUsed: now * 2, + }); + updateFormHistory(changes, run_next_test); +}); + +add_test(function test11() { + fac.autoCompleteSearchAsync("field4", "", null, null, false, { + onSearchCompletion(aResults) { + Assert.equal(aResults.matchCount, 3); + run_next_test(); + }, + }); +}); + +var syncValues = ["sync1", "sync1a", "sync2", "sync3"]; + +add_test(function test12() { + do_log_info("Check old synchronous api"); + + let changes = []; + for (let value of syncValues) { + changes.push({ op: "add", fieldname: "field5", value }); + } + updateFormHistory(changes, run_next_test); +}); + +add_test(function test_token_limit_DB() { + function test_token_limit_previousResult(previousResult) { + do_log_info( + "Check that the number of tokens used in a search is not capped to " + + "MAX_SEARCH_TOKENS when using a previousResult" + ); + // This provide more accuracy since performance is less of an issue. + // Search for a string where the first 10 tokens match the previous value but the 11th does not + // when re-using a previous result. + fac.autoCompleteSearchAsync( + "field_token_cap", + "a b c d e f g h i j .", + null, + previousResult, + false, + { + onSearchCompletion(aResults) { + Assert.equal( + aResults.matchCount, + 0, + "All search tokens should be used with previous results" + ); + run_next_test(); + }, + } + ); + } + + do_log_info( + "Check that the number of tokens used in a search is capped to MAX_SEARCH_TOKENS " + + "for performance when querying the DB" + ); + let changes = []; + changes.push({ + op: "add", + fieldname: "field_token_cap", + // value with 15 unique tokens + value: "a b c d e f g h i j k l m n o", + timesUsed: 1, + firstUsed: 0, + lastUsed: 0, + }); + updateFormHistory(changes, () => { + // Search for a string where the first 10 tokens match the value above but the 11th does not + // (which would prevent the result from being returned if the 11th term was used). + fac.autoCompleteSearchAsync( + "field_token_cap", + "a b c d e f g h i j .", + null, + null, + false, + { + onSearchCompletion(aResults) { + Assert.equal( + aResults.matchCount, + 1, + "Only the first MAX_SEARCH_TOKENS tokens " + + "should be used for DB queries" + ); + test_token_limit_previousResult(aResults); + }, + } + ); + }); +}); + +add_test(async function can_search_escape_marker() { + await promiseUpdate({ + op: "add", + fieldname: "field1", + value: "/* Further reading */ test", + timesUsed: 1, + firstUsed: now, + lastUsed: now, + }); + + fac.autoCompleteSearchAsync( + "field1", + "/* Further reading */ t", + null, + null, + false, + { + onSearchCompletion(aResults) { + Assert.equal(1, aResults.matchCount); + run_next_test(); + }, + } + ); +}); diff --git a/toolkit/components/satchel/test/unit/test_db_access_denied.js b/toolkit/components/satchel/test/unit/test_db_access_denied.js new file mode 100644 index 0000000000..99e038ea43 --- /dev/null +++ b/toolkit/components/satchel/test/unit/test_db_access_denied.js @@ -0,0 +1,52 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +var bakFile; +var dbFile; + +function run_test() { + let testfile = do_get_file("formhistory_apitest.sqlite"); + let profileDir = Services.dirsvc.get("ProfD", Ci.nsIFile); + + // Cleanup from any previous tests or failures. + let destFile = profileDir.clone(); + destFile.append("formhistory.sqlite"); + if (destFile.exists()) { + destFile.remove(false); + } + + bakFile = profileDir.clone(); + bakFile.append("formhistory.sqlite.corrupt"); + if (bakFile.exists()) { + bakFile.remove(false); + } + + dbFile = profileDir.clone(); + dbFile.append("formhistory.sqlite"); + + testfile.copyTo(profileDir, "formhistory.sqlite"); + + run_next_test(); +} + +add_test(async function initialize_database_in_readonly_results_in_db_reset() { + // original permissions are 440, now set to not readable... + dbFile.permissions = 0; + + // ...and reset them later for the next connection setup retry, which happens + // after 3 retries (10 + 20 + 40) ms + do_timeout(70, () => (dbFile.permissions = 440)); + + // this establishes a connection, the first one will fail but after a few + // retries we will have sufficiant permissions + const numEntriesAfter = await FormHistory.count({}); + + // original fixture data present + Assert.equal(9, numEntriesAfter); + + // No backup has been created + Assert.ok(!bakFile.exists(), "backup file does not exist"); + + run_next_test(); +}); diff --git a/toolkit/components/satchel/test/unit/test_db_corrupt.js b/toolkit/components/satchel/test/unit/test_db_corrupt.js new file mode 100644 index 0000000000..b53b5cd6d0 --- /dev/null +++ b/toolkit/components/satchel/test/unit/test_db_corrupt.js @@ -0,0 +1,80 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +var bakFile; + +function run_test() { + // ===== test init ===== + let testfile = do_get_file("formhistory_CORRUPT.sqlite"); + let profileDir = Services.dirsvc.get("ProfD", Ci.nsIFile); + + // Cleanup from any previous tests or failures. + let destFile = profileDir.clone(); + destFile.append("formhistory.sqlite"); + if (destFile.exists()) { + destFile.remove(false); + } + + bakFile = profileDir.clone(); + bakFile.append("formhistory.sqlite.corrupt"); + if (bakFile.exists()) { + bakFile.remove(false); + } + + testfile.copyTo(profileDir, "formhistory.sqlite"); + run_next_test(); +} + +add_test(function test_corruptFormHistoryDB_lazyCorruptInit1() { + do_log_info("ensure FormHistory backs up a corrupt DB on initialization."); + + // DB init is done lazily so the DB shouldn't be created yet. + Assert.ok(!bakFile.exists()); + // Doing any request to the DB should create it. + countEntries(null, null, run_next_test); +}); + +add_test(function test_corruptFormHistoryDB_lazyCorruptInit2() { + Assert.ok(bakFile.exists()); + bakFile.remove(false); + run_next_test(); +}); + +add_test(function test_corruptFormHistoryDB_emptyInit() { + do_log_info( + "test that FormHistory initializes an empty DB in place of corrupt DB." + ); + + (async function () { + let count = await FormHistory.count({}); + Assert.equal(count, 0); + count = await FormHistory.count({ fieldname: "name-A", value: "value-A" }); + Assert.equal(count, 0); + run_next_test(); + })().catch(error => { + do_throw("DB initialized after reading a corrupt DB file is not empty."); + }); +}); + +add_test(function test_corruptFormHistoryDB_addEntry() { + do_log_info("test adding an entry to the empty DB."); + + updateEntry("add", "name-A", "value-A", function () { + countEntries("name-A", "value-A", function (count) { + Assert.ok(count == 1); + run_next_test(); + }); + }); +}); + +add_test(function test_corruptFormHistoryDB_removeEntry() { + do_log_info("test removing an entry to the empty DB."); + + updateEntry("remove", "name-A", "value-A", function () { + countEntries("name-A", "value-A", function (count) { + Assert.ok(count == 0); + run_next_test(); + }); + }); +}); diff --git a/toolkit/components/satchel/test/unit/test_db_update_v4.js b/toolkit/components/satchel/test/unit/test_db_update_v4.js new file mode 100644 index 0000000000..8c39dd7788 --- /dev/null +++ b/toolkit/components/satchel/test/unit/test_db_update_v4.js @@ -0,0 +1,59 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const { Sqlite } = ChromeUtils.importESModule( + "resource://gre/modules/Sqlite.sys.mjs" +); + +add_task(async function () { + let testnum = 0; + + try { + // ===== test init ===== + let testfile = do_get_file("formhistory_v3.sqlite"); + let profileDir = Services.dirsvc.get("ProfD", Ci.nsIFile); + + // Cleanup from any previous tests or failures. + let destFile = profileDir.clone(); + destFile.append("formhistory.sqlite"); + if (destFile.exists()) { + destFile.remove(false); + } + + testfile.copyTo(profileDir, "formhistory.sqlite"); + Assert.equal(3, await getDBVersion(testfile)); + + Assert.ok(destFile.exists()); + + // ===== 1 ===== + testnum++; + + destFile = profileDir.clone(); + destFile.append("formhistory.sqlite"); + let dbConnection = await Sqlite.openConnection({ + path: destFile.path, + sharedMemoryCache: false, + }); + + // Do something that will cause FormHistory to access and upgrade the + // database + await FormHistory.count({}); + + // check for upgraded schema. + Assert.equal(CURRENT_SCHEMA, await getDBVersion(destFile)); + + // Check that the index was added + Assert.ok(dbConnection.tableExists("moz_deleted_formhistory")); + dbConnection.close(); + + // check for upgraded schema. + Assert.equal(CURRENT_SCHEMA, await getDBVersion(destFile)); + + // check that an entry still exists + let num = await promiseCountEntries("name-A", "value-A"); + Assert.ok(num > 0); + } catch (e) { + throw new Error(`FAILED in test #${testnum} -- ${e}`); + } +}); diff --git a/toolkit/components/satchel/test/unit/test_db_update_v4b.js b/toolkit/components/satchel/test/unit/test_db_update_v4b.js new file mode 100644 index 0000000000..d3319d2956 --- /dev/null +++ b/toolkit/components/satchel/test/unit/test_db_update_v4b.js @@ -0,0 +1,49 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +add_task(async function () { + let testnum = 0; + + try { + // ===== test init ===== + let testfile = do_get_file("formhistory_v3v4.sqlite"); + let profileDir = Services.dirsvc.get("ProfD", Ci.nsIFile); + + // Cleanup from any previous tests or failures. + let destFile = profileDir.clone(); + destFile.append("formhistory.sqlite"); + if (destFile.exists()) { + destFile.remove(false); + } + + testfile.copyTo(profileDir, "formhistory.sqlite"); + Assert.equal(3, await getDBVersion(testfile)); + + // ===== 1 ===== + testnum++; + + destFile = profileDir.clone(); + destFile.append("formhistory.sqlite"); + let dbConnection = await Sqlite.openConnection({ + path: destFile.path, + sharedMemoryCache: false, + }); + + // Do something that will cause FormHistory to access and upgrade the + // database + await FormHistory.count({}); + + // check for upgraded schema. + Assert.equal(CURRENT_SCHEMA, await getDBVersion(destFile)); + + // Check that the index was added + Assert.ok(dbConnection.tableExists("moz_deleted_formhistory")); + dbConnection.close(); + + // check that an entry still exists + Assert.ok((await promiseCountEntries("name-A", "value-A")) > 0); + } catch (e) { + throw new Error(`FAILED in test #${testnum} -- ${e}`); + } +}); diff --git a/toolkit/components/satchel/test/unit/test_db_update_v5.js b/toolkit/components/satchel/test/unit/test_db_update_v5.js new file mode 100644 index 0000000000..f546d4a0f5 --- /dev/null +++ b/toolkit/components/satchel/test/unit/test_db_update_v5.js @@ -0,0 +1,29 @@ +/* Any copyright is dedicated to the Public Domain. + * https://creativecommons.org/publicdomain/zero/1.0/ */ + +add_task(async function () { + let destPath = await copyToProfile( + "formhistory_v3.sqlite", + "formhistory.sqlite" + ); + Assert.equal(3, await getDBSchemaVersion(destPath)); + + // Do something that will cause FormHistory to access and upgrade the + // database + await FormHistory.count({}); + + // check for upgraded schema. + Assert.equal(CURRENT_SCHEMA, await getDBSchemaVersion(destPath)); + + // Check that the source tables were added. + let db = await Sqlite.openConnection({ path: destPath }); + try { + Assert.ok(db.tableExists("moz_sources")); + Assert.ok(db.tableExists("moz_sources_to_history")); + } finally { + await db.close(); + } + // check that an entry still exists + let num = await promiseCountEntries("name-A", "value-A"); + Assert.ok(num > 0); +}); diff --git a/toolkit/components/satchel/test/unit/test_db_update_v999a.js b/toolkit/components/satchel/test/unit/test_db_update_v999a.js new file mode 100644 index 0000000000..f48c43c5cc --- /dev/null +++ b/toolkit/components/satchel/test/unit/test_db_update_v999a.js @@ -0,0 +1,56 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* + * This test uses a formhistory.sqlite with schema version set to 999 (a + * future version). This exercies the code that allows using a future schema + * version as long as the expected columns are present. + * + * Part A tests this when the columns do match, so the DB is used. + * Part B tests this when the columns do *not* match, so the DB is reset. + */ + +add_task(async function () { + let testnum = 0; + + try { + // ===== test init ===== + let testfile = do_get_file("formhistory_v999a.sqlite"); + let profileDir = Services.dirsvc.get("ProfD", Ci.nsIFile); + + // Cleanup from any previous tests or failures. + let destFile = profileDir.clone(); + destFile.append("formhistory.sqlite"); + if (destFile.exists()) { + destFile.remove(false); + } + + testfile.copyTo(profileDir, "formhistory.sqlite"); + Assert.equal(999, await getDBVersion(testfile)); + + // ===== 1 ===== + testnum++; + // Check for expected contents. + Assert.ok((await promiseCountEntries(null, null)) > 0); + Assert.equal(1, await promiseCountEntries("name-A", "value-A")); + Assert.equal(1, await promiseCountEntries("name-B", "value-B")); + Assert.equal(1, await promiseCountEntries("name-C", "value-C1")); + Assert.equal(1, await promiseCountEntries("name-C", "value-C2")); + Assert.equal(1, await promiseCountEntries("name-E", "value-E")); + + // check for downgraded schema. + Assert.equal(CURRENT_SCHEMA, await getDBVersion(destFile)); + + // ===== 2 ===== + testnum++; + // Exercise adding and removing a name/value pair + Assert.equal(0, await promiseCountEntries("name-D", "value-D")); + await promiseUpdateEntry("add", "name-D", "value-D"); + Assert.equal(1, await promiseCountEntries("name-D", "value-D")); + await promiseUpdateEntry("remove", "name-D", "value-D"); + Assert.equal(0, await promiseCountEntries("name-D", "value-D")); + } catch (e) { + throw new Error(`FAILED in test #${testnum} -- ${e}`); + } +}); diff --git a/toolkit/components/satchel/test/unit/test_db_update_v999b.js b/toolkit/components/satchel/test/unit/test_db_update_v999b.js new file mode 100644 index 0000000000..3424411343 --- /dev/null +++ b/toolkit/components/satchel/test/unit/test_db_update_v999b.js @@ -0,0 +1,74 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* + * This test uses a formhistory.sqlite with schema version set to 999 (a + * future version). This exercies the code that allows using a future schema + * version as long as the expected columns are present. + * + * Part A tests this when the columns do match, so the DB is used. + * Part B tests this when the columns do *not* match, so the DB is reset. + */ + +add_task(async function () { + let testnum = 0; + + try { + // ===== test init ===== + let testfile = do_get_file("formhistory_v999b.sqlite"); + let profileDir = Services.dirsvc.get("ProfD", Ci.nsIFile); + + // Cleanup from any previous tests or failures. + let destFile = profileDir.clone(); + destFile.append("formhistory.sqlite"); + if (destFile.exists()) { + destFile.remove(false); + } + + let bakFile = profileDir.clone(); + bakFile.append("formhistory.sqlite.corrupt"); + if (bakFile.exists()) { + bakFile.remove(false); + } + + testfile.copyTo(profileDir, "formhistory.sqlite"); + Assert.equal(999, await getDBVersion(destFile)); + + // ===== 1 ===== + testnum++; + + // Open the DB, ensure that a backup of the corrupt DB is made. + // DB init is done lazily so the DB shouldn't be created yet. + Assert.ok(!bakFile.exists()); + // Doing any request to the DB should create it. + await promiseCountEntries("", ""); + + Assert.ok(bakFile.exists()); + bakFile.remove(false); + + // ===== 2 ===== + testnum++; + // File should be empty + Assert.ok(!(await promiseCountEntries(null, null))); + Assert.equal(0, await promiseCountEntries("name-A", "value-A")); + // check for current schema. + Assert.equal(CURRENT_SCHEMA, await getDBVersion(destFile)); + + // ===== 3 ===== + testnum++; + // Try adding an entry + await promiseUpdateEntry("add", "name-A", "value-A"); + Assert.equal(1, await promiseCountEntries(null, null)); + Assert.equal(1, await promiseCountEntries("name-A", "value-A")); + + // ===== 4 ===== + testnum++; + // Try removing an entry + await promiseUpdateEntry("remove", "name-A", "value-A"); + Assert.equal(0, await promiseCountEntries(null, null)); + Assert.equal(0, await promiseCountEntries("name-A", "value-A")); + } catch (e) { + throw new Error(`FAILED in test #${testnum} -- ${e}`); + } +}); diff --git a/toolkit/components/satchel/test/unit/test_history_api.js b/toolkit/components/satchel/test/unit/test_history_api.js new file mode 100644 index 0000000000..91da24696c --- /dev/null +++ b/toolkit/components/satchel/test/unit/test_history_api.js @@ -0,0 +1,485 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +var testnum = 0; +var dbConnection; // used for deleted table tests + +async function countDeletedEntries(expected) { + let stmt = "SELECT COUNT(*) AS numEntries FROM moz_deleted_formhistory"; + try { + let requiredRow = await dbConnection.executeCached(stmt); + Assert.equal(expected, requiredRow[0].getResultByName("numEntries")); + } catch (error) { + do_throw("Error occurred counting deleted entries: " + error); + } +} + +async function checkTimeDeleted(guid, checkFunction) { + let stmt = + "SELECT timeDeleted FROM moz_deleted_formhistory WHERE guid = :guid"; + let params = { guid }; + + try { + let requiredRow = await dbConnection.executeCached(stmt, params); + checkFunction(requiredRow[0].getResultByName("timeDeleted")); + } catch (error) { + do_throw("Error occurred getting deleted entries: " + error); + } +} + +function promiseUpdateEntry(op, name, value) { + let change = { op }; + if (name !== null) { + change.fieldname = name; + } + if (value !== null) { + change.value = value; + } + return promiseUpdate(change); +} + +add_task(async function () { + let oldSupportsDeletedTable = FormHistory._supportsDeletedTable; + FormHistory._supportsDeletedTable = true; + + try { + // ===== test init ===== + let testfile = do_get_file("formhistory_apitest.sqlite"); + let profileDir = Services.dirsvc.get("ProfD", Ci.nsIFile); + + // Cleanup from any previous tests or failures. + let destFile = profileDir.clone(); + destFile.append("formhistory.sqlite"); + if (destFile.exists()) { + destFile.remove(false); + } + + testfile.copyTo(profileDir, "formhistory.sqlite"); + + function checkExists(num) { + Assert.ok(num > 0); + } + function checkNotExists(num) { + Assert.ok(num == 0); + } + + // ===== 1 ===== + // Check initial state is as expected + testnum++; + await promiseCountEntries("name-A", null, checkExists); + await promiseCountEntries("name-B", null, checkExists); + await promiseCountEntries("name-C", null, checkExists); + await promiseCountEntries("name-D", null, checkExists); + await promiseCountEntries("name-A", "value-A", checkExists); + await promiseCountEntries("name-B", "value-B1", checkExists); + await promiseCountEntries("name-B", "value-B2", checkExists); + await promiseCountEntries("name-C", "value-C", checkExists); + await promiseCountEntries("name-D", "value-D", checkExists); + // time-A/B/C/D checked below. + + // Delete anything from the deleted table + let dbFile = Services.dirsvc.get("ProfD", Ci.nsIFile).clone(); + dbFile.append("formhistory.sqlite"); + + dbConnection = await Sqlite.openConnection({ + path: dbFile.path, + sharedMemoryCache: false, + }); + + let stmt = "DELETE FROM moz_deleted_formhistory"; + try { + await dbConnection.executeCached(stmt); + } catch (error) { + do_throw("Error occurred counting deleted all entries: " + error); + } + + // ===== 2 ===== + // Test looking for nonexistent / bogus data. + testnum++; + await promiseCountEntries("blah", null, checkNotExists); + await promiseCountEntries("", null, checkNotExists); + await promiseCountEntries("name-A", "blah", checkNotExists); + await promiseCountEntries("name-A", "", checkNotExists); + await promiseCountEntries("name-A", null, checkExists); + await promiseCountEntries("blah", "value-A", checkNotExists); + await promiseCountEntries("", "value-A", checkNotExists); + await promiseCountEntries(null, "value-A", checkExists); + + // Cannot use promiseCountEntries when name and value are null + // because it treats null values as not set + // and here a search should be done explicity for null. + let count = await FormHistory.count({ fieldname: null, value: null }); + checkNotExists(count); + + // ===== 3 ===== + // Test removeEntriesForName with a single matching value + testnum++; + await promiseUpdateEntry("remove", "name-A", null); + + await promiseCountEntries("name-A", "value-A", checkNotExists); + await promiseCountEntries("name-B", "value-B1", checkExists); + await promiseCountEntries("name-B", "value-B2", checkExists); + await promiseCountEntries("name-C", "value-C", checkExists); + await promiseCountEntries("name-D", "value-D", checkExists); + await countDeletedEntries(1); + + // ===== 4 ===== + // Test removeEntriesForName with multiple matching values + testnum++; + await promiseUpdateEntry("remove", "name-B", null); + + await promiseCountEntries("name-A", "value-A", checkNotExists); + await promiseCountEntries("name-B", "value-B1", checkNotExists); + await promiseCountEntries("name-B", "value-B2", checkNotExists); + await promiseCountEntries("name-C", "value-C", checkExists); + await promiseCountEntries("name-D", "value-D", checkExists); + await countDeletedEntries(3); + + // ===== 5 ===== + // Test removing by time range (single entry, not surrounding entries) + testnum++; + await promiseCountEntries("time-A", null, checkExists); // firstUsed=1000, lastUsed=1000 + await promiseCountEntries("time-B", null, checkExists); // firstUsed=1000, lastUsed=1099 + await promiseCountEntries("time-C", null, checkExists); // firstUsed=1099, lastUsed=1099 + await promiseCountEntries("time-D", null, checkExists); // firstUsed=2001, lastUsed=2001 + await promiseUpdate({ + op: "remove", + firstUsedStart: 1050, + firstUsedEnd: 2000, + }); + + await promiseCountEntries("time-A", null, checkExists); + await promiseCountEntries("time-B", null, checkExists); + await promiseCountEntries("time-C", null, checkNotExists); + await promiseCountEntries("time-D", null, checkExists); + await countDeletedEntries(4); + + // ===== 6 ===== + // Test removing by time range (multiple entries) + testnum++; + await promiseUpdate({ + op: "remove", + firstUsedStart: 1000, + firstUsedEnd: 2000, + }); + + await promiseCountEntries("time-A", null, checkNotExists); + await promiseCountEntries("time-B", null, checkNotExists); + await promiseCountEntries("time-C", null, checkNotExists); + await promiseCountEntries("time-D", null, checkExists); + await countDeletedEntries(6); + + // ===== 7 ===== + // test removeAllEntries + testnum++; + await promiseUpdateEntry("remove", null, null); + + await promiseCountEntries("name-C", null, checkNotExists); + await promiseCountEntries("name-D", null, checkNotExists); + await promiseCountEntries("name-C", "value-C", checkNotExists); + await promiseCountEntries("name-D", "value-D", checkNotExists); + + await promiseCountEntries(null, null, checkNotExists); + await countDeletedEntries(6); + + // ===== 8 ===== + // Add a single entry back + testnum++; + await promiseUpdateEntry("add", "newname-A", "newvalue-A"); + await promiseCountEntries("newname-A", "newvalue-A", checkExists); + + // ===== 9 ===== + // Remove the single entry + testnum++; + await promiseUpdateEntry("remove", "newname-A", "newvalue-A"); + await promiseCountEntries("newname-A", "newvalue-A", checkNotExists); + + // ===== 10 ===== + // Add a single entry + testnum++; + await promiseUpdateEntry("add", "field1", "value1"); + await promiseCountEntries("field1", "value1", checkExists); + + let processFirstResult = function processResults(results) { + // Only handle the first result + if (results.length) { + let result = results[0]; + return [ + result.timesUsed, + result.firstUsed, + result.lastUsed, + result.guid, + ]; + } + return undefined; + }; + + let results = await FormHistory.search( + ["timesUsed", "firstUsed", "lastUsed"], + { fieldname: "field1", value: "value1" } + ); + let [timesUsed, firstUsed, lastUsed] = processFirstResult(results); + Assert.equal(1, timesUsed); + Assert.ok(firstUsed > 0); + Assert.ok(lastUsed > 0); + await promiseCountEntries(null, null, num => Assert.equal(num, 1)); + + // ===== 11 ===== + // Add another single entry + testnum++; + await promiseUpdateEntry("add", "field1", "value1b"); + await promiseCountEntries("field1", "value1", checkExists); + await promiseCountEntries("field1", "value1b", checkExists); + await promiseCountEntries(null, null, num => Assert.equal(num, 2)); + + // ===== 12 ===== + // Update a single entry + testnum++; + + results = await FormHistory.search(["guid"], { + fieldname: "field1", + value: "value1", + }); + let guid = processFirstResult(results)[3]; + + await promiseUpdate({ op: "update", guid, value: "modifiedValue" }); + await promiseCountEntries("field1", "modifiedValue", checkExists); + await promiseCountEntries("field1", "value1", checkNotExists); + await promiseCountEntries("field1", "value1b", checkExists); + await promiseCountEntries(null, null, num => Assert.equal(num, 2)); + + // ===== 13 ===== + // Add a single entry with times + testnum++; + await promiseUpdate({ + op: "add", + fieldname: "field2", + value: "value2", + timesUsed: 20, + firstUsed: 100, + lastUsed: 500, + }); + + results = await FormHistory.search(["timesUsed", "firstUsed", "lastUsed"], { + fieldname: "field2", + value: "value2", + }); + [timesUsed, firstUsed, lastUsed] = processFirstResult(results); + + Assert.equal(20, timesUsed); + Assert.equal(100, firstUsed); + Assert.equal(500, lastUsed); + await promiseCountEntries(null, null, num => Assert.equal(num, 3)); + + // ===== 14 ===== + // Bump an entry, which updates its lastUsed field + testnum++; + await promiseUpdate({ + op: "bump", + fieldname: "field2", + value: "value2", + timesUsed: 20, + firstUsed: 100, + lastUsed: 500, + }); + results = await FormHistory.search(["timesUsed", "firstUsed", "lastUsed"], { + fieldname: "field2", + value: "value2", + }); + [timesUsed, firstUsed, lastUsed] = processFirstResult(results); + Assert.equal(21, timesUsed); + Assert.equal(100, firstUsed); + Assert.ok(lastUsed > 500); + await promiseCountEntries(null, null, num => Assert.equal(num, 3)); + + // ===== 15 ===== + // Bump an entry that does not exist + testnum++; + await promiseUpdate({ + op: "bump", + fieldname: "field3", + value: "value3", + timesUsed: 10, + firstUsed: 50, + lastUsed: 400, + }); + results = await FormHistory.search(["timesUsed", "firstUsed", "lastUsed"], { + fieldname: "field3", + value: "value3", + }); + [timesUsed, firstUsed, lastUsed] = processFirstResult(results); + Assert.equal(10, timesUsed); + Assert.equal(50, firstUsed); + Assert.equal(400, lastUsed); + await promiseCountEntries(null, null, num => Assert.equal(num, 4)); + + // ===== 16 ===== + // Bump an entry with a guid + testnum++; + results = await FormHistory.search(["guid"], { + fieldname: "field3", + value: "value3", + }); + guid = processFirstResult(results)[3]; + await promiseUpdate({ + op: "bump", + guid, + timesUsed: 20, + firstUsed: 55, + lastUsed: 400, + }); + results = await FormHistory.search(["timesUsed", "firstUsed", "lastUsed"], { + fieldname: "field3", + value: "value3", + }); + [timesUsed, firstUsed, lastUsed] = processFirstResult(results); + Assert.equal(11, timesUsed); + Assert.equal(50, firstUsed); + Assert.ok(lastUsed > 400); + await promiseCountEntries(null, null, num => Assert.equal(num, 4)); + + // ===== 17 ===== + // Remove an entry + testnum++; + await countDeletedEntries(7); + + results = await FormHistory.search(["guid"], { + fieldname: "field1", + value: "value1b", + }); + guid = processFirstResult(results)[3]; + + await promiseUpdate({ op: "remove", guid }); + await promiseCountEntries("field1", "modifiedValue", checkExists); + await promiseCountEntries("field1", "value1b", checkNotExists); + await promiseCountEntries(null, null, num => Assert.equal(num, 3)); + + await countDeletedEntries(8); + await checkTimeDeleted(guid, timeDeleted => Assert.ok(timeDeleted > 10000)); + + // ===== 18 ===== + // Add yet another single entry + testnum++; + await promiseUpdate({ + op: "add", + fieldname: "field4", + value: "value4", + timesUsed: 5, + firstUsed: 230, + lastUsed: 600, + }); + await promiseCountEntries(null, null, num => Assert.equal(num, 4)); + + // ===== 19 ===== + // Remove an entry by time + testnum++; + await promiseUpdate({ + op: "remove", + firstUsedStart: 60, + firstUsedEnd: 250, + }); + await promiseCountEntries("field1", "modifiedValue", checkExists); + await promiseCountEntries("field2", "value2", checkNotExists); + await promiseCountEntries("field3", "value3", checkExists); + await promiseCountEntries("field4", "value4", checkNotExists); + await promiseCountEntries(null, null, num => Assert.equal(num, 2)); + await countDeletedEntries(10); + + // ===== 20 ===== + // Bump multiple existing entries at once + testnum++; + + await promiseUpdate([ + { + op: "add", + fieldname: "field5", + value: "value5", + timesUsed: 5, + firstUsed: 230, + lastUsed: 600, + }, + { + op: "add", + fieldname: "field6", + value: "value6", + timesUsed: 12, + firstUsed: 430, + lastUsed: 700, + }, + ]); + await promiseCountEntries(null, null, num => Assert.equal(num, 4)); + + await promiseUpdate([ + { op: "bump", fieldname: "field5", value: "value5" }, + { op: "bump", fieldname: "field6", value: "value6" }, + ]); + results = await FormHistory.search( + ["fieldname", "timesUsed", "firstUsed", "lastUsed"], + {} + ); + + Assert.equal(6, results[2].timesUsed); + Assert.equal(13, results[3].timesUsed); + Assert.equal(230, results[2].firstUsed); + Assert.equal(430, results[3].firstUsed); + Assert.ok(results[2].lastUsed > 600); + Assert.ok(results[3].lastUsed > 700); + + await promiseCountEntries(null, null, num => Assert.equal(num, 4)); + + // ===== 21 ===== + // Check update fails if form history is disabled and the operation is not a + // pure removal. + testnum++; + Services.prefs.setBoolPref("browser.formfill.enable", false); + + // Cannot use arrow functions, see bug 1237961. + await Assert.rejects( + promiseUpdate({ op: "bump", fieldname: "field5", value: "value5" }), + /Form history is disabled, only remove operations are allowed/, + "bumping when form history is disabled should fail" + ); + await Assert.rejects( + promiseUpdate({ op: "add", fieldname: "field5", value: "value5" }), + /Form history is disabled, only remove operations are allowed/, + "Adding when form history is disabled should fail" + ); + await Assert.rejects( + promiseUpdate([ + { op: "update", fieldname: "field5", value: "value5" }, + { op: "remove", fieldname: "field5", value: "value5" }, + ]), + /Form history is disabled, only remove operations are allowed/, + "mixed operations when form history is disabled should fail" + ); + await Assert.rejects( + promiseUpdate([ + null, + undefined, + "", + 1, + {}, + { op: "remove", fieldname: "field5", value: "value5" }, + ]), + /Form history is disabled, only remove operations are allowed/, + "Invalid entries when form history is disabled should fail" + ); + + // Remove should work though. + await promiseUpdate([ + { op: "remove", fieldname: "field5", value: null }, + { op: "remove", fieldname: null, value: null }, + ]); + Services.prefs.clearUserPref("browser.formfill.enable"); + } catch (e) { + throw new Error(`FAILED in test #${testnum} -- ${e}`); + } finally { + FormHistory._supportsDeletedTable = oldSupportsDeletedTable; + await dbConnection.close(do_test_finished); + } +}); + +function run_test() { + return run_next_test(); +} diff --git a/toolkit/components/satchel/test/unit/test_history_sources.js b/toolkit/components/satchel/test/unit/test_history_sources.js new file mode 100644 index 0000000000..a598a457be --- /dev/null +++ b/toolkit/components/satchel/test/unit/test_history_sources.js @@ -0,0 +1,88 @@ +/* Any copyright is dedicated to the Public Domain. + * https://creativecommons.org/publicdomain/zero/1.0/ */ + +// Tests source usage in the form history API. + +add_task(async function () { + // Shorthands to improve test readability. + const count = FormHistoryTestUtils.count.bind(FormHistoryTestUtils); + async function search(fieldname, filters) { + let results1 = (await FormHistoryTestUtils.search(fieldname, filters)).map( + f => f.value + ); + // Check autocomplete returns the same value. + let results2 = ( + await FormHistoryTestUtils.autocomplete("va", fieldname, filters) + ).map(f => f.text); + Assert.deepEqual(results1, results2); + return results1; + } + + info("Sanity checks"); + Assert.equal(await count("field-A"), 0); + Assert.equal(await count("field-B"), 0); + await FormHistoryTestUtils.add("field-A", [{ value: "value-A" }]); + Assert.equal(await FormHistoryTestUtils.count("field-A"), 1); + Assert.deepEqual(await search("field-A"), ["value-A"]); + await FormHistoryTestUtils.remove("field-A", [{ value: "value-A" }]); + Assert.equal(await count("field-A"), 0); + + info("Test source for field-A"); + await FormHistoryTestUtils.add("field-A", [ + { value: "value-A", source: "test" }, + ]); + Assert.equal(await count("field-A"), 1); + Assert.deepEqual(await search("field-A"), ["value-A"]); + Assert.equal(await count("field-A", { source: "test" }), 1); + Assert.deepEqual(await search("field-A", { source: "test" }), ["value-A"]); + Assert.equal(await count("field-A", { source: "test2" }), 0); + Assert.deepEqual(await search("field-A", { source: "test2" }), []); + + info("Test source for field-B"); + await FormHistoryTestUtils.add("field-B", [ + { value: "value-B", source: "test" }, + ]); + Assert.equal(await count("field-B", { source: "test" }), 1); + Assert.equal(await count("field-B", { source: "test2" }), 0); + + info("Remove source"); + await FormHistoryTestUtils.add("field-B", [ + { value: "value-B", source: "test2" }, + ]); + Assert.equal(await count("field-B", { source: "test2" }), 1); + Assert.deepEqual(await search("field-B", { source: "test2" }), ["value-B"]); + await FormHistoryTestUtils.remove("field-B", [{ source: "test2" }]); + Assert.equal(await count("field-B", { source: "test2" }), 0); + Assert.deepEqual(await search("field-B", { source: "test2" }), []); + Assert.equal(await count("field-A"), 1); + Assert.deepEqual(await search("field-A"), ["value-A"]); + Assert.deepEqual(await search("field-A", { source: "test" }), ["value-A"]); + info("The other source should be untouched"); + Assert.equal(await count("field-B", { source: "test" }), 1); + Assert.deepEqual(await search("field-B", { source: "test" }), ["value-B"]); + Assert.equal(await count("field-B"), 1); + Assert.deepEqual(await search("field-B"), ["value-B"]); + + info("Clear field-A"); + await FormHistoryTestUtils.clear("field-A"); + Assert.equal(await count("field-A", { source: "test" }), 0); + Assert.equal(await count("field-A", { source: "test2" }), 0); + Assert.equal(await count("field-A"), 0); + Assert.equal(await count("field-B", { source: "test" }), 1); + Assert.equal(await count("field-B", { source: "test2" }), 0); + Assert.equal(await count("field-B"), 1); + + info("Clear All"); + await FormHistoryTestUtils.clear(); + Assert.equal(await count("field-B", { source: "test" }), 0); + Assert.equal(await count("field-B"), 0); + Assert.deepEqual(await search("field-A"), []); + Assert.deepEqual(await search("field-A", { source: "test" }), []); + Assert.deepEqual(await search("field-B"), []); + Assert.deepEqual(await search("field-B", { source: "test" }), []); + + info("Check there's no orphan sources"); + let db = await FormHistory.db; + let rows = await db.execute(`SELECT count(*) FROM moz_sources`); + Assert.equal(rows[0].getResultByIndex(0), 0, "There should be no orphans"); +}); diff --git a/toolkit/components/satchel/test/unit/test_notify.js b/toolkit/components/satchel/test/unit/test_notify.js new file mode 100644 index 0000000000..94be56f3f5 --- /dev/null +++ b/toolkit/components/satchel/test/unit/test_notify.js @@ -0,0 +1,171 @@ +/* + * Test suite for satchel notifications + * + * Tests notifications dispatched when modifying form history. + * + */ + +ChromeUtils.defineESModuleGetters(this, { + Preferences: "resource://gre/modules/Preferences.sys.mjs", + setTimeout: "resource://gre/modules/Timer.sys.mjs", +}); + +const TestObserver = { + observed: [], + QueryInterface: ChromeUtils.generateQI([ + "nsIObserver", + "nsISupportsWeakReference", + ]), + observe(subject, topic, data) { + if (subject instanceof Ci.nsISupportsString) { + subject = subject.toString(); + } + this.observed.push({ subject, topic, data }); + }, + reset() { + this.observed = []; + }, +}; + +const entry1 = ["entry1", "value1"]; +const entry2 = ["entry2", "value2"]; +const entry3 = ["entry3", "value3"]; + +add_setup(async () => { + await promiseUpdateEntry("remove", null, null); + const count = await promiseCountEntries(null, null); + Assert.ok(!count, "Checking initial DB is empty"); + + // Add the observer + Services.obs.addObserver(TestObserver, "satchel-storage-changed"); +}); + +add_task(async function addAndUpdateEntry() { + // Add + await promiseUpdateEntry("add", entry1[0], entry1[1]); + Assert.equal(TestObserver.observed.length, 1); + let { subject, data } = TestObserver.observed[0]; + Assert.equal(data, "formhistory-add"); + Assert.ok(isGUID.test(subject)); + + let count = await promiseCountEntries(entry1[0], entry1[1]); + Assert.equal(count, 1); + + // Update + TestObserver.reset(); + + await promiseUpdateEntry("update", entry1[0], entry1[1]); + Assert.equal(TestObserver.observed.length, 1); + ({ subject, data } = TestObserver.observed[0]); + Assert.equal(data, "formhistory-update"); + Assert.ok(isGUID.test(subject)); + + count = await promiseCountEntries(entry1[0], entry1[1]); + Assert.equal(count, 1); + + // Clean-up + await promiseUpdateEntry("remove", null, null); +}); + +add_task(async function removeEntry() { + TestObserver.reset(); + await promiseUpdateEntry("add", entry1[0], entry1[1]); + const guid = TestObserver.observed[0].subject; + TestObserver.reset(); + + await FormHistory.update({ + op: "remove", + fieldname: entry1[0], + value: entry1[1], + guid, + }); + Assert.equal(TestObserver.observed.length, 1); + const { subject, data } = TestObserver.observed[0]; + Assert.equal(data, "formhistory-remove"); + Assert.ok(isGUID.test(subject)); + + const count = await promiseCountEntries(entry1[0], entry1[1]); + Assert.equal(count, 0, "doesn't exist after remove"); +}); + +add_task(async function removeAllEntries() { + await promiseAddEntry(entry1[0], entry1[1]); + await promiseAddEntry(entry2[0], entry2[1]); + await promiseAddEntry(entry3[0], entry3[1]); + TestObserver.reset(); + + await promiseUpdateEntry("remove", null, null); + Assert.equal(TestObserver.observed.length, 3); + for (const notification of TestObserver.observed) { + const { subject, data } = notification; + Assert.equal(data, "formhistory-remove"); + Assert.ok(isGUID.test(subject)); + } + + const count = await promiseCountEntries(null, null); + Assert.equal(count, 0); +}); + +add_task(async function removeEntriesForName() { + await promiseAddEntry(entry1[0], entry1[1]); + await promiseAddEntry(entry2[0], entry2[1]); + await promiseAddEntry(entry3[0], entry3[1]); + TestObserver.reset(); + + await promiseUpdateEntry("remove", entry2[0], null); + Assert.equal(TestObserver.observed.length, 1); + const { subject, data } = TestObserver.observed[0]; + Assert.equal(data, "formhistory-remove"); + Assert.ok(isGUID.test(subject)); + + let count = await promiseCountEntries(entry2[0], entry2[1]); + Assert.equal(count, 0); + + count = await promiseCountEntries(null, null); + Assert.equal(count, 2, "the other entries are still there"); + + // Clean-up + await promiseUpdateEntry("remove", null, null); +}); + +add_task(async function removeEntriesByTimeframe() { + let timerPrecision = Preferences.get("privacy.reduceTimerPrecision"); + Preferences.set("privacy.reduceTimerPrecision", false); + + registerCleanupFunction(function () { + Preferences.set("privacy.reduceTimerPrecision", timerPrecision); + }); + + await promiseAddEntry(entry1[0], entry1[1]); + await promiseAddEntry(entry2[0], entry2[1]); + + const cutoffDate = Date.now(); + // eslint-disable-next-line mozilla/no-arbitrary-setTimeout + await new Promise(res => setTimeout(res, 10)); + + await promiseAddEntry(entry3[0], entry3[1]); + TestObserver.reset(); + + await FormHistory.update({ + op: "remove", + firstUsedStart: 10, + firstUsedEnd: cutoffDate * 1000, + }); + Assert.equal(TestObserver.observed.length, 2); + for (const notification of TestObserver.observed) { + const { subject, data } = notification; + Assert.equal(data, "formhistory-remove"); + Assert.ok(isGUID.test(subject)); + } + + const count = await promiseCountEntries(null, null); + Assert.equal(count, 1, "entry2 should still be there"); + + // Clean-up + await promiseUpdateEntry("remove", null, null); +}); + +add_task(async function teardown() { + await promiseUpdateEntry("remove", null, null); + Services.obs.removeObserver(TestObserver, "satchel-storage-changed"); +}); diff --git a/toolkit/components/satchel/test/unit/test_previous_result.js b/toolkit/components/satchel/test/unit/test_previous_result.js new file mode 100644 index 0000000000..a782832db7 --- /dev/null +++ b/toolkit/components/satchel/test/unit/test_previous_result.js @@ -0,0 +1,25 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +var aaaListener = { + onSearchResult(search, result) { + Assert.equal(result.searchString, "aaa"); + do_test_finished(); + }, +}; + +var aaListener = { + onSearchResult(search, result) { + Assert.equal(result.searchString, "aa"); + search.startSearch("aaa", "", result, aaaListener); + }, +}; + +function run_test() { + do_test_pending(); + let search = Cc[ + "@mozilla.org/autocomplete/search;1?name=form-history" + ].getService(Ci.nsIAutoCompleteSearch); + search.startSearch("aa", "", null, aaListener); +} diff --git a/toolkit/components/satchel/test/unit/xpcshell.toml b/toolkit/components/satchel/test/unit/xpcshell.toml new file mode 100644 index 0000000000..68a379e74d --- /dev/null +++ b/toolkit/components/satchel/test/unit/xpcshell.toml @@ -0,0 +1,43 @@ +[DEFAULT] +head = "head_satchel.js" +tags = "condprof" +skip-if = ["os == 'android'"] +support-files = [ + "asyncformhistory_expire.sqlite", + "formhistory_1000.sqlite", + "formhistory_CORRUPT.sqlite", + "formhistory_apitest.sqlite", + "formhistory_autocomplete.sqlite", + "formhistory_v3.sqlite", + "formhistory_v3v4.sqlite", + "formhistory_v999a.sqlite", + "formhistory_v999b.sqlite", +] + +["test_async_expire.js"] + +["test_autocomplete.js"] + +["test_db_access_denied.js"] +skip-if = ["os != 'linux'"] # simulates insufficiant file permissions + +["test_db_corrupt.js"] + +["test_db_update_v4.js"] + +["test_db_update_v4b.js"] + +["test_db_update_v5.js"] +skip-if = ["condprof"] # Bug 1769154 - not supported + +["test_db_update_v999a.js"] + +["test_db_update_v999b.js"] + +["test_history_api.js"] + +["test_history_sources.js"] + +["test_notify.js"] + +["test_previous_result.js"] |