summaryrefslogtreecommitdiffstats
path: root/toolkit/components/satchel/test/unit
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/components/satchel/test/unit')
-rw-r--r--toolkit/components/satchel/test/unit/asyncformhistory_expire.sqlitebin0 -> 98304 bytes
-rw-r--r--toolkit/components/satchel/test/unit/formhistory_1000.sqlitebin0 -> 164864 bytes
-rw-r--r--toolkit/components/satchel/test/unit/formhistory_CORRUPT.sqlite1
-rw-r--r--toolkit/components/satchel/test/unit/formhistory_apitest.sqlitebin0 -> 5120 bytes
-rw-r--r--toolkit/components/satchel/test/unit/formhistory_autocomplete.sqlitebin0 -> 72704 bytes
-rw-r--r--toolkit/components/satchel/test/unit/formhistory_v3.sqlitebin0 -> 5120 bytes
-rw-r--r--toolkit/components/satchel/test/unit/formhistory_v3v4.sqlitebin0 -> 6144 bytes
-rw-r--r--toolkit/components/satchel/test/unit/formhistory_v999a.sqlitebin0 -> 11264 bytes
-rw-r--r--toolkit/components/satchel/test/unit/formhistory_v999b.sqlitebin0 -> 8192 bytes
-rw-r--r--toolkit/components/satchel/test/unit/head_satchel.js188
-rw-r--r--toolkit/components/satchel/test/unit/test_async_expire.js134
-rw-r--r--toolkit/components/satchel/test/unit/test_autocomplete.js388
-rw-r--r--toolkit/components/satchel/test/unit/test_db_access_denied.js52
-rw-r--r--toolkit/components/satchel/test/unit/test_db_corrupt.js80
-rw-r--r--toolkit/components/satchel/test/unit/test_db_update_v4.js59
-rw-r--r--toolkit/components/satchel/test/unit/test_db_update_v4b.js49
-rw-r--r--toolkit/components/satchel/test/unit/test_db_update_v5.js29
-rw-r--r--toolkit/components/satchel/test/unit/test_db_update_v999a.js56
-rw-r--r--toolkit/components/satchel/test/unit/test_db_update_v999b.js74
-rw-r--r--toolkit/components/satchel/test/unit/test_history_api.js485
-rw-r--r--toolkit/components/satchel/test/unit/test_history_sources.js88
-rw-r--r--toolkit/components/satchel/test/unit/test_notify.js171
-rw-r--r--toolkit/components/satchel/test/unit/test_previous_result.js25
-rw-r--r--toolkit/components/satchel/test/unit/xpcshell.toml43
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
new file mode 100644
index 0000000000..07b43c2096
--- /dev/null
+++ b/toolkit/components/satchel/test/unit/asyncformhistory_expire.sqlite
Binary files differ
diff --git a/toolkit/components/satchel/test/unit/formhistory_1000.sqlite b/toolkit/components/satchel/test/unit/formhistory_1000.sqlite
new file mode 100644
index 0000000000..5eeab074fd
--- /dev/null
+++ b/toolkit/components/satchel/test/unit/formhistory_1000.sqlite
Binary files differ
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
new file mode 100644
index 0000000000..00daf03c27
--- /dev/null
+++ b/toolkit/components/satchel/test/unit/formhistory_apitest.sqlite
Binary files differ
diff --git a/toolkit/components/satchel/test/unit/formhistory_autocomplete.sqlite b/toolkit/components/satchel/test/unit/formhistory_autocomplete.sqlite
new file mode 100644
index 0000000000..724cff73f6
--- /dev/null
+++ b/toolkit/components/satchel/test/unit/formhistory_autocomplete.sqlite
Binary files differ
diff --git a/toolkit/components/satchel/test/unit/formhistory_v3.sqlite b/toolkit/components/satchel/test/unit/formhistory_v3.sqlite
new file mode 100644
index 0000000000..e0e8fe2468
--- /dev/null
+++ b/toolkit/components/satchel/test/unit/formhistory_v3.sqlite
Binary files differ
diff --git a/toolkit/components/satchel/test/unit/formhistory_v3v4.sqlite b/toolkit/components/satchel/test/unit/formhistory_v3v4.sqlite
new file mode 100644
index 0000000000..8eab177e97
--- /dev/null
+++ b/toolkit/components/satchel/test/unit/formhistory_v3v4.sqlite
Binary files differ
diff --git a/toolkit/components/satchel/test/unit/formhistory_v999a.sqlite b/toolkit/components/satchel/test/unit/formhistory_v999a.sqlite
new file mode 100644
index 0000000000..216bce4a34
--- /dev/null
+++ b/toolkit/components/satchel/test/unit/formhistory_v999a.sqlite
Binary files differ
diff --git a/toolkit/components/satchel/test/unit/formhistory_v999b.sqlite b/toolkit/components/satchel/test/unit/formhistory_v999b.sqlite
new file mode 100644
index 0000000000..fe400d04a1
--- /dev/null
+++ b/toolkit/components/satchel/test/unit/formhistory_v999b.sqlite
Binary files differ
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"]