/* 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(); }