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/.eslintrc.js7
-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 -> 6144 bytes
-rw-r--r--toolkit/components/satchel/test/unit/formhistory_v999b.sqlitebin0 -> 4096 bytes
-rw-r--r--toolkit/components/satchel/test/unit/head_satchel.js102
-rw-r--r--toolkit/components/satchel/test/unit/perf_autocomplete.js140
-rw-r--r--toolkit/components/satchel/test/unit/test_async_expire.js168
-rw-r--r--toolkit/components/satchel/test/unit/test_autocomplete.js266
-rw-r--r--toolkit/components/satchel/test/unit/test_db_corrupt.js89
-rw-r--r--toolkit/components/satchel/test/unit/test_db_update_v4.js60
-rw-r--r--toolkit/components/satchel/test/unit/test_db_update_v4b.js58
-rw-r--r--toolkit/components/satchel/test/unit/test_db_update_v999a.js75
-rw-r--r--toolkit/components/satchel/test/unit/test_db_update_v999b.js92
-rw-r--r--toolkit/components/satchel/test/unit/test_history_api.js457
-rw-r--r--toolkit/components/satchel/test/unit/test_notify.js158
-rw-r--r--toolkit/components/satchel/test/unit/test_previous_result.js25
-rw-r--r--toolkit/components/satchel/test/unit/xpcshell.ini26
23 files changed, 1724 insertions, 0 deletions
diff --git a/toolkit/components/satchel/test/unit/.eslintrc.js b/toolkit/components/satchel/test/unit/.eslintrc.js
new file mode 100644
index 000000000..d35787cd2
--- /dev/null
+++ b/toolkit/components/satchel/test/unit/.eslintrc.js
@@ -0,0 +1,7 @@
+"use strict";
+
+module.exports = {
+ "extends": [
+ "../../../../../testing/xpcshell/xpcshell.eslintrc.js"
+ ]
+};
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 000000000..07b43c209
--- /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 000000000..5eeab074f
--- /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 000000000..5f7498bfc
--- /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 000000000..00daf03c2
--- /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 000000000..724cff73f
--- /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 000000000..e0e8fe246
--- /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 000000000..8eab177e9
--- /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 000000000..14279f05f
--- /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 000000000..21d9c1f1c
--- /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 000000000..282d07ba5
--- /dev/null
+++ b/toolkit/components/satchel/test/unit/head_satchel.js
@@ -0,0 +1,102 @@
+/* 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/. */
+
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+Components.utils.import("resource://gre/modules/Services.jsm");
+Components.utils.import("resource://gre/modules/FormHistory.jsm");
+
+var Ci = Components.interfaces;
+var Cc = Components.classes;
+var Cu = Components.utils;
+
+const CURRENT_SCHEMA = 4;
+const PR_HOURS = 60 * 60 * 1000000;
+
+do_get_profile();
+
+var dirSvc = Cc["@mozilla.org/file/directory_service;1"].
+ getService(Ci.nsIProperties);
+
+// 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);
+
+function getDBVersion(dbfile) {
+ var ss = Cc["@mozilla.org/storage/service;1"].
+ getService(Ci.mozIStorageService);
+ var dbConnection = ss.openDatabase(dbfile);
+ var version = dbConnection.schemaVersion;
+ dbConnection.close();
+
+ return version;
+}
+
+const isGUID = /[A-Za-z0-9\+\/]{16}/;
+
+// Find form history entries.
+function searchEntries(terms, params, iter) {
+ let results = [];
+ FormHistory.search(terms, params, { handleResult: result => results.push(result),
+ handleError: function (error) {
+ do_throw("Error occurred searching form history: " + error);
+ },
+ handleCompletion: function (reason) { if (!reason) iter.next(results); }
+ });
+}
+
+// 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) {
+ var obj = {};
+ if (name !== null)
+ obj.fieldname = name;
+ if (value !== null)
+ obj.value = value;
+
+ let count = 0;
+ FormHistory.count(obj, { handleResult: result => count = result,
+ handleError: function (error) {
+ do_throw("Error occurred searching form history: " + error);
+ },
+ handleCompletion: function (reason) { if (!reason) then(count); }
+ });
+}
+
+// Perform a single form history update and call then() when done.
+function updateEntry(op, name, value, then) {
+ var obj = { op: 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: value, timesUsed: 1,
+ firstUsed: now, lastUsed: now }, then);
+}
+
+// Wrapper around FormHistory.update which handles errors. Calls then() when done.
+function updateFormHistory(changes, then) {
+ FormHistory.update(changes, { handleError: function (error) {
+ do_throw("Error occurred updating form history: " + error);
+ },
+ handleCompletion: function (reason) { if (!reason) then(); },
+ });
+}
+
+/**
+ * Logs info to the console in the standard way (includes the filename).
+ *
+ * @param aMessage
+ * The message to log to the console.
+ */
+function do_log_info(aMessage) {
+ print("TEST-INFO | " + _TEST_FILE + " | " + aMessage);
+}
diff --git a/toolkit/components/satchel/test/unit/perf_autocomplete.js b/toolkit/components/satchel/test/unit/perf_autocomplete.js
new file mode 100644
index 000000000..6e8bb5125
--- /dev/null
+++ b/toolkit/components/satchel/test/unit/perf_autocomplete.js
@@ -0,0 +1,140 @@
+/* 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 fh;
+var fac;
+var prefs;
+
+function countAllEntries() {
+ let stmt = fh.DBConnection.createStatement("SELECT COUNT(*) as numEntries FROM moz_formhistory");
+ do_check_true(stmt.executeStep());
+ let numEntries = stmt.row.numEntries;
+ stmt.finalize();
+ return numEntries;
+}
+
+function do_AC_search(searchTerm, previousResult) {
+ var duration = 0;
+ var searchCount = 5;
+ var tempPrevious = null;
+ var startTime;
+ for (var i = 0; i < searchCount; i++) {
+ if (previousResult !== null)
+ tempPrevious = fac.autoCompleteSearch("searchbar-history", previousResult, null, null);
+ startTime = Date.now();
+ results = fac.autoCompleteSearch("searchbar-history", searchTerm, null, tempPrevious);
+ duration += Date.now() - startTime;
+ }
+ dump("[autoCompleteSearch][test " + testnum + "] for '" + searchTerm + "' ");
+ if (previousResult !== null)
+ dump("with '" + previousResult + "' previous result ");
+ else
+ dump("w/o previous result ");
+ dump("took " + duration + " ms with " + results.matchCount + " matches. ");
+ dump("Average of " + Math.round(duration / searchCount) + " ms per search\n");
+ return results;
+}
+
+function run_test() {
+ try {
+
+ // ===== test init =====
+ var testfile = do_get_file("formhistory_1000.sqlite");
+ var profileDir = dirSvc.get("ProfD", Ci.nsIFile);
+ var results;
+
+ // Cleanup from any previous tests or failures.
+ var destFile = profileDir.clone();
+ destFile.append("formhistory.sqlite");
+ if (destFile.exists())
+ destFile.remove(false);
+
+ testfile.copyTo(profileDir, "formhistory.sqlite");
+
+ fh = Cc["@mozilla.org/satchel/form-history;1"].
+ getService(Ci.nsIFormHistory2);
+ fac = Cc["@mozilla.org/satchel/form-autocomplete;1"].
+ getService(Ci.nsIFormAutoComplete);
+ prefs = Cc["@mozilla.org/preferences-service;1"].
+ getService(Ci.nsIPrefBranch);
+
+ timeGroupingSize = prefs.getIntPref("browser.formfill.timeGroupingSize") * 1000 * 1000;
+ maxTimeGroupings = prefs.getIntPref("browser.formfill.maxTimeGroupings");
+ bucketSize = prefs.getIntPref("browser.formfill.bucketSize");
+
+ // ===== 1 =====
+ // Check initial state is as expected
+ testnum++;
+ do_check_true(fh.hasEntries);
+ do_check_eq(1000, countAllEntries());
+ fac.autoCompleteSearch("searchbar-history", "zzzzzzzzzz", null, null); // warm-up search
+ do_check_true(fh.nameExists("searchbar-history"));
+
+ // ===== 2 =====
+ // Search for '' with no previous result
+ testnum++;
+ results = do_AC_search("", null);
+ do_check_true(results.matchCount > 0);
+
+ // ===== 3 =====
+ // Search for 'r' with no previous result
+ testnum++;
+ results = do_AC_search("r", null);
+ do_check_true(results.matchCount > 0);
+
+ // ===== 4 =====
+ // Search for 'r' with '' previous result
+ testnum++;
+ results = do_AC_search("r", "");
+ do_check_true(results.matchCount > 0);
+
+ // ===== 5 =====
+ // Search for 're' with no previous result
+ testnum++;
+ results = do_AC_search("re", null);
+ do_check_true(results.matchCount > 0);
+
+ // ===== 6 =====
+ // Search for 're' with 'r' previous result
+ testnum++;
+ results = do_AC_search("re", "r");
+ do_check_true(results.matchCount > 0);
+
+ // ===== 7 =====
+ // Search for 'rea' without previous result
+ testnum++;
+ results = do_AC_search("rea", null);
+ let countREA = results.matchCount;
+
+ // ===== 8 =====
+ // Search for 'rea' with 're' previous result
+ testnum++;
+ results = do_AC_search("rea", "re");
+ do_check_eq(countREA, results.matchCount);
+
+ // ===== 9 =====
+ // Search for 'real' with 'rea' previous result
+ testnum++;
+ results = do_AC_search("real", "rea");
+ let countREAL = results.matchCount;
+ do_check_true(results.matchCount <= countREA);
+
+ // ===== 10 =====
+ // Search for 'real' with 're' previous result
+ testnum++;
+ results = do_AC_search("real", "re");
+ do_check_eq(countREAL, results.matchCount);
+
+ // ===== 11 =====
+ // Search for 'real' with no previous result
+ testnum++;
+ results = do_AC_search("real", null);
+ do_check_eq(countREAL, results.matchCount);
+
+
+ } catch (e) {
+ throw "FAILED in test #" + testnum + " -- " + e;
+ }
+}
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 000000000..501cbdfe5
--- /dev/null
+++ b/toolkit/components/satchel/test/unit/test_async_expire.js
@@ -0,0 +1,168 @@
+/* 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 dbFile, oldSize;
+var currentTestIndex = 0;
+
+function triggerExpiration() {
+ // 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", null);
+}
+
+var checkExists = function(num) { do_check_true(num > 0); next_test(); }
+var checkNotExists = function(num) { do_check_true(!num); next_test(); }
+
+var TestObserver = {
+ QueryInterface : XPCOMUtils.generateQI([Ci.nsIObserver, Ci.nsISupportsWeakReference]),
+
+ observe : function (subject, topic, data) {
+ do_check_eq(topic, "satchel-storage-changed");
+
+ if (data == "formhistory-expireoldentries") {
+ next_test();
+ }
+ }
+};
+
+function test_finished() {
+ // Make sure we always reset prefs.
+ if (Services.prefs.prefHasUserValue("browser.formfill.expire_days"))
+ Services.prefs.clearUserPref("browser.formfill.expire_days");
+
+ do_test_finished();
+}
+
+var iter = tests();
+
+function run_test()
+{
+ do_test_pending();
+ iter.next();
+}
+
+function next_test()
+{
+ iter.next();
+}
+
+function* tests()
+{
+ Services.obs.addObserver(TestObserver, "satchel-storage-changed", true);
+
+ // ===== test init =====
+ var testfile = do_get_file("asyncformhistory_expire.sqlite");
+ var profileDir = do_get_profile();
+
+ // Cleanup from any previous tests or failures.
+ dbFile = profileDir.clone();
+ dbFile.append("formhistory.sqlite");
+ if (dbFile.exists())
+ dbFile.remove(false);
+
+ testfile.copyTo(profileDir, "formhistory.sqlite");
+ do_check_true(dbFile.exists());
+
+ // We're going to clear this at the end, so it better have the default value now.
+ do_check_false(Services.prefs.prefHasUserValue("browser.formfill.expire_days"));
+
+ // Sanity check initial state
+ yield countEntries(null, null, function(num) { do_check_eq(508, num); next_test(); });
+ yield countEntries("name-A", "value-A", checkExists); // lastUsed == distant past
+ yield countEntries("name-B", "value-B", checkExists); // lastUsed == distant future
+
+ do_check_eq(CURRENT_SCHEMA, FormHistory.schemaVersion);
+
+ // Add a new entry
+ yield countEntries("name-C", "value-C", checkNotExists);
+ yield addEntry("name-C", "value-C", next_test);
+ yield countEntries("name-C", "value-C", checkExists);
+
+ // Update some existing entries to have ages relative to when the test runs.
+ var now = 1000 * Date.now();
+ let updateLastUsed = function updateLastUsedFn(results, age)
+ {
+ let lastUsed = now - age * 24 * PR_HOURS;
+
+ let changes = [ ];
+ for (let r = 0; r < results.length; r++) {
+ changes.push({ op: "update", lastUsed: lastUsed, guid: results[r].guid });
+ }
+
+ return changes;
+ }
+
+ let results = yield searchEntries(["guid"], { lastUsed: 181 }, iter);
+ yield updateFormHistory(updateLastUsed(results, 181), next_test);
+
+ results = yield searchEntries(["guid"], { lastUsed: 179 }, iter);
+ yield updateFormHistory(updateLastUsed(results, 179), next_test);
+
+ results = yield searchEntries(["guid"], { lastUsed: 31 }, iter);
+ yield updateFormHistory(updateLastUsed(results, 31), next_test);
+
+ results = yield searchEntries(["guid"], { lastUsed: 29 }, iter);
+ yield updateFormHistory(updateLastUsed(results, 29), next_test);
+
+ results = yield searchEntries(["guid"], { lastUsed: 9999 }, iter);
+ yield updateFormHistory(updateLastUsed(results, 11), next_test);
+
+ results = yield searchEntries(["guid"], { lastUsed: 9 }, iter);
+ yield updateFormHistory(updateLastUsed(results, 9), next_test);
+
+ yield countEntries("name-A", "value-A", checkExists);
+ yield countEntries("181DaysOld", "foo", checkExists);
+ yield countEntries("179DaysOld", "foo", checkExists);
+ yield countEntries(null, null, function(num) { do_check_eq(509, num); next_test(); });
+
+ // 2 entries are expected to expire.
+ triggerExpiration();
+ yield;
+
+ yield countEntries("name-A", "value-A", checkNotExists);
+ yield countEntries("181DaysOld", "foo", checkNotExists);
+ yield countEntries("179DaysOld", "foo", checkExists);
+ yield countEntries(null, null, function(num) { do_check_eq(507, num); next_test(); });
+
+ // And again. No change expected.
+ triggerExpiration();
+ yield;
+
+ yield countEntries(null, null, function(num) { do_check_eq(507, num); next_test(); });
+
+ // Set formfill pref to 30 days.
+ Services.prefs.setIntPref("browser.formfill.expire_days", 30);
+ yield countEntries("179DaysOld", "foo", checkExists);
+ yield countEntries("bar", "31days", checkExists);
+ yield countEntries("bar", "29days", checkExists);
+ yield countEntries(null, null, function(num) { do_check_eq(507, num); next_test(); });
+
+ triggerExpiration();
+ yield;
+
+ yield countEntries("179DaysOld", "foo", checkNotExists);
+ yield countEntries("bar", "31days", checkNotExists);
+ yield countEntries("bar", "29days", checkExists);
+ yield countEntries(null, null, function(num) { do_check_eq(505, num); next_test(); });
+
+ // 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);
+
+ yield countEntries("bar", "29days", checkExists);
+ yield countEntries("9DaysOld", "foo", checkExists);
+ yield countEntries(null, null, function(num) { do_check_eq(505, num); next_test(); });
+
+ triggerExpiration();
+ yield;
+
+ yield countEntries("bar", "29days", checkNotExists);
+ yield countEntries("9DaysOld", "foo", checkExists);
+ yield countEntries("name-B", "value-B", checkExists);
+ yield countEntries("name-C", "value-C", checkExists);
+ yield countEntries(null, null, function(num) { do_check_eq(3, num); next_test(); });
+
+ test_finished();
+}
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 000000000..211753809
--- /dev/null
+++ b/toolkit/components/satchel/test/unit/test_autocomplete.js
@@ -0,0 +1,266 @@
+/* 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 testnum = 0;
+var fac;
+var prefs;
+
+var numRecords, timeGroupingSize, now;
+
+const DEFAULT_EXPIRE_DAYS = 180;
+
+function padLeft(number, length) {
+ var str = number + '';
+ while (str.length < length)
+ str = '0' + str;
+ return str;
+}
+
+function getFormExpiryDays() {
+ if (prefs.prefHasUserValue("browser.formfill.expire_days")) {
+ return prefs.getIntPref("browser.formfill.expire_days");
+ }
+ return DEFAULT_EXPIRE_DAYS;
+}
+
+function run_test() {
+ // ===== test init =====
+ var testfile = do_get_file("formhistory_autocomplete.sqlite");
+ var profileDir = dirSvc.get("ProfD", Ci.nsIFile);
+
+ // Cleanup from any previous tests or failures.
+ var 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);
+ prefs = Cc["@mozilla.org/preferences-service;1"].
+ getService(Ci.nsIPrefBranch);
+
+ timeGroupingSize = prefs.getIntPref("browser.formfill.timeGroupingSize") * 1000 * 1000;
+
+ run_next_test();
+}
+
+add_test(function test0() {
+ var maxTimeGroupings = prefs.getIntPref("browser.formfill.maxTimeGroupings");
+ var bucketSize = 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) {
+ do_check_true(count > 0);
+ run_next_test();
+ });
+ });
+});
+
+add_test(function test2() {
+ do_log_info("Check search contains all entries");
+
+ fac.autoCompleteSearchAsync("field1", "", null, null, null, {
+ onSearchCompletion : function(aResults) {
+ do_check_eq(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, null, {
+ onSearchCompletion : function(aResults) {
+ for (let i = 0; i < numRecords; i+=2) {
+ do_check_eq(parseInt(aResults.getValueAt(i + 1).substr(5), 10), --lastFound);
+ do_check_eq(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, null, {
+ onSearchCompletion : function(aResults) {
+ for (let i = 0; i < numRecords; i+=2) {
+ do_check_eq(parseInt(aResults.getValueAt(i + 1).substr(5), 10), --lastFound);
+ do_check_eq(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, null, {
+ onSearchCompletion : function(aResults) {
+ for (let i = 0; i < timesUsedSamples; i++) {
+ do_check_eq(parseInt(aResults.getValueAt(i).substr(5)), --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, null, {
+ onSearchCompletion : function(aResults) {
+ for (let i = 0; i < timesUsedSamples; i++) {
+ do_check_eq(parseInt(aResults.getValueAt(i).substr(5)), --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, null, {
+ onSearchCompletion : function(aResults) {
+ do_check_eq(aResults.getValueAt(0), "senior citizen");
+ do_check_eq(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, null, {
+ onSearchCompletion : function(aResults) {
+ do_check_eq(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: 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, null, {
+ onSearchCompletion : function(aResults) {
+ do_check_eq(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, null, {
+ onSearchCompletion : function(aResults) {
+ do_check_eq(aResults.matchCount, 1,
+ "Only the first MAX_SEARCH_TOKENS tokens " +
+ "should be used for DB queries");
+ test_token_limit_previousResult(aResults);
+ }
+ });
+ });
+});
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 000000000..a6fdc4c02
--- /dev/null
+++ b/toolkit/components/satchel/test/unit/test_db_corrupt.js
@@ -0,0 +1,89 @@
+/* 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 = 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.
+ do_check_false(bakFile.exists());
+ // Doing any request to the DB should create it.
+ countEntries(null, null, run_next_test);
+});
+
+add_test(function test_corruptFormHistoryDB_lazyCorruptInit2() {
+ do_check_true(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.");
+
+ FormHistory.count({}, {
+ handleResult : function(aNumEntries) {
+ do_check_true(aNumEntries == 0);
+ FormHistory.count({ fieldname : "name-A", value : "value-A" }, {
+ handleResult : function(aNumEntries2) {
+ do_check_true(aNumEntries2 == 0);
+ run_next_test();
+ },
+ handleError : function(aError2) {
+ do_throw("DB initialized after reading a corrupt DB file found an entry.");
+ }
+ });
+ },
+ handleError : function (aError) {
+ 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) {
+ do_check_true(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) {
+ do_check_true(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 000000000..84b17e8a0
--- /dev/null
+++ b/toolkit/components/satchel/test/unit/test_db_update_v4.js
@@ -0,0 +1,60 @@
+/* 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 iter;
+
+function run_test()
+{
+ do_test_pending();
+ iter = next_test();
+ iter.next();
+}
+
+function* next_test()
+{
+ try {
+
+ // ===== test init =====
+ var testfile = do_get_file("formhistory_v3.sqlite");
+ var profileDir = dirSvc.get("ProfD", Ci.nsIFile);
+
+ // Cleanup from any previous tests or failures.
+ var destFile = profileDir.clone();
+ destFile.append("formhistory.sqlite");
+ if (destFile.exists())
+ destFile.remove(false);
+
+ testfile.copyTo(profileDir, "formhistory.sqlite");
+ do_check_eq(3, getDBVersion(testfile));
+
+ // ===== 1 =====
+ testnum++;
+
+ destFile = profileDir.clone();
+ destFile.append("formhistory.sqlite");
+ let dbConnection = Services.storage.openUnsharedDatabase(destFile);
+
+ // check for upgraded schema.
+ do_check_eq(CURRENT_SCHEMA, FormHistory.schemaVersion);
+
+ // Check that the index was added
+ do_check_true(dbConnection.tableExists("moz_deleted_formhistory"));
+ dbConnection.close();
+
+ // check for upgraded schema.
+ do_check_eq(CURRENT_SCHEMA, FormHistory.schemaVersion);
+ // check that an entry still exists
+ yield countEntries("name-A", "value-A",
+ function (num) {
+ do_check_true(num > 0);
+ do_test_finished();
+ }
+ );
+
+ } catch (e) {
+ throw "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 000000000..356d34a48
--- /dev/null
+++ b/toolkit/components/satchel/test/unit/test_db_update_v4b.js
@@ -0,0 +1,58 @@
+/* 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 iter;
+
+function run_test()
+{
+ do_test_pending();
+ iter = next_test();
+ iter.next();
+}
+
+function* next_test()
+{
+ try {
+
+ // ===== test init =====
+ var testfile = do_get_file("formhistory_v3v4.sqlite");
+ var profileDir = dirSvc.get("ProfD", Ci.nsIFile);
+
+ // Cleanup from any previous tests or failures.
+ var destFile = profileDir.clone();
+ destFile.append("formhistory.sqlite");
+ if (destFile.exists())
+ destFile.remove(false);
+
+ testfile.copyTo(profileDir, "formhistory.sqlite");
+ do_check_eq(3, getDBVersion(testfile));
+
+ // ===== 1 =====
+ testnum++;
+
+ destFile = profileDir.clone();
+ destFile.append("formhistory.sqlite");
+ dbConnection = Services.storage.openUnsharedDatabase(destFile);
+
+ // check for upgraded schema.
+ do_check_eq(CURRENT_SCHEMA, FormHistory.schemaVersion);
+
+ // Check that the index was added
+ do_check_true(dbConnection.tableExists("moz_deleted_formhistory"));
+ dbConnection.close();
+
+ // check that an entry still exists
+ yield countEntries("name-A", "value-A",
+ function (num) {
+ do_check_true(num > 0);
+ do_test_finished();
+ }
+ );
+
+ } catch (e) {
+ throw "FAILED in test #" + testnum + " -- " + e;
+ }
+}
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 000000000..0a44d06aa
--- /dev/null
+++ b/toolkit/components/satchel/test/unit/test_db_update_v999a.js
@@ -0,0 +1,75 @@
+/* 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.
+ */
+
+var iter = tests();
+
+function run_test()
+{
+ do_test_pending();
+ iter.next();
+}
+
+function next_test()
+{
+ iter.next();
+}
+
+function* tests()
+{
+ try {
+ var testnum = 0;
+
+ // ===== test init =====
+ var testfile = do_get_file("formhistory_v999a.sqlite");
+ var profileDir = dirSvc.get("ProfD", Ci.nsIFile);
+
+ // Cleanup from any previous tests or failures.
+ var destFile = profileDir.clone();
+ destFile.append("formhistory.sqlite");
+ if (destFile.exists())
+ destFile.remove(false);
+
+ testfile.copyTo(profileDir, "formhistory.sqlite");
+ do_check_eq(999, getDBVersion(testfile));
+
+ let checkZero = function(num) { do_check_eq(num, 0); next_test(); }
+ let checkOne = function(num) { do_check_eq(num, 1); next_test(); }
+
+ // ===== 1 =====
+ testnum++;
+ // Check for expected contents.
+ yield countEntries(null, null, function(num) { do_check_true(num > 0); next_test(); });
+ yield countEntries("name-A", "value-A", checkOne);
+ yield countEntries("name-B", "value-B", checkOne);
+ yield countEntries("name-C", "value-C1", checkOne);
+ yield countEntries("name-C", "value-C2", checkOne);
+ yield countEntries("name-E", "value-E", checkOne);
+
+ // check for downgraded schema.
+ do_check_eq(CURRENT_SCHEMA, FormHistory.schemaVersion);
+
+ // ===== 2 =====
+ testnum++;
+ // Exercise adding and removing a name/value pair
+ yield countEntries("name-D", "value-D", checkZero);
+ yield updateEntry("add", "name-D", "value-D", next_test);
+ yield countEntries("name-D", "value-D", checkOne);
+ yield updateEntry("remove", "name-D", "value-D", next_test);
+ yield countEntries("name-D", "value-D", checkZero);
+
+ } catch (e) {
+ throw "FAILED in test #" + testnum + " -- " + e;
+ }
+
+ do_test_finished();
+}
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 000000000..fb0ecd1b7
--- /dev/null
+++ b/toolkit/components/satchel/test/unit/test_db_update_v999b.js
@@ -0,0 +1,92 @@
+/* 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.
+ */
+
+var iter = tests();
+
+function run_test()
+{
+ do_test_pending();
+ iter.next();
+}
+
+function next_test()
+{
+ iter.next();
+}
+
+function* tests()
+{
+ try {
+ var testnum = 0;
+
+ // ===== test init =====
+ var testfile = do_get_file("formhistory_v999b.sqlite");
+ var profileDir = dirSvc.get("ProfD", Ci.nsIFile);
+
+ // Cleanup from any previous tests or failures.
+ var destFile = profileDir.clone();
+ destFile.append("formhistory.sqlite");
+ if (destFile.exists())
+ destFile.remove(false);
+
+ var bakFile = profileDir.clone();
+ bakFile.append("formhistory.sqlite.corrupt");
+ if (bakFile.exists())
+ bakFile.remove(false);
+
+ testfile.copyTo(profileDir, "formhistory.sqlite");
+ do_check_eq(999, getDBVersion(testfile));
+
+ let checkZero = function(num) { do_check_eq(num, 0); next_test(); }
+ let checkOne = function(num) { do_check_eq(num, 1); next_test(); }
+
+ // ===== 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.
+ do_check_false(bakFile.exists());
+ // Doing any request to the DB should create it.
+ yield countEntries("", "", next_test);
+
+ do_check_true(bakFile.exists());
+ bakFile.remove(false);
+
+ // ===== 2 =====
+ testnum++;
+ // File should be empty
+ yield countEntries(null, null, function(num) { do_check_false(num); next_test(); });
+ yield countEntries("name-A", "value-A", checkZero);
+ // check for current schema.
+ do_check_eq(CURRENT_SCHEMA, FormHistory.schemaVersion);
+
+ // ===== 3 =====
+ testnum++;
+ // Try adding an entry
+ yield updateEntry("add", "name-A", "value-A", next_test);
+ yield countEntries(null, null, checkOne);
+ yield countEntries("name-A", "value-A", checkOne);
+
+ // ===== 4 =====
+ testnum++;
+ // Try removing an entry
+ yield updateEntry("remove", "name-A", "value-A", next_test);
+ yield countEntries(null, null, checkZero);
+ yield countEntries("name-A", "value-A", checkZero);
+
+ } catch (e) {
+ throw "FAILED in test #" + testnum + " -- " + e;
+ }
+
+ do_test_finished();
+}
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 000000000..753504183
--- /dev/null
+++ b/toolkit/components/satchel/test/unit/test_history_api.js
@@ -0,0 +1,457 @@
+/* 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
+
+Cu.import("resource://gre/modules/Promise.jsm");
+
+function countDeletedEntries(expected)
+{
+ let deferred = Promise.defer();
+ let stmt = dbConnection.createAsyncStatement("SELECT COUNT(*) AS numEntries FROM moz_deleted_formhistory");
+ stmt.executeAsync({
+ handleResult: function(resultSet) {
+ do_check_eq(expected, resultSet.getNextRow().getResultByName("numEntries"));
+ deferred.resolve();
+ },
+ handleError : function () {
+ do_throw("Error occurred counting deleted entries: " + error);
+ deferred.reject();
+ },
+ handleCompletion : function () {
+ stmt.finalize();
+ }
+ });
+ return deferred.promise;
+}
+
+function checkTimeDeleted(guid, checkFunction)
+{
+ let deferred = Promise.defer();
+ let stmt = dbConnection.createAsyncStatement("SELECT timeDeleted FROM moz_deleted_formhistory WHERE guid = :guid");
+ stmt.params.guid = guid;
+ stmt.executeAsync({
+ handleResult: function(resultSet) {
+ checkFunction(resultSet.getNextRow().getResultByName("timeDeleted"));
+ deferred.resolve();
+ },
+ handleError : function () {
+ do_throw("Error occurred getting deleted entries: " + error);
+ deferred.reject();
+ },
+ handleCompletion : function () {
+ stmt.finalize();
+ }
+ });
+ return deferred.promise;
+}
+
+function promiseUpdateEntry(op, name, value)
+{
+ var change = { op: op };
+ if (name !== null)
+ change.fieldname = name;
+ if (value !== null)
+ change.value = value;
+ return promiseUpdate(change);
+}
+
+function promiseUpdate(change) {
+ return new Promise((resolve, reject) => {
+ FormHistory.update(change, {
+ handleError(error) {
+ this._error = error;
+ },
+ handleCompletion(reason) {
+ if (reason) {
+ reject(this._error);
+ } else {
+ resolve();
+ }
+ }
+ });
+ });
+}
+
+function promiseSearchEntries(terms, params)
+{
+ let deferred = Promise.defer();
+ let results = [];
+ FormHistory.search(terms, params,
+ { handleResult: result => results.push(result),
+ handleError: function (error) {
+ do_throw("Error occurred searching form history: " + error);
+ deferred.reject(error);
+ },
+ handleCompletion: function (reason) { if (!reason) deferred.resolve(results); }
+ });
+ return deferred.promise;
+}
+
+function promiseCountEntries(name, value, checkFn)
+{
+ let deferred = Promise.defer();
+ countEntries(name, value, function (result) { checkFn(result); deferred.resolve(); } );
+ return deferred.promise;
+}
+
+add_task(function* ()
+{
+ let oldSupportsDeletedTable = FormHistory._supportsDeletedTable;
+ FormHistory._supportsDeletedTable = true;
+
+ try {
+
+ // ===== test init =====
+ var testfile = do_get_file("formhistory_apitest.sqlite");
+ var profileDir = dirSvc.get("ProfD", Ci.nsIFile);
+
+ // Cleanup from any previous tests or failures.
+ var destFile = profileDir.clone();
+ destFile.append("formhistory.sqlite");
+ if (destFile.exists())
+ destFile.remove(false);
+
+ testfile.copyTo(profileDir, "formhistory.sqlite");
+
+ function checkExists(num) { do_check_true(num > 0); }
+ function checkNotExists(num) { do_check_true(num == 0); }
+
+ // ===== 1 =====
+ // Check initial state is as expected
+ testnum++;
+ yield promiseCountEntries("name-A", null, checkExists);
+ yield promiseCountEntries("name-B", null, checkExists);
+ yield promiseCountEntries("name-C", null, checkExists);
+ yield promiseCountEntries("name-D", null, checkExists);
+ yield promiseCountEntries("name-A", "value-A", checkExists);
+ yield promiseCountEntries("name-B", "value-B1", checkExists);
+ yield promiseCountEntries("name-B", "value-B2", checkExists);
+ yield promiseCountEntries("name-C", "value-C", checkExists);
+ yield 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 = Services.storage.openUnsharedDatabase(dbFile);
+
+ let deferred = Promise.defer();
+
+ let stmt = dbConnection.createAsyncStatement("DELETE FROM moz_deleted_formhistory");
+ stmt.executeAsync({
+ handleResult: function(resultSet) { },
+ handleError : function () {
+ do_throw("Error occurred counting deleted all entries: " + error);
+ },
+ handleCompletion : function () {
+ stmt.finalize();
+ deferred.resolve();
+ }
+ });
+ yield deferred.promise;
+
+ // ===== 2 =====
+ // Test looking for nonexistent / bogus data.
+ testnum++;
+ yield promiseCountEntries("blah", null, checkNotExists);
+ yield promiseCountEntries("", null, checkNotExists);
+ yield promiseCountEntries("name-A", "blah", checkNotExists);
+ yield promiseCountEntries("name-A", "", checkNotExists);
+ yield promiseCountEntries("name-A", null, checkExists);
+ yield promiseCountEntries("blah", "value-A", checkNotExists);
+ yield promiseCountEntries("", "value-A", checkNotExists);
+ yield 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.
+ deferred = Promise.defer();
+ yield FormHistory.count({ fieldname: null, value: null },
+ { handleResult: result => checkNotExists(result),
+ handleError: function (error) {
+ do_throw("Error occurred searching form history: " + error);
+ },
+ handleCompletion: function(reason) { if (!reason) deferred.resolve() }
+ });
+ yield deferred.promise;
+
+ // ===== 3 =====
+ // Test removeEntriesForName with a single matching value
+ testnum++;
+ yield promiseUpdateEntry("remove", "name-A", null);
+
+ yield promiseCountEntries("name-A", "value-A", checkNotExists);
+ yield promiseCountEntries("name-B", "value-B1", checkExists);
+ yield promiseCountEntries("name-B", "value-B2", checkExists);
+ yield promiseCountEntries("name-C", "value-C", checkExists);
+ yield promiseCountEntries("name-D", "value-D", checkExists);
+ yield countDeletedEntries(1);
+
+ // ===== 4 =====
+ // Test removeEntriesForName with multiple matching values
+ testnum++;
+ yield promiseUpdateEntry("remove", "name-B", null);
+
+ yield promiseCountEntries("name-A", "value-A", checkNotExists);
+ yield promiseCountEntries("name-B", "value-B1", checkNotExists);
+ yield promiseCountEntries("name-B", "value-B2", checkNotExists);
+ yield promiseCountEntries("name-C", "value-C", checkExists);
+ yield promiseCountEntries("name-D", "value-D", checkExists);
+ yield countDeletedEntries(3);
+
+ // ===== 5 =====
+ // Test removing by time range (single entry, not surrounding entries)
+ testnum++;
+ yield promiseCountEntries("time-A", null, checkExists); // firstUsed=1000, lastUsed=1000
+ yield promiseCountEntries("time-B", null, checkExists); // firstUsed=1000, lastUsed=1099
+ yield promiseCountEntries("time-C", null, checkExists); // firstUsed=1099, lastUsed=1099
+ yield promiseCountEntries("time-D", null, checkExists); // firstUsed=2001, lastUsed=2001
+ yield promiseUpdate({ op : "remove", firstUsedStart: 1050, firstUsedEnd: 2000 });
+
+ yield promiseCountEntries("time-A", null, checkExists);
+ yield promiseCountEntries("time-B", null, checkExists);
+ yield promiseCountEntries("time-C", null, checkNotExists);
+ yield promiseCountEntries("time-D", null, checkExists);
+ yield countDeletedEntries(4);
+
+ // ===== 6 =====
+ // Test removing by time range (multiple entries)
+ testnum++;
+ yield promiseUpdate({ op : "remove", firstUsedStart: 1000, firstUsedEnd: 2000 });
+
+ yield promiseCountEntries("time-A", null, checkNotExists);
+ yield promiseCountEntries("time-B", null, checkNotExists);
+ yield promiseCountEntries("time-C", null, checkNotExists);
+ yield promiseCountEntries("time-D", null, checkExists);
+ yield countDeletedEntries(6);
+
+ // ===== 7 =====
+ // test removeAllEntries
+ testnum++;
+ yield promiseUpdateEntry("remove", null, null);
+
+ yield promiseCountEntries("name-C", null, checkNotExists);
+ yield promiseCountEntries("name-D", null, checkNotExists);
+ yield promiseCountEntries("name-C", "value-C", checkNotExists);
+ yield promiseCountEntries("name-D", "value-D", checkNotExists);
+
+ yield promiseCountEntries(null, null, checkNotExists);
+ yield countDeletedEntries(6);
+
+ // ===== 8 =====
+ // Add a single entry back
+ testnum++;
+ yield promiseUpdateEntry("add", "newname-A", "newvalue-A");
+ yield promiseCountEntries("newname-A", "newvalue-A", checkExists);
+
+ // ===== 9 =====
+ // Remove the single entry
+ testnum++;
+ yield promiseUpdateEntry("remove", "newname-A", "newvalue-A");
+ yield promiseCountEntries("newname-A", "newvalue-A", checkNotExists);
+
+ // ===== 10 =====
+ // Add a single entry
+ testnum++;
+ yield promiseUpdateEntry("add", "field1", "value1");
+ yield promiseCountEntries("field1", "value1", checkExists);
+
+ let processFirstResult = function processResults(results)
+ {
+ // Only handle the first result
+ if (results.length > 0) {
+ let result = results[0];
+ return [result.timesUsed, result.firstUsed, result.lastUsed, result.guid];
+ }
+ return undefined;
+ }
+
+ results = yield promiseSearchEntries(["timesUsed", "firstUsed", "lastUsed"],
+ { fieldname: "field1", value: "value1" });
+ let [timesUsed, firstUsed, lastUsed] = processFirstResult(results);
+ do_check_eq(1, timesUsed);
+ do_check_true(firstUsed > 0);
+ do_check_true(lastUsed > 0);
+ yield promiseCountEntries(null, null, num => do_check_eq(num, 1));
+
+ // ===== 11 =====
+ // Add another single entry
+ testnum++;
+ yield promiseUpdateEntry("add", "field1", "value1b");
+ yield promiseCountEntries("field1", "value1", checkExists);
+ yield promiseCountEntries("field1", "value1b", checkExists);
+ yield promiseCountEntries(null, null, num => do_check_eq(num, 2));
+
+ // ===== 12 =====
+ // Update a single entry
+ testnum++;
+
+ results = yield promiseSearchEntries(["guid"], { fieldname: "field1", value: "value1" });
+ let guid = processFirstResult(results)[3];
+
+ yield promiseUpdate({ op : "update", guid: guid, value: "modifiedValue" });
+ yield promiseCountEntries("field1", "modifiedValue", checkExists);
+ yield promiseCountEntries("field1", "value1", checkNotExists);
+ yield promiseCountEntries("field1", "value1b", checkExists);
+ yield promiseCountEntries(null, null, num => do_check_eq(num, 2));
+
+ // ===== 13 =====
+ // Add a single entry with times
+ testnum++;
+ yield promiseUpdate({ op : "add", fieldname: "field2", value: "value2",
+ timesUsed: 20, firstUsed: 100, lastUsed: 500 });
+
+ results = yield promiseSearchEntries(["timesUsed", "firstUsed", "lastUsed"],
+ { fieldname: "field2", value: "value2" });
+ [timesUsed, firstUsed, lastUsed] = processFirstResult(results);
+
+ do_check_eq(20, timesUsed);
+ do_check_eq(100, firstUsed);
+ do_check_eq(500, lastUsed);
+ yield promiseCountEntries(null, null, num => do_check_eq(num, 3));
+
+ // ===== 14 =====
+ // Bump an entry, which updates its lastUsed field
+ testnum++;
+ yield promiseUpdate({ op : "bump", fieldname: "field2", value: "value2",
+ timesUsed: 20, firstUsed: 100, lastUsed: 500 });
+ results = yield promiseSearchEntries(["timesUsed", "firstUsed", "lastUsed"],
+ { fieldname: "field2", value: "value2" });
+ [timesUsed, firstUsed, lastUsed] = processFirstResult(results);
+ do_check_eq(21, timesUsed);
+ do_check_eq(100, firstUsed);
+ do_check_true(lastUsed > 500);
+ yield promiseCountEntries(null, null, num => do_check_eq(num, 3));
+
+ // ===== 15 =====
+ // Bump an entry that does not exist
+ testnum++;
+ yield promiseUpdate({ op : "bump", fieldname: "field3", value: "value3",
+ timesUsed: 10, firstUsed: 50, lastUsed: 400 });
+ results = yield promiseSearchEntries(["timesUsed", "firstUsed", "lastUsed"],
+ { fieldname: "field3", value: "value3" });
+ [timesUsed, firstUsed, lastUsed] = processFirstResult(results);
+ do_check_eq(10, timesUsed);
+ do_check_eq(50, firstUsed);
+ do_check_eq(400, lastUsed);
+ yield promiseCountEntries(null, null, num => do_check_eq(num, 4));
+
+ // ===== 16 =====
+ // Bump an entry with a guid
+ testnum++;
+ results = yield promiseSearchEntries(["guid"], { fieldname: "field3", value: "value3" });
+ guid = processFirstResult(results)[3];
+ yield promiseUpdate({ op : "bump", guid: guid, timesUsed: 20, firstUsed: 55, lastUsed: 400 });
+ results = yield promiseSearchEntries(["timesUsed", "firstUsed", "lastUsed"],
+ { fieldname: "field3", value: "value3" });
+ [timesUsed, firstUsed, lastUsed] = processFirstResult(results);
+ do_check_eq(11, timesUsed);
+ do_check_eq(50, firstUsed);
+ do_check_true(lastUsed > 400);
+ yield promiseCountEntries(null, null, num => do_check_eq(num, 4));
+
+ // ===== 17 =====
+ // Remove an entry
+ testnum++;
+ yield countDeletedEntries(7);
+
+ results = yield promiseSearchEntries(["guid"], { fieldname: "field1", value: "value1b" });
+ guid = processFirstResult(results)[3];
+
+ yield promiseUpdate({ op : "remove", guid: guid});
+ yield promiseCountEntries("field1", "modifiedValue", checkExists);
+ yield promiseCountEntries("field1", "value1b", checkNotExists);
+ yield promiseCountEntries(null, null, num => do_check_eq(num, 3));
+
+ yield countDeletedEntries(8);
+ yield checkTimeDeleted(guid, timeDeleted => do_check_true(timeDeleted > 10000));
+
+ // ===== 18 =====
+ // Add yet another single entry
+ testnum++;
+ yield promiseUpdate({ op : "add", fieldname: "field4", value: "value4",
+ timesUsed: 5, firstUsed: 230, lastUsed: 600 });
+ yield promiseCountEntries(null, null, num => do_check_eq(num, 4));
+
+ // ===== 19 =====
+ // Remove an entry by time
+ testnum++;
+ yield promiseUpdate({ op : "remove", firstUsedStart: 60, firstUsedEnd: 250 });
+ yield promiseCountEntries("field1", "modifiedValue", checkExists);
+ yield promiseCountEntries("field2", "value2", checkNotExists);
+ yield promiseCountEntries("field3", "value3", checkExists);
+ yield promiseCountEntries("field4", "value4", checkNotExists);
+ yield promiseCountEntries(null, null, num => do_check_eq(num, 2));
+ yield countDeletedEntries(10);
+
+ // ===== 20 =====
+ // Bump multiple existing entries at once
+ testnum++;
+
+ yield 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 }]);
+ yield promiseCountEntries(null, null, num => do_check_eq(num, 4));
+
+ yield promiseUpdate([
+ { op : "bump", fieldname: "field5", value: "value5" },
+ { op : "bump", fieldname: "field6", value: "value6" }]);
+ results = yield promiseSearchEntries(["fieldname", "timesUsed", "firstUsed", "lastUsed"], { });
+
+ do_check_eq(6, results[2].timesUsed);
+ do_check_eq(13, results[3].timesUsed);
+ do_check_eq(230, results[2].firstUsed);
+ do_check_eq(430, results[3].firstUsed);
+ do_check_true(results[2].lastUsed > 600);
+ do_check_true(results[3].lastUsed > 700);
+
+ yield promiseCountEntries(null, null, num => do_check_eq(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.
+ Assert.rejects(promiseUpdate(
+ { op : "bump", fieldname: "field5", value: "value5" }),
+ function(err) { return err.result == Ci.mozIStorageError.MISUSE; },
+ "bumping when form history is disabled should fail");
+ Assert.rejects(promiseUpdate(
+ { op : "add", fieldname: "field5", value: "value5" }),
+ function(err) { return err.result == Ci.mozIStorageError.MISUSE; },
+ "Adding when form history is disabled should fail");
+ Assert.rejects(promiseUpdate([
+ { op : "update", fieldname: "field5", value: "value5" },
+ { op : "remove", fieldname: "field5", value: "value5" }
+ ]),
+ function(err) { return err.result == Ci.mozIStorageError.MISUSE; },
+ "mixed operations when form history is disabled should fail");
+ Assert.rejects(promiseUpdate([
+ null, undefined, "", 1, {},
+ { op : "remove", fieldname: "field5", value: "value5" }
+ ]),
+ function(err) { return err.result == Ci.mozIStorageError.MISUSE; },
+ "Invalid entries when form history is disabled should fail");
+
+ // Remove should work though.
+ yield promiseUpdate([{ op: "remove", fieldname: "field5", value: null },
+ { op: "remove", fieldname: null, value: null }]);
+ Services.prefs.clearUserPref("browser.formfill.enable");
+
+ } catch (e) {
+ throw "FAILED in test #" + testnum + " -- " + e;
+ }
+ finally {
+ FormHistory._supportsDeletedTable = oldSupportsDeletedTable;
+ dbConnection.asyncClose(do_test_finished);
+ }
+});
+
+function run_test() {
+ return run_next_test();
+}
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 000000000..556ecd4b0
--- /dev/null
+++ b/toolkit/components/satchel/test/unit/test_notify.js
@@ -0,0 +1,158 @@
+/*
+ * Test suite for satchel notifications
+ *
+ * Tests notifications dispatched when modifying form history.
+ *
+ */
+
+var expectedNotification;
+var expectedData;
+
+var TestObserver = {
+ QueryInterface : XPCOMUtils.generateQI([Ci.nsIObserver, Ci.nsISupportsWeakReference]),
+
+ observe : function (subject, topic, data) {
+ do_check_eq(topic, "satchel-storage-changed");
+ do_check_eq(data, expectedNotification);
+
+ switch (data) {
+ case "formhistory-add":
+ case "formhistory-update":
+ do_check_true(subject instanceof Ci.nsISupportsString);
+ do_check_true(isGUID.test(subject.toString()));
+ break;
+ case "formhistory-remove":
+ do_check_eq(null, subject);
+ break;
+ default:
+ do_throw("Unhandled notification: " + data + " / " + topic);
+ }
+
+ expectedNotification = null;
+ expectedData = null;
+ }
+};
+
+var testIterator = null;
+
+function run_test() {
+ do_test_pending();
+ testIterator = run_test_steps();
+ testIterator.next();
+}
+
+function next_test()
+{
+ testIterator.next();
+}
+
+function* run_test_steps() {
+
+try {
+
+var testnum = 0;
+var testdesc = "Setup of test form history entries";
+
+var entry1 = ["entry1", "value1"];
+
+/* ========== 1 ========== */
+testnum = 1;
+testdesc = "Initial connection to storage module"
+
+yield updateEntry("remove", null, null, next_test);
+yield countEntries(null, null, function (num) { do_check_false(num, "Checking initial DB is empty"); next_test(); });
+
+// Add the observer
+var os = Cc["@mozilla.org/observer-service;1"].
+ getService(Ci.nsIObserverService);
+os.addObserver(TestObserver, "satchel-storage-changed", false);
+
+/* ========== 2 ========== */
+testnum++;
+testdesc = "addEntry";
+
+expectedNotification = "formhistory-add";
+expectedData = entry1;
+
+yield updateEntry("add", entry1[0], entry1[1], next_test);
+do_check_eq(expectedNotification, null); // check that observer got a notification
+
+yield countEntries(entry1[0], entry1[1], function (num) { do_check_true(num > 0); next_test(); });
+
+/* ========== 3 ========== */
+testnum++;
+testdesc = "modifyEntry";
+
+expectedNotification = "formhistory-update";
+expectedData = entry1;
+// will update previous entry
+yield updateEntry("update", entry1[0], entry1[1], next_test);
+yield countEntries(entry1[0], entry1[1], function (num) { do_check_true(num > 0); next_test(); });
+
+do_check_eq(expectedNotification, null);
+
+/* ========== 4 ========== */
+testnum++;
+testdesc = "removeEntry";
+
+expectedNotification = "formhistory-remove";
+expectedData = entry1;
+yield updateEntry("remove", entry1[0], entry1[1], next_test);
+
+do_check_eq(expectedNotification, null);
+yield countEntries(entry1[0], entry1[1], function(num) { do_check_false(num, "doesn't exist after remove"); next_test(); });
+
+/* ========== 5 ========== */
+testnum++;
+testdesc = "removeAllEntries";
+
+expectedNotification = "formhistory-remove";
+expectedData = null; // no data expected
+yield updateEntry("remove", null, null, next_test);
+
+do_check_eq(expectedNotification, null);
+
+/* ========== 6 ========== */
+testnum++;
+testdesc = "removeAllEntries (again)";
+
+expectedNotification = "formhistory-remove";
+expectedData = null;
+yield updateEntry("remove", null, null, next_test);
+
+do_check_eq(expectedNotification, null);
+
+/* ========== 7 ========== */
+testnum++;
+testdesc = "removeEntriesForName";
+
+expectedNotification = "formhistory-remove";
+expectedData = "field2";
+yield updateEntry("remove", null, "field2", next_test);
+
+do_check_eq(expectedNotification, null);
+
+/* ========== 8 ========== */
+testnum++;
+testdesc = "removeEntriesByTimeframe";
+
+expectedNotification = "formhistory-remove";
+expectedData = [10, 99999999999];
+
+yield FormHistory.update({ op: "remove", firstUsedStart: expectedData[0], firstUsedEnd: expectedData[1] },
+ { handleCompletion: function(reason) { if (!reason) next_test() },
+ handleErrors: function (error) {
+ do_throw("Error occurred updating form history: " + error);
+ }
+ });
+
+do_check_eq(expectedNotification, null);
+
+os.removeObserver(TestObserver, "satchel-storage-changed", false);
+
+do_test_finished();
+
+} catch (e) {
+ throw "FAILED in test #" + testnum + " -- " + testdesc + ": " + e;
+}
+}
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 000000000..06e5a385b
--- /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: function(search, result) {
+ do_check_eq(result.searchString, "aaa");
+ do_test_finished();
+ }
+};
+
+var aaListener = {
+ onSearchResult: function(search, result) {
+ do_check_eq(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(Components.interfaces.nsIAutoCompleteSearch);
+ search.startSearch("aa", "", null, aaListener);
+}
diff --git a/toolkit/components/satchel/test/unit/xpcshell.ini b/toolkit/components/satchel/test/unit/xpcshell.ini
new file mode 100644
index 000000000..4a41b47d6
--- /dev/null
+++ b/toolkit/components/satchel/test/unit/xpcshell.ini
@@ -0,0 +1,26 @@
+[DEFAULT]
+head = head_satchel.js
+tail =
+skip-if = toolkit == '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
+ perf_autocomplete.js
+
+[test_async_expire.js]
+[test_autocomplete.js]
+[test_db_corrupt.js]
+[test_db_update_v4.js]
+[test_db_update_v4b.js]
+[test_db_update_v999a.js]
+[test_db_update_v999b.js]
+[test_history_api.js]
+[test_notify.js]
+[test_previous_result.js]