diff options
Diffstat (limited to 'dom/localstorage/test/unit')
47 files changed, 3096 insertions, 0 deletions
diff --git a/dom/localstorage/test/unit/archive_profile.zip b/dom/localstorage/test/unit/archive_profile.zip Binary files differnew file mode 100644 index 0000000000..71b2d1e5f9 --- /dev/null +++ b/dom/localstorage/test/unit/archive_profile.zip diff --git a/dom/localstorage/test/unit/corruptedDatabase_missingUsageFile_profile.zip b/dom/localstorage/test/unit/corruptedDatabase_missingUsageFile_profile.zip Binary files differnew file mode 100644 index 0000000000..8cfa6e3d43 --- /dev/null +++ b/dom/localstorage/test/unit/corruptedDatabase_missingUsageFile_profile.zip diff --git a/dom/localstorage/test/unit/corruptedDatabase_profile.zip b/dom/localstorage/test/unit/corruptedDatabase_profile.zip Binary files differnew file mode 100644 index 0000000000..2f60db2a45 --- /dev/null +++ b/dom/localstorage/test/unit/corruptedDatabase_profile.zip diff --git a/dom/localstorage/test/unit/databaseShadowing-shared.js b/dom/localstorage/test/unit/databaseShadowing-shared.js new file mode 100644 index 0000000000..ffee8579cb --- /dev/null +++ b/dom/localstorage/test/unit/databaseShadowing-shared.js @@ -0,0 +1,130 @@ +/* import-globals-from head.js */ + +const principalInfos = [ + { url: "http://example.com", attrs: {} }, + + { url: "http://origin.test", attrs: {} }, + + { url: "http://prefix.test", attrs: {} }, + { url: "http://prefix.test", attrs: { userContextId: 10 } }, + + { url: "http://pattern.test", attrs: { userContextId: 15 } }, + { url: "http://pattern.test:8080", attrs: { userContextId: 15 } }, + { url: "https://pattern.test", attrs: { userContextId: 15 } }, +]; + +const surrogate = String.fromCharCode(0xdc00); +const replacement = String.fromCharCode(0xfffd); +const beginning = "beginning"; +const ending = "ending"; +const complexValue = beginning + surrogate + surrogate + ending; +const corruptedValue = beginning + replacement + replacement + ending; + +function enableNextGenLocalStorage() { + info("Setting pref"); + + Services.prefs.setBoolPref( + "dom.storage.enable_unsupported_legacy_implementation", + false + ); +} + +function disableNextGenLocalStorage() { + info("Setting pref"); + + Services.prefs.setBoolPref( + "dom.storage.enable_unsupported_legacy_implementation", + true + ); +} + +function storeData() { + for (let i = 0; i < principalInfos.length; i++) { + let principalInfo = principalInfos[i]; + let principal = getPrincipal(principalInfo.url, principalInfo.attrs); + + info("Getting storage"); + + let storage = getLocalStorage(principal); + + info("Adding data"); + + storage.setItem("key0", "value0"); + storage.clear(); + storage.setItem("key1", "value1"); + storage.removeItem("key1"); + storage.setItem("key2", "value2"); + storage.setItem("complexKey", complexValue); + + info("Closing storage"); + + storage.close(); + } +} + +function exportShadowDatabase(name) { + info("Verifying shadow database"); + + let profileDir = Services.dirsvc.get("ProfD", Ci.nsIFile); + let shadowDatabase = profileDir.clone(); + shadowDatabase.append("webappsstore.sqlite"); + + let exists = shadowDatabase.exists(); + ok(exists, "Shadow database does exist"); + + info("Copying shadow database"); + + let currentDir = Services.dirsvc.get("CurWorkD", Ci.nsIFile); + shadowDatabase.copyTo(currentDir, name); +} + +function importShadowDatabase(name) { + info("Verifying shadow database"); + + let currentDir = Services.dirsvc.get("CurWorkD", Ci.nsIFile); + let shadowDatabase = currentDir.clone(); + shadowDatabase.append(name); + + let exists = shadowDatabase.exists(); + if (!exists) { + return false; + } + + info("Copying shadow database"); + + let profileDir = Services.dirsvc.get("ProfD", Ci.nsIFile); + shadowDatabase.copyTo(profileDir, "webappsstore.sqlite"); + + return true; +} + +function verifyData(clearedOrigins, migrated = false) { + for (let i = 0; i < principalInfos.length; i++) { + let principalInfo = principalInfos[i]; + let principal = getPrincipal(principalInfo.url, principalInfo.attrs); + + info("Getting storage"); + + let storage = getLocalStorage(principal); + + info("Verifying data"); + + if (clearedOrigins.includes(i)) { + ok(storage.getItem("key2") == null, "Correct value"); + ok(storage.getItem("complexKey") == null, "Correct value"); + } else { + ok(storage.getItem("key0") == null, "Correct value"); + ok(storage.getItem("key1") == null, "Correct value"); + is(storage.getItem("key2"), "value2", "Correct value"); + is( + storage.getItem("complexKey"), + migrated ? corruptedValue : complexValue, + "Correct value" + ); + } + + info("Closing storage"); + + storage.close(); + } +} diff --git a/dom/localstorage/test/unit/groupMismatch_profile.zip b/dom/localstorage/test/unit/groupMismatch_profile.zip Binary files differnew file mode 100644 index 0000000000..182b013de0 --- /dev/null +++ b/dom/localstorage/test/unit/groupMismatch_profile.zip diff --git a/dom/localstorage/test/unit/head.js b/dom/localstorage/test/unit/head.js new file mode 100644 index 0000000000..eaca1ed173 --- /dev/null +++ b/dom/localstorage/test/unit/head.js @@ -0,0 +1,332 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +// Tests are expected to define testSteps. +/* globals testSteps */ + +const NS_ERROR_DOM_QUOTA_EXCEEDED_ERR = 22; + +function is(a, b, msg) { + Assert.equal(a, b, msg); +} + +function ok(cond, msg) { + Assert.ok(!!cond, msg); +} + +add_setup(function() { + do_get_profile(); + + enableTesting(); + + Cu.importGlobalProperties(["crypto"]); + + registerCleanupFunction(resetTesting); +}); + +function returnToEventLoop() { + return new Promise(function(resolve) { + executeSoon(resolve); + }); +} + +function enableTesting() { + Services.prefs.setBoolPref("dom.simpleDB.enabled", true); + Services.prefs.setBoolPref("dom.storage.testing", true); + + // xpcshell globals don't have associated clients in the Clients API sense, so + // we need to disable client validation so that the unit tests are allowed to + // use LocalStorage. + Services.prefs.setBoolPref("dom.storage.client_validation", false); + + Services.prefs.setBoolPref("dom.quotaManager.testing", true); +} + +function resetTesting() { + Services.prefs.clearUserPref("dom.quotaManager.testing"); + Services.prefs.clearUserPref("dom.storage.client_validation"); + Services.prefs.clearUserPref("dom.storage.testing"); + Services.prefs.clearUserPref("dom.simpleDB.enabled"); +} + +function setGlobalLimit(globalLimit) { + Services.prefs.setIntPref( + "dom.quotaManager.temporaryStorage.fixedLimit", + globalLimit + ); +} + +function resetGlobalLimit() { + Services.prefs.clearUserPref("dom.quotaManager.temporaryStorage.fixedLimit"); +} + +function setOriginLimit(originLimit) { + Services.prefs.setIntPref("dom.storage.default_quota", originLimit); +} + +function resetOriginLimit() { + Services.prefs.clearUserPref("dom.storage.default_quota"); +} + +function setTimeout(callback, timeout) { + let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); + + timer.initWithCallback( + { + notify() { + callback(); + }, + }, + timeout, + Ci.nsITimer.TYPE_ONE_SHOT + ); + + return timer; +} + +function initStorage() { + return Services.qms.init(); +} + +function initTemporaryStorage() { + return Services.qms.initTemporaryStorage(); +} + +function initPersistentOrigin(principal) { + return Services.qms.initializePersistentOrigin(principal); +} + +function initTemporaryOrigin(persistence, principal) { + return Services.qms.initializeTemporaryOrigin(persistence, principal); +} + +function getOriginUsage(principal, fromMemory = false) { + let request = Services.qms.getUsageForPrincipal( + principal, + function() {}, + fromMemory + ); + + return request; +} + +function clear() { + let request = Services.qms.clear(); + + return request; +} + +function clearOriginsByPattern(pattern) { + let request = Services.qms.clearStoragesForOriginAttributesPattern(pattern); + + return request; +} + +function clearOriginsByPrefix(principal, persistence) { + let request = Services.qms.clearStoragesForPrincipal( + principal, + persistence, + null, + true + ); + + return request; +} + +function clearOrigin(principal, persistence) { + let request = Services.qms.clearStoragesForPrincipal(principal, persistence); + + return request; +} + +function reset() { + let request = Services.qms.reset(); + + return request; +} + +function resetOrigin(principal) { + let request = Services.qms.resetStoragesForPrincipal( + principal, + "default", + "ls" + ); + + return request; +} + +function installPackage(packageName) { + let currentDir = Services.dirsvc.get("CurWorkD", Ci.nsIFile); + + let packageFile = currentDir.clone(); + packageFile.append(packageName + ".zip"); + + let zipReader = Cc["@mozilla.org/libjar/zip-reader;1"].createInstance( + Ci.nsIZipReader + ); + zipReader.open(packageFile); + + let entryNames = []; + let entries = zipReader.findEntries(null); + while (entries.hasMore()) { + let entry = entries.getNext(); + entryNames.push(entry); + } + entryNames.sort(); + + for (let entryName of entryNames) { + let zipentry = zipReader.getEntry(entryName); + + let file = getRelativeFile(entryName); + + if (zipentry.isDirectory) { + file.create(Ci.nsIFile.DIRECTORY_TYPE, parseInt("0755", 8)); + } else { + let istream = zipReader.getInputStream(entryName); + + var ostream = Cc[ + "@mozilla.org/network/file-output-stream;1" + ].createInstance(Ci.nsIFileOutputStream); + ostream.init(file, -1, parseInt("0644", 8), 0); + + let bostream = Cc[ + "@mozilla.org/network/buffered-output-stream;1" + ].createInstance(Ci.nsIBufferedOutputStream); + bostream.init(ostream, 32768); + + bostream.writeFrom(istream, istream.available()); + + istream.close(); + bostream.close(); + } + } + + zipReader.close(); +} + +function getProfileDir() { + return Services.dirsvc.get("ProfD", Ci.nsIFile); +} + +// Given a "/"-delimited path relative to the profile directory, +// return an nsIFile representing the path. This does not test +// for the existence of the file or parent directories. +// It is safe even on Windows where the directory separator is not "/", +// but make sure you're not passing in a "\"-delimited path. +function getRelativeFile(relativePath) { + let profileDir = getProfileDir(); + + let file = profileDir.clone(); + relativePath.split("/").forEach(function(component) { + file.append(component); + }); + + return file; +} + +function repeatChar(count, ch) { + if (count == 0) { + return ""; + } + + let result = ch; + let count2 = count / 2; + + // Double the input until it is long enough. + while (result.length <= count2) { + result += result; + } + + // Use substring to hit the precise length target without using extra memory. + return result + result.substring(0, count - result.length); +} + +function getPrincipal(url, attrs) { + let uri = Services.io.newURI(url); + if (!attrs) { + attrs = {}; + } + return Services.scriptSecurityManager.createContentPrincipal(uri, attrs); +} + +function getCurrentPrincipal() { + return Cc["@mozilla.org/systemprincipal;1"].createInstance(Ci.nsIPrincipal); +} + +function getDefaultPrincipal() { + return getPrincipal("http://example.com"); +} + +function getSimpleDatabase(principal, persistence) { + let connection = Cc["@mozilla.org/dom/sdb-connection;1"].createInstance( + Ci.nsISDBConnection + ); + + if (!principal) { + principal = getDefaultPrincipal(); + } + + connection.init(principal, persistence); + + return connection; +} + +function getLocalStorage(principal) { + if (!principal) { + principal = getDefaultPrincipal(); + } + + return Services.domStorageManager.createStorage( + null, + principal, + principal, + "" + ); +} + +class RequestError extends Error { + constructor(resultCode, resultName) { + super(`Request failed (code: ${resultCode}, name: ${resultName})`); + this.name = "RequestError"; + this.resultCode = resultCode; + this.resultName = resultName; + } +} + +async function requestFinished(request) { + await new Promise(function(resolve) { + request.callback = function() { + resolve(); + }; + }); + + if (request.resultCode !== Cr.NS_OK) { + throw new RequestError(request.resultCode, request.resultName); + } + + return request.result; +} + +function loadSubscript(path) { + let file = do_get_file(path, false); + let uri = Services.io.newFileURI(file); + Services.scriptloader.loadSubScript(uri.spec); +} + +async function readUsageFromUsageFile(usageFile) { + let file = await File.createFromNsIFile(usageFile); + + let buffer = await new Promise(resolve => { + let reader = new FileReader(); + reader.onloadend = () => resolve(reader.result); + reader.readAsArrayBuffer(file); + }); + + // Manually getting the lower 32-bits because of the lack of support for + // 64-bit values currently from DataView/JS (other than the BigInt support + // that's currently behind a flag). + let view = new DataView(buffer, 8, 4); + return view.getUint32(); +} diff --git a/dom/localstorage/test/unit/make_migration_emptyValue.js b/dom/localstorage/test/unit/make_migration_emptyValue.js new file mode 100644 index 0000000000..12484ad7f3 --- /dev/null +++ b/dom/localstorage/test/unit/make_migration_emptyValue.js @@ -0,0 +1,23 @@ +/* + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +*/ + +async function testSteps() { + const data = { + key: "foo", + value: "", + }; + + info("Setting pref"); + + Services.prefs.setBoolPref("dom.storage.next_gen", false); + + info("Getting storage"); + + const storage = getLocalStorage(); + + info("Adding data"); + + storage.setItem(data.key, data.value); +} diff --git a/dom/localstorage/test/unit/migration_emptyValue_profile.zip b/dom/localstorage/test/unit/migration_emptyValue_profile.zip Binary files differnew file mode 100644 index 0000000000..b829beae77 --- /dev/null +++ b/dom/localstorage/test/unit/migration_emptyValue_profile.zip diff --git a/dom/localstorage/test/unit/migration_profile.zip b/dom/localstorage/test/unit/migration_profile.zip Binary files differnew file mode 100644 index 0000000000..19dc3d4805 --- /dev/null +++ b/dom/localstorage/test/unit/migration_profile.zip diff --git a/dom/localstorage/test/unit/schema3upgrade_profile.zip b/dom/localstorage/test/unit/schema3upgrade_profile.zip Binary files differnew file mode 100644 index 0000000000..1ee9bfbf2e --- /dev/null +++ b/dom/localstorage/test/unit/schema3upgrade_profile.zip diff --git a/dom/localstorage/test/unit/schema4upgrade_profile.zip b/dom/localstorage/test/unit/schema4upgrade_profile.zip Binary files differnew file mode 100644 index 0000000000..ae8ba09606 --- /dev/null +++ b/dom/localstorage/test/unit/schema4upgrade_profile.zip diff --git a/dom/localstorage/test/unit/stringLength2_profile.zip b/dom/localstorage/test/unit/stringLength2_profile.zip Binary files differnew file mode 100644 index 0000000000..de4d0fc3aa --- /dev/null +++ b/dom/localstorage/test/unit/stringLength2_profile.zip diff --git a/dom/localstorage/test/unit/stringLength_profile.zip b/dom/localstorage/test/unit/stringLength_profile.zip Binary files differnew file mode 100644 index 0000000000..6cac890860 --- /dev/null +++ b/dom/localstorage/test/unit/stringLength_profile.zip diff --git a/dom/localstorage/test/unit/test_archive.js b/dom/localstorage/test/unit/test_archive.js new file mode 100644 index 0000000000..51af78db1b --- /dev/null +++ b/dom/localstorage/test/unit/test_archive.js @@ -0,0 +1,78 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +add_task(async function testSteps() { + const lsArchiveFile = "storage/ls-archive.sqlite"; + + const principalInfo = { + url: "http://example.com", + attrs: {}, + }; + + function checkStorage() { + let principal = getPrincipal(principalInfo.url, principalInfo.attrs); + let storage = getLocalStorage(principal); + try { + storage.open(); + ok(true, "Did not throw"); + } catch (ex) { + ok(false, "Should not have thrown"); + } + } + + info("Setting pref"); + + Services.prefs.setBoolPref("dom.storage.next_gen", true); + + info("Sub test case 1 - Archive file is a directory."); + + info("Clearing"); + + let request = clear(); + await requestFinished(request); + + let archiveFile = getRelativeFile(lsArchiveFile); + + archiveFile.create(Ci.nsIFile.DIRECTORY_TYPE, parseInt("0755", 8)); + + checkStorage(); + + info("Sub test case 2 - Corrupted archive file."); + + info("Clearing"); + + request = clear(); + await requestFinished(request); + + let ostream = Cc["@mozilla.org/network/file-output-stream;1"].createInstance( + Ci.nsIFileOutputStream + ); + ostream.init(archiveFile, -1, parseInt("0644", 8), 0); + ostream.write("foobar", 6); + ostream.close(); + + checkStorage(); + + info("Sub test case 3 - Nonupdateable archive file."); + + info("Clearing"); + + request = clear(); + await requestFinished(request); + + info("Installing package"); + + // The profile contains storage.sqlite and storage/ls-archive.sqlite + // storage/ls-archive.sqlite was taken from FF 54 to force an upgrade. + // There's just one record in the webappsstore2 table. The record was + // modified by renaming the origin attribute userContextId to userContextKey. + // This triggers an error during the upgrade. + installPackage("archive_profile"); + + let fileSize = archiveFile.fileSize; + ok(fileSize > 0, "archive file size is greater than zero"); + + checkStorage(); +}); diff --git a/dom/localstorage/test/unit/test_clientValidation.js b/dom/localstorage/test/unit/test_clientValidation.js new file mode 100644 index 0000000000..26dc2bfdf7 --- /dev/null +++ b/dom/localstorage/test/unit/test_clientValidation.js @@ -0,0 +1,32 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/** + * Because this is an xpcshell global, it does not have an associated client id. + * We turn on client validation for LocalStorage and ensure that we don't have + * access to LocalStorage. + */ +add_task(async function testSteps() { + const principal = getPrincipal("http://example.com"); + + info("Setting prefs"); + + Services.prefs.setBoolPref( + "dom.storage.enable_unsupported_legacy_implementation", + false + ); + Services.prefs.setBoolPref("dom.storage.client_validation", true); + + info("Getting storage"); + + try { + getLocalStorage(principal); + ok(false, "Should have thrown"); + } catch (ex) { + ok(true, "Did throw"); + is(ex.name, "NS_ERROR_FAILURE", "Threw right Exception"); + is(ex.result, Cr.NS_ERROR_FAILURE, "Threw with right result"); + } +}); diff --git a/dom/localstorage/test/unit/test_corruptedDatabase.js b/dom/localstorage/test/unit/test_corruptedDatabase.js new file mode 100644 index 0000000000..da73bb92f6 --- /dev/null +++ b/dom/localstorage/test/unit/test_corruptedDatabase.js @@ -0,0 +1,73 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +async function doTest(profile) { + info("Testing profile " + profile); + + info("Clearing"); + + let request = clear(); + await requestFinished(request); + + info("Installing package"); + + installPackage(profile); + + const principal = getPrincipal("http://example.org"); + + let storage = getLocalStorage(principal); + + let length = storage.length; + + ok(length === 0, "Correct length"); + + info("Resetting origin"); + + request = resetOrigin(principal); + await requestFinished(request); + + info("Getting usage"); + + request = getOriginUsage(principal); + await requestFinished(request); + + is(request.result.usage, 0, "Correct usage"); +} + +add_task(async function testSteps() { + info("Setting pref"); + + Services.prefs.setBoolPref( + "dom.storage.enable_unsupported_legacy_implementation", + false + ); + + // XXX This should be refactored into separate sub test cases. + + const profiles = [ + // This profile contains one localStorage, all localStorage related files, a + // script for localStorage creation and the storage database: + // - storage/default/http+++example.org/ls + // - storage/ls-archive.sqlite + // - create_db.js + // - storage.sqlite + // - webappsstore.sqlite + // The file create_db.js in the package was run locally, specifically it was + // temporarily added to xpcshell.ini and then executed: + // mach xpcshell-test --interactive dom/localstorage/test/unit/create_db.js + // Note: to make it become the profile in the test, additional manual steps + // are needed. + // 1. Manually change first 6 chars in data.sqlite to "foobar". + // 2. Remove the folder "storage/temporary". + "corruptedDatabase_profile", + // This profile is the same as corruptedDatabase_profile, except that the usage + // file (storage/default/http+++example.org/ls/usage) is missing. + "corruptedDatabase_missingUsageFile_profile", + ]; + + for (const profile of profiles) { + await doTest(profile); + } +}); diff --git a/dom/localstorage/test/unit/test_databaseShadowing1.js b/dom/localstorage/test/unit/test_databaseShadowing1.js new file mode 100644 index 0000000000..8582f434fc --- /dev/null +++ b/dom/localstorage/test/unit/test_databaseShadowing1.js @@ -0,0 +1,23 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* import-globals-from databaseShadowing-shared.js */ +loadSubscript("databaseShadowing-shared.js"); + +add_task(async function testSteps() { + enableNextGenLocalStorage(); + + storeData(); + + verifyData([]); + + // Wait for all database connections to close. + let request = reset(); + await requestFinished(request); + + exportShadowDatabase("shadowdb.sqlite"); + + // The shadow database is now prepared for test_databaseShadowing2.js +}); diff --git a/dom/localstorage/test/unit/test_databaseShadowing2.js b/dom/localstorage/test/unit/test_databaseShadowing2.js new file mode 100644 index 0000000000..f7b0ddb1a2 --- /dev/null +++ b/dom/localstorage/test/unit/test_databaseShadowing2.js @@ -0,0 +1,17 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* import-globals-from databaseShadowing-shared.js */ +loadSubscript("databaseShadowing-shared.js"); + +add_task(async function testSteps() { + // The shadow database was prepared in test_databaseShadowing1.js + + disableNextGenLocalStorage(); + + ok(importShadowDatabase("shadowdb.sqlite"), "Import succeeded"); + + verifyData([], /* migrated */ true); +}); diff --git a/dom/localstorage/test/unit/test_databaseShadowing_clearOrigin1.js b/dom/localstorage/test/unit/test_databaseShadowing_clearOrigin1.js new file mode 100644 index 0000000000..d88fde52e5 --- /dev/null +++ b/dom/localstorage/test/unit/test_databaseShadowing_clearOrigin1.js @@ -0,0 +1,30 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* import-globals-from databaseShadowing-shared.js */ +loadSubscript("databaseShadowing-shared.js"); + +add_task(async function testSteps() { + enableNextGenLocalStorage(); + + storeData(); + + verifyData([]); + + let principal = getPrincipal("http://origin.test", {}); + let request = clearOrigin(principal, "default"); + await requestFinished(request); + + verifyData([1]); + + // Wait for all database connections to close. + request = reset(); + await requestFinished(request); + + exportShadowDatabase("shadowdb-clearedOrigin.sqlite"); + + // The shadow database is now prepared for + // test_databaseShadowing_clearOrigin2.js +}); diff --git a/dom/localstorage/test/unit/test_databaseShadowing_clearOrigin2.js b/dom/localstorage/test/unit/test_databaseShadowing_clearOrigin2.js new file mode 100644 index 0000000000..83d792b496 --- /dev/null +++ b/dom/localstorage/test/unit/test_databaseShadowing_clearOrigin2.js @@ -0,0 +1,17 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* import-globals-from databaseShadowing-shared.js */ +loadSubscript("databaseShadowing-shared.js"); + +add_task(async function testSteps() { + // The shadow database was prepared in test_databaseShadowing_clearOrigin1.js + + disableNextGenLocalStorage(); + + ok(importShadowDatabase("shadowdb-clearedOrigin.sqlite"), "Import succeeded"); + + verifyData([1], /* migrated */ true); +}); diff --git a/dom/localstorage/test/unit/test_databaseShadowing_clearOriginsByPattern1.js b/dom/localstorage/test/unit/test_databaseShadowing_clearOriginsByPattern1.js new file mode 100644 index 0000000000..70367bbeff --- /dev/null +++ b/dom/localstorage/test/unit/test_databaseShadowing_clearOriginsByPattern1.js @@ -0,0 +1,29 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* import-globals-from databaseShadowing-shared.js */ +loadSubscript("databaseShadowing-shared.js"); + +add_task(async function testSteps() { + enableNextGenLocalStorage(); + + storeData(); + + verifyData([]); + + let request = clearOriginsByPattern(JSON.stringify({ userContextId: 15 })); + await requestFinished(request); + + verifyData([4, 5, 6]); + + // Wait for all database connections to close. + request = reset(); + await requestFinished(request); + + exportShadowDatabase("shadowdb-clearedOriginsByPattern.sqlite"); + + // The shadow database is now prepared for + // test_databaseShadowing_clearOriginsByPattern2.js +}); diff --git a/dom/localstorage/test/unit/test_databaseShadowing_clearOriginsByPattern2.js b/dom/localstorage/test/unit/test_databaseShadowing_clearOriginsByPattern2.js new file mode 100644 index 0000000000..6c4d794d04 --- /dev/null +++ b/dom/localstorage/test/unit/test_databaseShadowing_clearOriginsByPattern2.js @@ -0,0 +1,21 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* import-globals-from databaseShadowing-shared.js */ +loadSubscript("databaseShadowing-shared.js"); + +add_task(async function testSteps() { + // The shadow database was prepared in + // test_databaseShadowing_clearOriginsByPattern1.js + + disableNextGenLocalStorage(); + + ok( + importShadowDatabase("shadowdb-clearedOriginsByPattern.sqlite"), + "Import succeeded" + ); + + verifyData([4, 5, 6], /* migrated */ true); +}); diff --git a/dom/localstorage/test/unit/test_databaseShadowing_clearOriginsByPrefix1.js b/dom/localstorage/test/unit/test_databaseShadowing_clearOriginsByPrefix1.js new file mode 100644 index 0000000000..2b605e953f --- /dev/null +++ b/dom/localstorage/test/unit/test_databaseShadowing_clearOriginsByPrefix1.js @@ -0,0 +1,28 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* import-globals-from databaseShadowing-shared.js */ +loadSubscript("databaseShadowing-shared.js"); + +add_task(async function testSteps() { + enableNextGenLocalStorage(); + + storeData(); + + verifyData([]); + + let principal = getPrincipal("http://prefix.test", {}); + let request = clearOriginsByPrefix(principal, "default"); + await requestFinished(request); + + // Wait for all database connections to close. + request = reset(); + await requestFinished(request); + + exportShadowDatabase("shadowdb-clearedOriginsByPrefix.sqlite"); + + // The shadow database is now prepared for + // test_databaseShadowing_clearOriginsByPrefix2.js +}); diff --git a/dom/localstorage/test/unit/test_databaseShadowing_clearOriginsByPrefix2.js b/dom/localstorage/test/unit/test_databaseShadowing_clearOriginsByPrefix2.js new file mode 100644 index 0000000000..892a470723 --- /dev/null +++ b/dom/localstorage/test/unit/test_databaseShadowing_clearOriginsByPrefix2.js @@ -0,0 +1,21 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* import-globals-from databaseShadowing-shared.js */ +loadSubscript("databaseShadowing-shared.js"); + +add_task(async function testSteps() { + // The shadow database was prepared in + // test_databaseShadowing_clearOriginsByPrefix1.js + + disableNextGenLocalStorage(); + + ok( + importShadowDatabase("shadowdb-clearedOriginsByPrefix.sqlite"), + "Import succeeded" + ); + + verifyData([2, 3], /* migrated */ true); +}); diff --git a/dom/localstorage/test/unit/test_eviction.js b/dom/localstorage/test/unit/test_eviction.js new file mode 100644 index 0000000000..d8c10b771f --- /dev/null +++ b/dom/localstorage/test/unit/test_eviction.js @@ -0,0 +1,91 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +add_task(async function testSteps() { + const globalLimitKB = 5 * 1024; + + const data = {}; + data.sizeKB = 1 * 1024; + data.key = "A"; + data.value = repeatChar(data.sizeKB * 1024 - data.key.length, "."); + data.urlCount = globalLimitKB / data.sizeKB; + + function getSpec(index) { + return "http://example" + index + ".com"; + } + + info("Setting prefs"); + + Services.prefs.setBoolPref( + "dom.storage.enable_unsupported_legacy_implementation", + false + ); + Services.prefs.setBoolPref("dom.storage.snapshot_reusing", false); + + info("Setting limits"); + + setGlobalLimit(globalLimitKB); + + let request = clear(); + await requestFinished(request); + + info("Getting storages"); + + let storages = []; + for (let i = 0; i < data.urlCount; i++) { + let storage = getLocalStorage(getPrincipal(getSpec(i))); + storages.push(storage); + } + + info("Filling up entire default storage"); + + for (let i = 0; i < data.urlCount; i++) { + storages[i].setItem(data.key, data.value); + await returnToEventLoop(); + } + + info("Verifying no more data can be written"); + + for (let i = 0; i < data.urlCount; i++) { + try { + storages[i].setItem("B", ""); + ok(false, "Should have thrown"); + } catch (ex) { + ok(true, "Did throw"); + ok(DOMException.isInstance(ex), "Threw DOMException"); + is(ex.name, "QuotaExceededError", "Threw right DOMException"); + is(ex.code, NS_ERROR_DOM_QUOTA_EXCEEDED_ERR, "Threw with right code"); + } + } + + info("Closing first origin"); + + storages[0].close(); + + let principal = getPrincipal("http://example0.com"); + + request = resetOrigin(principal); + await requestFinished(request); + + info("Getting usage for first origin"); + + request = getOriginUsage(principal); + await requestFinished(request); + + is(request.result.usage, data.sizeKB * 1024, "Correct usage"); + + info("Verifying more data data can be written"); + + for (let i = 1; i < data.urlCount; i++) { + storages[i].setItem("B", ""); + } + + info("Getting usage for first origin"); + + request = getOriginUsage(principal); + await requestFinished(request); + + is(request.result.usage, 0, "Zero usage"); +}); diff --git a/dom/localstorage/test/unit/test_flushing.js b/dom/localstorage/test/unit/test_flushing.js new file mode 100644 index 0000000000..b33ef7c099 --- /dev/null +++ b/dom/localstorage/test/unit/test_flushing.js @@ -0,0 +1,72 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ +/* eslint-disable mozilla/no-arbitrary-setTimeout */ + +/** + * This test is mainly to verify that the flush operation detaches the shadow + * database in the event of early return due to error. See bug 1559029. + */ + +add_task(async function testSteps() { + const principal1 = getPrincipal("http://example1.com"); + + const usageFile1 = getRelativeFile( + "storage/default/http+++example1.com/ls/usage" + ); + + const principal2 = getPrincipal("http://example2.com"); + + const data = { + key: "foo", + value: "bar", + }; + + const flushSleepTimeSec = 6; + + info("Setting prefs"); + + Services.prefs.setBoolPref("dom.storage.next_gen", true); + + info("Getting storage 1"); + + let storage1 = getLocalStorage(principal1); + + info("Adding item"); + + storage1.setItem(data.key, data.value); + + info("Creating usage as a directory"); + + // This will cause a failure during the flush for first principal. + usageFile1.create(Ci.nsIFile.DIRECTORY_TYPE, parseInt("0755", 8)); + + info("Getting storage 2"); + + let storage2 = getLocalStorage(principal2); + + info("Adding item"); + + storage2.setItem(data.key, data.value); + + // The flush for second principal shouldn't be affected by failed flush for + // first principal. + + info( + "Sleeping for " + + flushSleepTimeSec + + " seconds to let all flushes " + + "finish" + ); + + await new Promise(function(resolve) { + setTimeout(resolve, flushSleepTimeSec * 1000); + }); + + info("Resetting"); + + // Wait for all database connections to close. + let request = reset(); + await requestFinished(request); +}); diff --git a/dom/localstorage/test/unit/test_groupLimit.js b/dom/localstorage/test/unit/test_groupLimit.js new file mode 100644 index 0000000000..92ff07b7ab --- /dev/null +++ b/dom/localstorage/test/unit/test_groupLimit.js @@ -0,0 +1,85 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +add_task(async function testSteps() { + const groupLimitKB = 10 * 1024; + + const globalLimitKB = groupLimitKB * 5; + + const originLimit = 10 * 1024; + + const urls = [ + "http://example.com", + "http://test1.example.com", + "https://test2.example.com", + "http://test3.example.com:8080", + ]; + + const data = {}; + data.sizeKB = 5 * 1024; + data.key = "A"; + data.value = repeatChar(data.sizeKB * 1024 - data.key.length, "."); + data.urlCount = groupLimitKB / data.sizeKB; + + info("Setting pref"); + + Services.prefs.setBoolPref( + "dom.storage.enable_unsupported_legacy_implementation", + false + ); + Services.prefs.setBoolPref("dom.storage.snapshot_reusing", false); + + info("Setting limits"); + + setGlobalLimit(globalLimitKB); + + let request = clear(); + await requestFinished(request); + + setOriginLimit(originLimit); + + info("Getting storages"); + + let storages = []; + for (let i = 0; i < urls.length; i++) { + let storage = getLocalStorage(getPrincipal(urls[i])); + storages.push(storage); + } + + info("Filling up the whole group"); + + for (let i = 0; i < data.urlCount; i++) { + storages[i].setItem(data.key, data.value); + await returnToEventLoop(); + } + + info("Verifying no more data can be written"); + + for (let i = 0; i < urls.length; i++) { + try { + storages[i].setItem("B", ""); + ok(false, "Should have thrown"); + } catch (ex) { + ok(true, "Did throw"); + ok(DOMException.isInstance(ex), "Threw DOMException"); + is(ex.name, "QuotaExceededError", "Threw right DOMException"); + is(ex.code, NS_ERROR_DOM_QUOTA_EXCEEDED_ERR, "Threw with right code"); + } + } + + info("Clearing first origin"); + + storages[0].clear(); + + // Let the internal snapshot finish (usage is not descreased until all + // snapshots finish).. + await returnToEventLoop(); + + info("Verifying more data can be written"); + + for (let i = 0; i < urls.length; i++) { + storages[i].setItem("B", ""); + } +}); diff --git a/dom/localstorage/test/unit/test_groupMismatch.js b/dom/localstorage/test/unit/test_groupMismatch.js new file mode 100644 index 0000000000..46533aa2f7 --- /dev/null +++ b/dom/localstorage/test/unit/test_groupMismatch.js @@ -0,0 +1,45 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/** + * This test is mainly to verify that metadata files with old group information + * get updated, so writing to local storage won't cause a crash because of null + * quota object. See bug 1516333. + */ + +add_task(async function testSteps() { + const principal = getPrincipal("https://foo.bar.mozilla-iot.org"); + + info("Clearing"); + + let request = clear(); + await requestFinished(request); + + info("Installing package"); + + // The profile contains one initialized origin directory, a script for origin + // initialization and the storage database: + // - storage/default/https+++foo.bar.mozilla-iot.org + // - create_db.js + // - storage.sqlite + // The file create_db.js in the package was run locally, specifically it was + // temporarily added to xpcshell.ini and then executed: + // mach xpcshell-test --interactive dom/localstorage/test/unit/create_db.js + // Note: to make it become the profile in the test, additional manual steps + // are needed. + // 1. Manually change the group in .metadata and .metadata-v2 from + // "bar.mozilla-iot.org" to "mozilla-iot.org". + // 2. Remove the folder "storage/temporary". + // 3. Remove the file "storage/ls-archive.sqlite". + installPackage("groupMismatch_profile"); + + info("Getting storage"); + + let storage = getLocalStorage(principal); + + info("Adding item"); + + storage.setItem("foo", "bar"); +}); diff --git a/dom/localstorage/test/unit/test_largeItems.js b/dom/localstorage/test/unit/test_largeItems.js new file mode 100644 index 0000000000..3ea6bd21b4 --- /dev/null +++ b/dom/localstorage/test/unit/test_largeItems.js @@ -0,0 +1,88 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/** + * Test repeatedly setting values that are just under the LocalStorage quota + * limit without yielding control flow in order to verify that the write + * optimizer is present / works. If there was no write optimizer present, the + * IPC message size limit would be exceeded, resulting in a crash. + */ + +add_task(async function testSteps() { + const globalLimitKB = 5 * 1024; + + // 18 and more iterations would produce an IPC message with size greater than + // 256 MB if write optimizer was not present. This number was determined + // experimentally by running the test with disabled write optimizer. + const numberOfIterations = 18; + + const randomStringBlockSize = 65536; + + // We need to use a random string because LS internally tries to compress + // values. + function getRandomString(size) { + let crypto = this.window ? this.window.crypto : this.crypto; + let decoder = new TextDecoder("ISO-8859-2"); + + function getRandomStringBlock(array) { + crypto.getRandomValues(array); + return decoder.decode(array); + } + + let string = ""; + + let quotient = size / randomStringBlockSize; + if (quotient) { + let array = new Uint8Array(randomStringBlockSize); + for (let i = 1; i <= quotient; i++) { + string += getRandomStringBlock(array); + } + } + + let remainder = size % randomStringBlockSize; + if (remainder) { + let array = new Uint8Array(remainder); + string += getRandomStringBlock(array); + } + + return string; + } + + const data = {}; + data.key = "foo"; + data.value = getRandomString( + globalLimitKB * 1024 - + data.key.length - + numberOfIterations.toString().length + ); + + info("Setting pref"); + + // By disabling snapshot reusing, we guarantee that the snapshot will be + // checkpointed once control returns to the event loop. + if (this.window) { + await SpecialPowers.pushPrefEnv({ + set: [["dom.storage.snapshot_reusing", false]], + }); + } else { + Services.prefs.setBoolPref("dom.storage.snapshot_reusing", false); + } + + info("Getting storage"); + + let storage = getLocalStorage(); + + info("Adding/updating item"); + + for (var i = 0; i < numberOfIterations; i++) { + storage.setItem(data.key, data.value + i); + } + + info("Returning to event loop"); + + await returnToEventLoop(); + + ok(!storage.hasSnapshot, "Snapshot successfully finished"); +}); diff --git a/dom/localstorage/test/unit/test_lsng_enabled.js b/dom/localstorage/test/unit/test_lsng_enabled.js new file mode 100644 index 0000000000..d978aaa901 --- /dev/null +++ b/dom/localstorage/test/unit/test_lsng_enabled.js @@ -0,0 +1,13 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/** + * This test is mainly to verify that LSNG is not accidentally disabled which + * can lead to a data loss in a combination with disabled shadow writes. + */ + +add_task(async function testSteps() { + ok(Services.domStorageManager.nextGenLocalStorageEnabled, "LSNG enabled"); +}); diff --git a/dom/localstorage/test/unit/test_migration.js b/dom/localstorage/test/unit/test_migration.js new file mode 100644 index 0000000000..1249eb076f --- /dev/null +++ b/dom/localstorage/test/unit/test_migration.js @@ -0,0 +1,127 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +add_task(async function testSteps() { + const principalInfos = [ + { url: "http://localhost", attrs: {} }, + { url: "http://www.mozilla.org", attrs: {} }, + { url: "http://example.com", attrs: {} }, + { url: "http://example.org", attrs: { userContextId: 5 } }, + + { url: "http://origin.test", attrs: {} }, + + { url: "http://prefix.test", attrs: {} }, + { url: "http://prefix.test", attrs: { userContextId: 10 } }, + + { url: "http://pattern.test", attrs: { userContextId: 15 } }, + { url: "http://pattern.test:8080", attrs: { userContextId: 15 } }, + { url: "https://pattern.test", attrs: { userContextId: 15 } }, + ]; + + const data = { + key: "foo", + value: "bar", + }; + + function verifyData(clearedOrigins) { + info("Getting storages"); + + let storages = []; + for (let i = 0; i < principalInfos.length; i++) { + let principalInfo = principalInfos[i]; + let principal = getPrincipal(principalInfo.url, principalInfo.attrs); + let storage = getLocalStorage(principal); + storages.push(storage); + } + + info("Verifying data"); + + for (let i = 0; i < storages.length; i++) { + let value = storages[i].getItem(data.key + i); + if (clearedOrigins.includes(i)) { + is(value, null, "Correct value"); + } else { + is(value, data.value + i, "Correct value"); + } + } + } + + info("Setting pref"); + + Services.prefs.setBoolPref( + "dom.storage.enable_unsupported_legacy_implementation", + false + ); + + info("Stage 1 - Testing archived data migration"); + + info("Clearing"); + + let request = clear(); + await requestFinished(request); + + info("Installing package"); + + // The profile contains storage.sqlite and webappsstore.sqlite. The file + // create_db.js in the package was run locally, specifically it was + // temporarily added to xpcshell.ini and then executed: + // mach xpcshell-test --interactive dom/localstorage/test/unit/create_db.js + installPackage("migration_profile"); + + verifyData([]); + + info("Stage 2 - Testing archived data clearing"); + + for (let type of ["origin", "prefix", "pattern"]) { + info("Clearing"); + + request = clear(); + await requestFinished(request); + + info("Installing package"); + + // See the comment for the first installPackage() call. + installPackage("migration_profile"); + + let clearedOrigins = []; + + switch (type) { + case "origin": { + let principal = getPrincipal("http://origin.test", {}); + request = clearOrigin(principal, "default"); + await requestFinished(request); + + clearedOrigins.push(4); + + break; + } + + case "prefix": { + let principal = getPrincipal("http://prefix.test", {}); + request = clearOriginsByPrefix(principal, "default"); + await requestFinished(request); + + clearedOrigins.push(5, 6); + + break; + } + + case "pattern": { + request = clearOriginsByPattern(JSON.stringify({ userContextId: 15 })); + await requestFinished(request); + + clearedOrigins.push(7, 8, 9); + + break; + } + + default: { + throw new Error("Unknown type: " + type); + } + } + + verifyData(clearedOrigins); + } +}); diff --git a/dom/localstorage/test/unit/test_migration_emptyValue.js b/dom/localstorage/test/unit/test_migration_emptyValue.js new file mode 100644 index 0000000000..dd09c82e88 --- /dev/null +++ b/dom/localstorage/test/unit/test_migration_emptyValue.js @@ -0,0 +1,37 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +add_task(async function testSteps() { + const data = { + key: "foo", + value: "", + }; + + info("Clearing"); + + let request = clear(); + await requestFinished(request); + + info("Installing package"); + + // The profile contains storage.sqlite and webappsstore.sqlite. + // The archive migration_emptyValue_profile.zip was created by running + // make_migration_emptyValue.js locally, specifically the special test was + // temporarily activated in xpcshell.ini and then it was run as: + // mach test --interactive dom/localstorage/test/unit/make_migration_emptyValue.js + // Before packaging, additional manual steps are needed: + // 1. Folder "cache2" is removed. + // 2. Folder "crashes" is removed. + // 3. File "mozinfo.json" is removed. + installPackage("migration_emptyValue_profile"); + + info("Getting storage"); + + const storage = getLocalStorage(); + + info("Verifying data"); + + is(storage.getItem(data.key), data.value, "Correct value"); +}); diff --git a/dom/localstorage/test/unit/test_old_lsng_pref.js b/dom/localstorage/test/unit/test_old_lsng_pref.js new file mode 100644 index 0000000000..d502ee8779 --- /dev/null +++ b/dom/localstorage/test/unit/test_old_lsng_pref.js @@ -0,0 +1,17 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/** + * This test is mainly to verify that the old pref for switching LS + * implementations has no effect anymore. + */ + +add_task(async function testSteps() { + info("Setting pref"); + + Services.prefs.setBoolPref("dom.storage.next_gen", false); + + ok(Services.domStorageManager.nextGenLocalStorageEnabled, "LSNG enabled"); +}); diff --git a/dom/localstorage/test/unit/test_orderingAfterRemoveAdd.js b/dom/localstorage/test/unit/test_orderingAfterRemoveAdd.js new file mode 100644 index 0000000000..88a2e45d2a --- /dev/null +++ b/dom/localstorage/test/unit/test_orderingAfterRemoveAdd.js @@ -0,0 +1,70 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +add_task(async function testSteps() { + const url = "http://example.com"; + + info("Setting pref"); + + Services.prefs.setBoolPref("dom.storage.snapshot_reusing", false); + + const items = [ + { key: "key01", value: "value01" }, + { key: "key02", value: "value02" }, + { key: "key03", value: "value03" }, + { key: "key04", value: "value04" }, + { key: "key05", value: "value05" }, + ]; + + info("Getting storage"); + + let storage = getLocalStorage(getPrincipal(url)); + + // 1st snapshot + + info("Adding data"); + + for (let item of items) { + storage.setItem(item.key, item.value); + } + + info("Returning to event loop"); + + await returnToEventLoop(); + + // 2nd snapshot + + // Remove first two items, add some new items and add the two items back. + + storage.removeItem("key01"); + storage.removeItem("key02"); + + storage.setItem("key06", "value06"); + storage.setItem("key07", "value07"); + storage.setItem("key08", "value08"); + + storage.setItem("key01", "value01"); + storage.setItem("key02", "value02"); + + info("Saving key order"); + + let savedKeys = Object.keys(storage); + + info("Returning to event loop"); + + await returnToEventLoop(); + + // 3rd snapshot + + info("Verifying key order"); + + let keys = Object.keys(storage); + + is(keys.length, savedKeys.length); + + for (let i = 0; i < keys.length; i++) { + is(keys[i], savedKeys[i], "Correct key"); + } +}); diff --git a/dom/localstorage/test/unit/test_originInit.js b/dom/localstorage/test/unit/test_originInit.js new file mode 100644 index 0000000000..48afdf971b --- /dev/null +++ b/dom/localstorage/test/unit/test_originInit.js @@ -0,0 +1,372 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +add_task(async function testSteps() { + const storageDirName = "storage"; + const persistenceTypeDefaultDirName = "default"; + const persistenceTypePersistentDirName = "permanent"; + + const principal = getPrincipal("http://example.com"); + + const originDirName = "http+++example.com"; + + const clientLSDirName = "ls"; + + const dataFile = getRelativeFile( + `${storageDirName}/${persistenceTypeDefaultDirName}/${originDirName}/` + + `${clientLSDirName}/data.sqlite` + ); + + const usageJournalFile = getRelativeFile( + `${storageDirName}/${persistenceTypeDefaultDirName}/${originDirName}/` + + `${clientLSDirName}/usage-journal` + ); + + const usageFile = getRelativeFile( + `${storageDirName}/${persistenceTypeDefaultDirName}/${originDirName}/` + + `${clientLSDirName}/usage` + ); + + const persistentLSDir = getRelativeFile( + `${storageDirName}/${persistenceTypePersistentDirName}/${originDirName}/` + + `${clientLSDirName}` + ); + + const data = {}; + data.key = "key1"; + data.value = "value1"; + data.usage = data.key.length + data.value.length; + + const usageFileCookie = 0x420a420a; + + async function createTestOrigin() { + let storage = getLocalStorage(principal); + + storage.setItem(data.key, data.value); + + let request = reset(); + await requestFinished(request); + } + + async function createPersistentTestOrigin() { + let database = getSimpleDatabase(principal, "persistent"); + + let request = database.open("data"); + await requestFinished(request); + + request = reset(); + await requestFinished(request); + } + + function removeFile(file) { + file.remove(false); + } + + function createEmptyFile(file) { + file.create(Ci.nsIFile.NORMAL_FILE_TYPE, 0o0644); + } + + function createEmptyDirectory(dir) { + dir.create(Ci.nsIFile.DIRECTORY_TYPE, 0o0755); + } + + function getBinaryOutputStream(file) { + var ostream = Cc[ + "@mozilla.org/network/file-output-stream;1" + ].createInstance(Ci.nsIFileOutputStream); + ostream.init(file, -1, parseInt("0644", 8), 0); + + let bstream = Cc["@mozilla.org/binaryoutputstream;1"].createInstance( + Ci.nsIBinaryOutputStream + ); + bstream.setOutputStream(ostream); + + return bstream; + } + + async function initTestOrigin() { + let request = initStorage(); + await requestFinished(request); + + request = initTemporaryStorage(); + await requestFinished(request); + + request = initTemporaryOrigin("default", principal); + await requestFinished(request); + } + + async function initPersistentTestOrigin() { + let request = initStorage(); + await requestFinished(request); + + request = initPersistentOrigin(principal); + await requestFinished(request); + } + + async function checkFiles(wantData, wantUsage) { + let exists = dataFile.exists(); + if (wantData) { + ok(exists, "Data file does exist"); + } else { + ok(!exists, "Data file doesn't exist"); + } + + exists = usageJournalFile.exists(); + ok(!exists, "Usage journal file doesn't exist"); + + exists = usageFile.exists(); + if (wantUsage) { + ok(exists, "Usage file does exist"); + } else { + ok(!exists, "Usage file doesn't exist"); + return; + } + + let usage = await readUsageFromUsageFile(usageFile); + ok(usage == data.usage, "Correct usage"); + } + + async function clearTestOrigin() { + let request = clearOrigin(principal, "default"); + await requestFinished(request); + } + + async function clearPersistentTestOrigin() { + let request = clearOrigin(principal, "persistent"); + await requestFinished(request); + } + + info("Setting prefs"); + + Services.prefs.setBoolPref( + "dom.storage.enable_unsupported_legacy_implementation", + false + ); + + info( + "Stage 1 - " + + "data.sqlite file doesn't exist, " + + "usage-journal file doesn't exist, " + + "any usage file exists" + ); + + await createTestOrigin(); + + removeFile(dataFile); + + await initTestOrigin(); + + await checkFiles(/* wantData */ false, /* wantUsage */ false); + + await clearTestOrigin(); + + info( + "Stage 2 - " + + "data.sqlite file doesn't exist, " + + "any usage-journal file exists, " + + "any usage file exists" + ); + + await createTestOrigin(); + + removeFile(dataFile); + createEmptyFile(usageJournalFile); + + await initTestOrigin(); + + await checkFiles(/* wantData */ false, /* wantUsage */ false); + + await clearTestOrigin(); + + info( + "Stage 3 - " + + "valid data.sqlite file exists, " + + "usage-journal file doesn't exist, " + + "usage file doesn't exist" + ); + + await createTestOrigin(); + + removeFile(usageFile); + + await initTestOrigin(); + + await checkFiles(/* wantData */ true, /* wantUsage */ true); + + await clearTestOrigin(); + + info( + "Stage 4 - " + + "valid data.sqlite file exists, " + + "usage-journal file doesn't exist, " + + "invalid (wrong file size) usage file exists" + ); + + await createTestOrigin(); + + removeFile(usageFile); + createEmptyFile(usageFile); + + await initTestOrigin(); + + await checkFiles(/* wantData */ true, /* wantUsage */ true); + + await clearTestOrigin(); + + info( + "Stage 5 - " + + "valid data.sqlite file exists, " + + "usage-journal file doesn't exist, " + + "invalid (wrong cookie) usage file exists" + ); + + await createTestOrigin(); + + let stream = getBinaryOutputStream(usageFile); + stream.write32(usageFileCookie - 1); + stream.write64(data.usage); + stream.close(); + + await initTestOrigin(); + + await checkFiles(/* wantData */ true, /* wantUsage */ true); + + await clearTestOrigin(); + + info( + "Stage 6 - " + + "valid data.sqlite file exists, " + + "usage-journal file doesn't exist, " + + "valid usage file exists" + ); + + await createTestOrigin(); + + await initTestOrigin(); + + await checkFiles(/* wantData */ true, /* wantUsage */ true); + + await clearTestOrigin(); + + info( + "Stage 7 - " + + "valid data.sqlite file exists, " + + "any usage-journal exists, " + + "usage file doesn't exist" + ); + + await createTestOrigin(); + + createEmptyFile(usageJournalFile); + removeFile(usageFile); + + await initTestOrigin(); + + await checkFiles(/* wantData */ true, /* wantUsage */ true); + + await clearTestOrigin(); + + info( + "Stage 8 - " + + "valid data.sqlite file exists, " + + "any usage-journal exists, " + + "invalid (wrong file size) usage file exists" + ); + + await createTestOrigin(); + + createEmptyFile(usageJournalFile); + removeFile(usageFile); + createEmptyFile(usageFile); + + await initTestOrigin(); + + await checkFiles(/* wantData */ true, /* wantUsage */ true); + + await clearTestOrigin(); + + info( + "Stage 9 - " + + "valid data.sqlite file exists, " + + "any usage-journal exists, " + + "invalid (wrong cookie) usage file exists" + ); + + await createTestOrigin(); + + createEmptyFile(usageJournalFile); + stream = getBinaryOutputStream(usageFile); + stream.write32(usageFileCookie - 1); + stream.write64(data.usage); + stream.close(); + + await initTestOrigin(); + + await checkFiles(/* wantData */ true, /* wantUsage */ true); + + await clearTestOrigin(); + + info( + "Stage 10 - " + + "valid data.sqlite file exists, " + + "any usage-journal exists, " + + "invalid (wrong usage) usage file exists" + ); + + await createTestOrigin(); + + createEmptyFile(usageJournalFile); + stream = getBinaryOutputStream(usageFile); + stream.write32(usageFileCookie); + stream.write64(data.usage - 1); + stream.close(); + + await initTestOrigin(); + + await checkFiles(/* wantData */ true, /* wantUsage */ true); + + await clearTestOrigin(); + + info( + "Stage 11 - " + + "valid data.sqlite file exists, " + + "any usage-journal exists, " + + "valid usage file exists" + ); + + await createTestOrigin(); + + createEmptyFile(usageJournalFile); + + await initTestOrigin(); + + await checkFiles(/* wantData */ true, /* wantUsage */ true); + + await clearTestOrigin(); + + // Verify that InitializeOrigin doesn't fail when a + // storage/permanent/${origin}/ls exists. + info( + "Stage 12 - Testing initialization of ls directory placed in permanent " + + "origin directory" + ); + + await createPersistentTestOrigin(); + + createEmptyDirectory(persistentLSDir); + + try { + await initPersistentTestOrigin(); + + ok(true, "Should not have thrown"); + } catch (ex) { + ok(false, "Should not have thrown"); + } + + let exists = persistentLSDir.exists(); + ok(exists, "ls directory in permanent origin directory does exist"); + + await clearPersistentTestOrigin(); +}); diff --git a/dom/localstorage/test/unit/test_preloading.js b/dom/localstorage/test/unit/test_preloading.js new file mode 100644 index 0000000000..977ab10d99 --- /dev/null +++ b/dom/localstorage/test/unit/test_preloading.js @@ -0,0 +1,84 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +add_task(async function testSteps() { + const principals = [ + getPrincipal("http://example.com", {}), + getPrincipal("http://example.com", { privateBrowsingId: 1 }), + ]; + + async function isPreloaded(principal) { + return Services.domStorageManager.isPreloaded(principal); + } + + info("Setting prefs"); + + Services.prefs.setBoolPref( + "dom.storage.enable_unsupported_legacy_implementation", + false + ); + Services.prefs.setBoolPref("dom.storage.snapshot_reusing", false); + + for (const principal of principals) { + info("Getting storage"); + + let storage = getLocalStorage(principal); + + ok( + !(await isPreloaded(principal)), + "Data is not preloaded after getting storage" + ); + + info("Opening storage"); + + storage.open(); + + ok(await isPreloaded(principal), "Data is preloaded after opening storage"); + + info("Closing storage"); + + storage.close(); + + if (principal.privateBrowsingId > 0) { + ok( + await isPreloaded(principal), + "Data is still preloaded after closing storage" + ); + + info("Closing private session"); + + Services.obs.notifyObservers(null, "last-pb-context-exited"); + + ok( + !(await isPreloaded(principal)), + "Data is not preloaded anymore after closing private session" + ); + } else { + ok( + !(await isPreloaded(principal)), + "Data is not preloaded anymore after closing storage" + ); + } + + info("Opening storage again"); + + storage.open(); + + ok( + await isPreloaded(principal), + "Data is preloaded after opening storage again" + ); + + info("Clearing origin"); + + let request = clearOrigin(principal, "default"); + await requestFinished(request); + + ok( + !(await isPreloaded(principal)), + "Data is not preloaded after clearing origin" + ); + } +}); diff --git a/dom/localstorage/test/unit/test_schema3upgrade.js b/dom/localstorage/test/unit/test_schema3upgrade.js new file mode 100644 index 0000000000..4b851642ea --- /dev/null +++ b/dom/localstorage/test/unit/test_schema3upgrade.js @@ -0,0 +1,39 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +add_task(async function testSteps() { + const url = "http://example.com"; + + info("Setting pref"); + + Services.prefs.setBoolPref("dom.storage.next_gen", true); + + info("Clearing"); + + let request = clear(); + await requestFinished(request); + + info("Installing package"); + + // The profile contains one initialized origin directory with local + // storage data, local storage archive, a script for origin initialization, + // the storage database and the web apps store database: + // - storage/default/http+++example.com + // - storage/ls-archive.sqlite + // - create_db.js + // - storage.sqlite + // - webappsstore.sqlite + // The file create_db.js in the package was run locally (with a build with + // local storage archive version 1 and database schema version 2), + // specifically it was temporarily added to xpcshell.ini and then executed: + // mach xpcshell-test --interactive dom/localstorage/test/unit/create_db.js + // Note: to make it become the profile in the test, additional manual steps + // are needed. + // 1. Remove the folder "storage/temporary". + installPackage("schema3upgrade_profile"); + + let storage = getLocalStorage(getPrincipal(url)); + storage.open(); +}); diff --git a/dom/localstorage/test/unit/test_schema4upgrade.js b/dom/localstorage/test/unit/test_schema4upgrade.js new file mode 100644 index 0000000000..a6c308af35 --- /dev/null +++ b/dom/localstorage/test/unit/test_schema4upgrade.js @@ -0,0 +1,39 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +add_task(async function testSteps() { + const url = "http://example.com"; + + info("Setting pref"); + + Services.prefs.setBoolPref("dom.storage.next_gen", true); + + info("Clearing"); + + let request = clear(); + await requestFinished(request); + + info("Installing package"); + + // The profile contains one initialized origin directory with local + // storage data, local storage archive, a script for origin initialization, + // the storage database and the web apps store database: + // - storage/default/http+++example.com + // - storage/ls-archive.sqlite + // - storage.sqlite + // - test_create_db.js + // - webappsstore.sqlite + // + // The file test_create_db.js in the package was run locally by + // adding it temporarily to xpcshell.ini and then executed with + // mach xpcshell-test --headless dom/localstorage/test/unit/test_create_db.js + // Note: to make it become the profile in the test, additional manual steps + // are needed. + // 1. Remove the folder "storage/temporary". + installPackage("schema4upgrade_profile"); + + let storage = getLocalStorage(getPrincipal(url)); + storage.open(); +}); diff --git a/dom/localstorage/test/unit/test_snapshotting.js b/dom/localstorage/test/unit/test_snapshotting.js new file mode 100644 index 0000000000..4b639395f7 --- /dev/null +++ b/dom/localstorage/test/unit/test_snapshotting.js @@ -0,0 +1,330 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +add_task(async function testSteps() { + const url = "http://example.com"; + + info("Setting pref"); + + Services.prefs.setBoolPref("dom.storage.snapshot_reusing", false); + + const items = [ + { key: "key01", value: "value01" }, + { key: "key02", value: "value02" }, + { key: "key03", value: "value03" }, + { key: "key04", value: "value04" }, + { key: "key05", value: "value05" }, + { key: "key06", value: "value06" }, + { key: "key07", value: "value07" }, + { key: "key08", value: "value08" }, + { key: "key09", value: "value09" }, + { key: "key10", value: "value10" }, + ]; + + let sizeOfOneKey; + let sizeOfOneValue; + let sizeOfOneItem; + let sizeOfKeys = 0; + let sizeOfItems = 0; + + for (let i = 0; i < items.length; i++) { + let item = items[i]; + let sizeOfKey = item.key.length; + let sizeOfValue = item.value.length; + let sizeOfItem = sizeOfKey + sizeOfValue; + if (i == 0) { + sizeOfOneKey = sizeOfKey; + sizeOfOneValue = sizeOfValue; + sizeOfOneItem = sizeOfItem; + } + sizeOfKeys += sizeOfKey; + sizeOfItems += sizeOfItem; + } + + info("Size of one key is " + sizeOfOneKey); + info("Size of one value is " + sizeOfOneValue); + info("Size of one item is " + sizeOfOneItem); + info("Size of keys is " + sizeOfKeys); + info("Size of items is " + sizeOfItems); + + const prefillValues = [ + // Zero prefill (prefill disabled) + 0, + // Less than one key length prefill + sizeOfOneKey - 1, + // Greater than one key length and less than one item length prefill + sizeOfOneKey + 1, + // Precisely one item length prefill + sizeOfOneItem, + // Precisely two times one item length prefill + 2 * sizeOfOneItem, + // Precisely three times one item length prefill + 3 * sizeOfOneItem, + // Precisely four times one item length prefill + 4 * sizeOfOneItem, + // Precisely size of keys prefill + sizeOfKeys, + // Less than size of keys plus one value length prefill + sizeOfKeys + sizeOfOneValue - 1, + // Precisely size of keys plus one value length prefill + sizeOfKeys + sizeOfOneValue, + // Greater than size of keys plus one value length and less than size of + // keys plus two times one value length prefill + sizeOfKeys + sizeOfOneValue + 1, + // Precisely size of keys plus two times one value length prefill + sizeOfKeys + 2 * sizeOfOneValue, + // Precisely size of keys plus three times one value length prefill + sizeOfKeys + 3 * sizeOfOneValue, + // Precisely size of keys plus four times one value length prefill + sizeOfKeys + 4 * sizeOfOneValue, + // Precisely size of keys plus five times one value length prefill + sizeOfKeys + 5 * sizeOfOneValue, + // Precisely size of keys plus six times one value length prefill + sizeOfKeys + 6 * sizeOfOneValue, + // Precisely size of keys plus seven times one value length prefill + sizeOfKeys + 7 * sizeOfOneValue, + // Precisely size of keys plus eight times one value length prefill + sizeOfKeys + 8 * sizeOfOneValue, + // Precisely size of keys plus nine times one value length prefill + sizeOfKeys + 9 * sizeOfOneValue, + // Precisely size of items prefill + sizeOfItems, + // Unlimited prefill + -1, + ]; + + for (let prefillValue of prefillValues) { + info("Setting prefill value to " + prefillValue); + + Services.prefs.setIntPref("dom.storage.snapshot_prefill", prefillValue); + + const gradualPrefillValues = [ + // Zero gradual prefill + 0, + // Less than one key length gradual prefill + sizeOfOneKey - 1, + // Greater than one key length and less than one item length gradual + // prefill + sizeOfOneKey + 1, + // Precisely one item length gradual prefill + sizeOfOneItem, + // Precisely two times one item length gradual prefill + 2 * sizeOfOneItem, + // Precisely three times one item length gradual prefill + 3 * sizeOfOneItem, + // Precisely four times one item length gradual prefill + 4 * sizeOfOneItem, + // Precisely five times one item length gradual prefill + 5 * sizeOfOneItem, + // Precisely six times one item length gradual prefill + 6 * sizeOfOneItem, + // Precisely seven times one item length gradual prefill + 7 * sizeOfOneItem, + // Precisely eight times one item length gradual prefill + 8 * sizeOfOneItem, + // Precisely nine times one item length gradual prefill + 9 * sizeOfOneItem, + // Precisely size of items prefill + sizeOfItems, + // Unlimited gradual prefill + -1, + ]; + + for (let gradualPrefillValue of gradualPrefillValues) { + info("Setting gradual prefill value to " + gradualPrefillValue); + + Services.prefs.setIntPref( + "dom.storage.snapshot_gradual_prefill", + gradualPrefillValue + ); + + info("Getting storage"); + + let storage = getLocalStorage(getPrincipal(url)); + + // 1st snapshot + + info("Adding data"); + + for (let item of items) { + storage.setItem(item.key, item.value); + } + + info("Saving key order"); + + // This forces GetKeys to be called internally. + let savedKeys = Object.keys(storage); + + // GetKey should match GetKeys + for (let i = 0; i < savedKeys.length; i++) { + is(storage.key(i), savedKeys[i], "Correct key"); + } + + info("Returning to event loop"); + + // Returning to event loop forces the internal snapshot to finish. + await returnToEventLoop(); + + // 2nd snapshot + + info("Verifying length"); + + is(storage.length, items.length, "Correct length"); + + info("Verifying key order"); + + let keys = Object.keys(storage); + + is(keys.length, savedKeys.length); + + for (let i = 0; i < keys.length; i++) { + is(keys[i], savedKeys[i], "Correct key"); + } + + info("Verifying values"); + + for (let item of items) { + is(storage.getItem(item.key), item.value, "Correct value"); + } + + info("Returning to event loop"); + + await returnToEventLoop(); + + // 3rd snapshot + + // Force key2 to load. + storage.getItem("key02"); + + // Fill out write infos a bit. + storage.removeItem("key05"); + storage.setItem("key05", "value05"); + storage.removeItem("key05"); + storage.setItem("key11", "value11"); + storage.setItem("key05", "value05"); + + items.push({ key: "key11", value: "value11" }); + + info("Verifying length"); + + is(storage.length, items.length, "Correct length"); + + // This forces to get all keys from the parent and then apply write infos + // on already cached values. + savedKeys = Object.keys(storage); + + info("Verifying values"); + + for (let item of items) { + is(storage.getItem(item.key), item.value, "Correct value"); + } + + storage.removeItem("key11"); + + items.pop(); + + info("Returning to event loop"); + + await returnToEventLoop(); + + // 4th snapshot + + // Force loading of all items. + info("Verifying length"); + + is(storage.length, items.length, "Correct length"); + + info("Verifying values"); + + for (let item of items) { + is(storage.getItem(item.key), item.value, "Correct value"); + } + + is(storage.getItem("key11"), null, "Correct value"); + + info("Returning to event loop"); + + await returnToEventLoop(); + + // 5th snapshot + + // Force loading of all keys. + info("Saving key order"); + + savedKeys = Object.keys(storage); + + // Force loading of all items. + info("Verifying length"); + + is(storage.length, items.length, "Correct length"); + + info("Verifying values"); + + for (let item of items) { + is(storage.getItem(item.key), item.value, "Correct value"); + } + + is(storage.getItem("key11"), null, "Correct value"); + + info("Returning to event loop"); + + await returnToEventLoop(); + + // 6th snapshot + info("Verifying unknown item"); + + is(storage.getItem("key11"), null, "Correct value"); + + info("Verifying unknown item again"); + + is(storage.getItem("key11"), null, "Correct value"); + + info("Returning to event loop"); + + await returnToEventLoop(); + + // 7th snapshot + + // Save actual key order. + info("Saving key order"); + + savedKeys = Object.keys(storage); + + await returnToEventLoop(); + + // 8th snapshot + + // Force loading of all items, but in reverse order. + info("Getting values"); + + for (let i = items.length - 1; i >= 0; i--) { + let item = items[i]; + storage.getItem(item.key); + } + + info("Verifying key order"); + + keys = Object.keys(storage); + + is(keys.length, savedKeys.length); + + for (let i = 0; i < keys.length; i++) { + is(keys[i], savedKeys[i], "Correct key"); + } + + await returnToEventLoop(); + + // 9th snapshot + + info("Clearing"); + + storage.clear(); + + info("Returning to event loop"); + + await returnToEventLoop(); + } + } +}); diff --git a/dom/localstorage/test/unit/test_stringLength.js b/dom/localstorage/test/unit/test_stringLength.js new file mode 100644 index 0000000000..52ee59f7d6 --- /dev/null +++ b/dom/localstorage/test/unit/test_stringLength.js @@ -0,0 +1,74 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +add_task(async function testSteps() { + const principal = getPrincipal("http://example.org"); + + const data = {}; + data.key = "foobar"; + data.secondKey = "foobaz"; + data.value = { + length: 25637, + }; + data.usage = data.key.length + data.value.length; + + async function checkUsage(expectedUsage) { + info("Checking usage"); + + // This forces any pending changes to be flushed to disk. It also forces + // data to be reloaded from disk at next localStorage API call. + request = resetOrigin(principal); + await requestFinished(request); + + request = getOriginUsage(principal); + await requestFinished(request); + + is(request.result.usage, expectedUsage, "Correct usage"); + } + + info("Setting pref"); + + Services.prefs.setBoolPref( + "dom.storage.enable_unsupported_legacy_implementation", + false + ); + + info("Stage 1 - Checking usage after profile installation"); + + info("Clearing"); + + let request = clear(); + await requestFinished(request); + + info("Installing package"); + + // The profile contains storage.sqlite and webappsstore.sqlite. + installPackage("stringLength_profile"); + + await checkUsage(0); + + info("Stage 2 - Checking usage after archived data migration"); + + info("Opening database"); + + let storage = getLocalStorage(principal); + storage.open(); + + await checkUsage(data.usage); + + info("Stage 3 - Checking usage after copying the value"); + + info("Adding a second copy of the value"); + + let value = storage.getItem(data.key); + storage.setItem(data.secondKey, value); + + await checkUsage(2 * data.usage); + + info("Stage 4 - Checking length of the copied value"); + + value = storage.getItem(data.secondKey); + ok(value.length === data.value.length, "Correct string length"); +}); diff --git a/dom/localstorage/test/unit/test_stringLength2.js b/dom/localstorage/test/unit/test_stringLength2.js new file mode 100644 index 0000000000..f1a1a902cf --- /dev/null +++ b/dom/localstorage/test/unit/test_stringLength2.js @@ -0,0 +1,79 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/** + * This test is mainly to verify that string length is correctly computed for + * database values containing NULs. See bug 1541681. + */ + +add_task(async function testSteps() { + const principal = getPrincipal("http://example.org"); + + const data = {}; + data.key = "foobar"; + data.secondKey = "foobaz"; + data.value = { + length: 19253, + }; + data.usage = data.key.length + data.value.length; + + async function checkUsage(expectedUsage) { + info("Checking usage"); + + // This forces any pending changes to be flushed to disk. It also forces + // data to be reloaded from disk at next localStorage API call. + request = resetOrigin(principal); + await requestFinished(request); + + request = getOriginUsage(principal); + await requestFinished(request); + + is(request.result.usage, expectedUsage, "Correct usage"); + } + + info("Setting pref"); + + Services.prefs.setBoolPref( + "dom.storage.enable_unsupported_legacy_implementation", + false + ); + + info("Stage 1 - Checking usage after profile installation"); + + info("Clearing"); + + let request = clear(); + await requestFinished(request); + + info("Installing package"); + + // The profile contains storage.sqlite and webappsstore.sqlite. + installPackage("stringLength2_profile"); + + await checkUsage(0); + + info("Stage 2 - Checking usage after archived data migration"); + + info("Opening database"); + + let storage = getLocalStorage(principal); + storage.open(); + + await checkUsage(data.usage); + + info("Stage 3 - Checking usage after copying the value"); + + info("Adding a second copy of the value"); + + let value = storage.getItem(data.key); + storage.setItem(data.secondKey, value); + + await checkUsage(2 * data.usage); + + info("Stage 4 - Checking length of the copied value"); + + value = storage.getItem(data.secondKey); + ok(value.length === data.value.length, "Correct string length"); +}); diff --git a/dom/localstorage/test/unit/test_unicodeCharacters.js b/dom/localstorage/test/unit/test_unicodeCharacters.js new file mode 100644 index 0000000000..9e48274161 --- /dev/null +++ b/dom/localstorage/test/unit/test_unicodeCharacters.js @@ -0,0 +1,205 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +const interpretChar = (chars, index) => { + return chars + .charCodeAt(index) + .toString(16) + .padStart(4, "0"); +}; + +const hexEncode = str => { + let result = ""; + const len = str.length; + for (let i = 0; i < len; ++i) { + result += interpretChar(str, i); + } + return result; +}; + +const collectCorrupted = (expected, actual) => { + const len = Math.min(expected.length, actual.length); + let notEquals = []; + for (let i = 0; i < len; ++i) { + if (expected[i] !== actual[i]) { + notEquals.push([hexEncode(expected[i]), hexEncode(actual[i])]); + } + } + return notEquals; +}; + +const sanitizeOutputWithSurrogates = (testValue, prefix = "") => { + let utf8What = prefix; + for (let i = 0; i < testValue.length; ++i) { + const valueChar = testValue.charCodeAt(i); + const isPlanar = 0xd800 <= valueChar && valueChar <= 0xdfff; + utf8What += isPlanar ? "\\u" + interpretChar(testValue, i) : testValue[i]; + } + return utf8What; +}; + +const getEncodingSample = () => { + const expectedSample = + "3681207208613504e0a5028800b945551988c60050008027ebc2808c00d38e806e03d8210ac906722b85499be9d00000"; + + let result = ""; + const len = expectedSample.length; + for (let i = 0; i < len; i += 4) { + result += String.fromCharCode(parseInt(expectedSample.slice(i, i + 4), 16)); + } + return result; +}; + +const getSeparatedBasePlane = () => { + let result = ""; + for (let i = 0xffff; i >= 0; --i) { + result += String.fromCharCode(i) + "\n"; + } + return result; +}; + +const getJoinedBasePlane = () => { + let result = ""; + for (let i = 0; i <= 0xffff; ++i) { + result += String.fromCharCode(i); + } + return result; +}; + +const getSurrogateCombinations = () => { + const upperLead = String.fromCharCode(0xdbff); + const lowerTrail = String.fromCharCode(0xdc00); + + const regularSlot = ["w", "abcdefghijklmnopqrst", "aaaaaaaaaaaaaaaaaaaa", ""]; + const surrogateSlot = [lowerTrail, upperLead]; + + let samples = []; + for (const leadSnippet of regularSlot) { + for (const firstSlot of surrogateSlot) { + for (const trailSnippet of regularSlot) { + for (const secondSlot of surrogateSlot) { + samples.push(leadSnippet + firstSlot + secondSlot + trailSnippet); + } + samples.push(leadSnippet + firstSlot + trailSnippet); + } + } + } + + return samples; +}; + +const fetchFrom = async (itemKey, sample, meanwhile) => { + const principal = getPrincipal("http://example.com/", {}); + + let request = clearOrigin(principal); + await requestFinished(request); + + const storage = getLocalStorage(principal); + + await storage.setItem(itemKey, sample); + + await meanwhile(principal); + + return storage.getItem(itemKey); +}; + +/** + * Value fetched from existing snapshot based on + * existing in-memory datastore in the parent process + * without any communication between content/parent + */ +const fetchFromExistingSnapshotExistingDatastore = async (itemKey, sample) => { + return fetchFrom(itemKey, sample, async () => {}); +}; + +/** + * Value fetched from newly created snapshot based on + * existing in-memory datastore in the parent process + */ +const fetchFromNewSnapshotExistingDatastore = async (itemKey, sample) => { + return fetchFrom(itemKey, sample, async () => { + await returnToEventLoop(); + }); +}; + +/** + * Value fetched from newly created snapshot based on newly created + * in-memory datastore based on database in the parent process + */ +const fetchFromNewSnapshotNewDatastore = async (itemKey, sample) => { + return fetchFrom(itemKey, sample, async principal => { + let request = resetOrigin(principal); + await requestFinished(request); + }); +}; + +add_task(async function testSteps() { + /* This test is based on bug 1681300 */ + Services.prefs.setBoolPref( + "dom.storage.enable_unsupported_legacy_implementation", + false + ); + Services.prefs.setBoolPref("dom.storage.snapshot_reusing", false); + + const reportWhat = (testKey, testValue) => { + if (testKey.length + testValue.length > 82) { + return testKey; + } + return sanitizeOutputWithSurrogates(testValue, /* prefix */ testKey + ":"); + }; + + const testFetchMode = async (testType, storeAndLookup) => { + const testPairs = [ + { testEmptyValue: [""] }, + { testSampleKey: [getEncodingSample()] }, + { testSeparatedKey: [getSeparatedBasePlane()] }, + { testJoinedKey: [getJoinedBasePlane()] }, + { testCombinations: getSurrogateCombinations() }, + ]; + + for (const testPair of testPairs) { + for (const [testKey, expectedValues] of Object.entries(testPair)) { + for (const expected of expectedValues) { + const actual = await storeAndLookup(testKey, expected); + const testInfo = reportWhat(testKey, expected); + is( + null != actual, + true, + testType + ": Value not null for " + testInfo + ); + is( + expected.length, + actual.length, + testType + ": Returned size for " + testInfo + ); + + const notEquals = collectCorrupted(expected, actual); + for (let i = 0; i < notEquals.length; ++i) { + is( + notEquals[i][0], + notEquals[i][1], + testType + ": Unequal character at " + i + " for " + testInfo + ); + } + } + } + } + }; + + await testFetchMode( + "ExistingSnapshotExistingDatastore", + fetchFromExistingSnapshotExistingDatastore + ); + + await testFetchMode( + "NewSnapshotExistingDatastore", + fetchFromNewSnapshotExistingDatastore + ); + + await testFetchMode( + "NewSnapshotNewDatastore", + fetchFromNewSnapshotNewDatastore + ); +}); diff --git a/dom/localstorage/test/unit/test_uri_encoding_edge_cases.js b/dom/localstorage/test/unit/test_uri_encoding_edge_cases.js new file mode 100644 index 0000000000..0da4e8584d --- /dev/null +++ b/dom/localstorage/test/unit/test_uri_encoding_edge_cases.js @@ -0,0 +1,69 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/** + * This test verifies that group and origin strings for URIs with special + * characters are consistent between calling + * EnsureQuotaForOringin/EnsureOriginIsInitailized and GetQuotaObject in + * PrepareDatastoreOp, so writing to local storage won't cause a crash because + * of a null quota object. See bug 1516333. + */ + +add_task(async function testSteps() { + /** + * The edge cases are specified in this array of origins. Each edge case must + * contain two properties uri and path (origin directory path relative to the + * profile directory). + */ + const origins = [ + { + uri: "file:///test'.html", + path: "storage/default/file++++test'.html", + }, + { + uri: "file:///test>.html", + path: "storage/default/file++++test%3E.html", + }, + ]; + + info("Setting prefs"); + + Services.prefs.setBoolPref( + "dom.storage.enable_unsupported_legacy_implementation", + false + ); + + for (let origin of origins) { + const principal = getPrincipal(origin.uri); + + let originDir = getRelativeFile(origin.path); + + info("Checking the origin directory existence"); + + ok( + !originDir.exists(), + `The origin directory ${origin.path} should not exists` + ); + + info("Getting storage"); + + let storage = getLocalStorage(principal); + + info("Adding item"); + + storage.setItem("foo", "bar"); + + info("Resetting origin"); + + // This forces any pending changes to be flushed to disk (including origin + // directory creation). + let request = resetOrigin(principal); + await requestFinished(request); + + info("Checking the origin directory existence"); + + ok(originDir.exists(), `The origin directory ${origin.path} should exist`); + } +}); diff --git a/dom/localstorage/test/unit/test_usage.js b/dom/localstorage/test/unit/test_usage.js new file mode 100644 index 0000000000..552a45e4a6 --- /dev/null +++ b/dom/localstorage/test/unit/test_usage.js @@ -0,0 +1,69 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +add_task(async function testSteps() { + const data = {}; + data.key = "key1"; + data.value = "value1"; + data.usage = data.key.length + data.value.length; + + const principal = getPrincipal("http://example.com"); + + info("Setting prefs"); + + Services.prefs.setBoolPref( + "dom.storage.enable_unsupported_legacy_implementation", + false + ); + + info("Stage 1 - Testing usage after adding item"); + + info("Getting storage"); + + let storage = getLocalStorage(principal); + + info("Adding item"); + + storage.setItem(data.key, data.value); + + info("Resetting origin"); + + let request = resetOrigin(principal); + await requestFinished(request); + + info("Getting usage"); + + request = getOriginUsage(principal); + await requestFinished(request); + + is(request.result.usage, data.usage, "Correct usage"); + + info("Resetting"); + + request = reset(); + await requestFinished(request); + + info("Stage 2 - Testing usage after removing item"); + + info("Getting storage"); + + storage = getLocalStorage(principal); + + info("Removing item"); + + storage.removeItem(data.key); + + info("Resetting origin"); + + request = resetOrigin(principal); + await requestFinished(request); + + info("Getting usage"); + + request = getOriginUsage(principal); + await requestFinished(request); + + is(request.result.usage, 0, "Correct usage"); +}); diff --git a/dom/localstorage/test/unit/test_usageAfterMigration.js b/dom/localstorage/test/unit/test_usageAfterMigration.js new file mode 100644 index 0000000000..a0bd5efd5b --- /dev/null +++ b/dom/localstorage/test/unit/test_usageAfterMigration.js @@ -0,0 +1,164 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +add_task(async function testSteps() { + const principal = getPrincipal("http://example.com"); + + const dataFile = getRelativeFile( + "storage/default/http+++example.com/ls/data.sqlite" + ); + + const usageJournalFile = getRelativeFile( + "storage/default/http+++example.com/ls/usage-journal" + ); + + const usageFile = getRelativeFile( + "storage/default/http+++example.com/ls/usage" + ); + + const data = {}; + data.key = "foo"; + data.value = "bar"; + data.usage = data.key.length + data.value.length; + + async function createStorageForMigration(createUsageDir) { + info("Clearing"); + + let request = clear(); + await requestFinished(request); + + info("Installing package"); + + // The profile contains storage.sqlite and webappsstore.sqlite. The file + // create_db.js in the package was run locally, specifically it was + // temporarily added to xpcshell.ini and then executed: + // mach xpcshell-test --interactive dom/localstorage/test/unit/create_db.js + installPackage("usageAfterMigration_profile"); + + if (createUsageDir) { + // Origin must be initialized before the usage dir is created. + + info("Initializing storage"); + + request = initStorage(); + await requestFinished(request); + + info("Initializing temporary storage"); + + request = initTemporaryStorage(); + await requestFinished(request); + + info("Initializing origin"); + + request = initTemporaryOrigin("default", principal); + await requestFinished(request); + + info("Creating usage as a directory"); + + // This will cause a failure during migration. + usageFile.create(Ci.nsIFile.DIRECTORY_TYPE, parseInt("0755", 8)); + } + } + + function verifyData() { + ok(dataFile.exists(), "Data file does exist"); + } + + async function verifyUsage(success) { + info("Verifying usage in memory"); + + let request = getOriginUsage(principal, /* fromMemory */ true); + await requestFinished(request); + + if (success) { + is(request.result.usage, data.usage, "Correct usage"); + } else { + is(request.result.usage, 0, "Zero usage"); + } + + info("Verifying usage on disk"); + + if (success) { + ok(!usageJournalFile.exists(), "Usage journal file doesn't exist"); + ok(usageFile.exists(), "Usage file does exist"); + let usage = await readUsageFromUsageFile(usageFile); + is(usage, data.usage, "Correct usage"); + } else { + ok(usageJournalFile.exists(), "Usage journal file does exist"); + ok(usageFile.exists(), "Usage file does exist"); + } + } + + info("Setting prefs"); + + Services.prefs.setBoolPref( + "dom.storage.enable_unsupported_legacy_implementation", + false + ); + + info("Stage 1 - Testing usage after successful data migration"); + + await createStorageForMigration(/* createUsageDir */ false); + + info("Getting storage"); + + let storage = getLocalStorage(principal); + + info("Opening"); + + storage.open(); + + verifyData(); + + await verifyUsage(/* success */ true); + + info("Stage 2 - Testing usage after unsuccessful data migration"); + + await createStorageForMigration(/* createUsageDir */ true); + + info("Getting storage"); + + storage = getLocalStorage(principal); + + info("Opening"); + + try { + storage.open(); + ok(false, "Should have thrown"); + } catch (ex) { + ok(true, "Did throw"); + } + + verifyData(); + + await verifyUsage(/* success */ false); + + info("Stage 3 - Testing usage after unsuccessful/successful data migration"); + + await createStorageForMigration(/* createUsageDir */ true); + + info("Getting storage"); + + storage = getLocalStorage(principal); + + info("Opening"); + + try { + storage.open(); + ok(false, "Should have thrown"); + } catch (ex) { + ok(true, "Did throw"); + } + + usageFile.remove(true); + + info("Opening"); + + storage.open(); + + verifyData(); + + await verifyUsage(/* success */ true); +}); diff --git a/dom/localstorage/test/unit/usageAfterMigration_profile.zip b/dom/localstorage/test/unit/usageAfterMigration_profile.zip Binary files differnew file mode 100644 index 0000000000..30a73292c3 --- /dev/null +++ b/dom/localstorage/test/unit/usageAfterMigration_profile.zip diff --git a/dom/localstorage/test/unit/xpcshell.ini b/dom/localstorage/test/unit/xpcshell.ini new file mode 100644 index 0000000000..9b86f34349 --- /dev/null +++ b/dom/localstorage/test/unit/xpcshell.ini @@ -0,0 +1,73 @@ +# 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/. + +[DEFAULT] +head = head.js +tags = condprof +support-files = + archive_profile.zip + corruptedDatabase_profile.zip + corruptedDatabase_missingUsageFile_profile.zip + groupMismatch_profile.zip + migration_profile.zip + schema3upgrade_profile.zip + schema4upgrade_profile.zip + stringLength2_profile.zip + stringLength_profile.zip + usageAfterMigration_profile.zip + +[make_migration_emptyValue.js] +skip-if = true # Only used for recreating migration_emptyValue_profile.zip +[test_archive.js] +[test_clientValidation.js] +[test_corruptedDatabase.js] +[test_databaseShadowing1.js] +prefs = + dom.storage.shadow_writes=true +run-sequentially = test_databaseShadowing2.js depends on a file produced by this test +[test_databaseShadowing2.js] +run-sequentially = this test depends on a file produced by test_databaseShadowing1.js +[test_databaseShadowing_clearOrigin1.js] +prefs = + dom.storage.shadow_writes=true +run-sequentially = test_databaseShadowing_clearOrigin2.js depends on a file produced by this test +[test_databaseShadowing_clearOrigin2.js] +run-sequentially = this test depends on a file produced by test_databaseShadowing_clearOrigin1.js +[test_databaseShadowing_clearOriginsByPattern1.js] +prefs = + dom.storage.shadow_writes=true +run-sequentially = test_databaseShadowing_clearOriginsByPattern2.js depends on a file produced by this test +[test_databaseShadowing_clearOriginsByPattern2.js] +run-sequentially = this test depends on a file produced by test_databaseShadowing_clearOriginsByPattern1.js +[test_databaseShadowing_clearOriginsByPrefix1.js] +prefs = + dom.storage.shadow_writes=true +run-sequentially = test_databaseShadowing_clearOriginsByPrefix2.js depends on a file produced by this test +[test_databaseShadowing_clearOriginsByPrefix2.js] +run-sequentially = this test depends on a file produced by test_databaseShadowing_clearOriginsByPrefix1.js +[test_eviction.js] +[test_flushing.js] +[test_groupLimit.js] +[test_groupMismatch.js] +[test_largeItems.js] +[test_lsng_enabled.js] +[test_migration.js] +[test_migration_emptyValue.js] +support-files = + migration_emptyValue_profile.zip +[test_old_lsng_pref.js] +[test_orderingAfterRemoveAdd.js] +[test_originInit.js] +[test_preloading.js] +[test_schema3upgrade.js] +[test_schema4upgrade.js] +[test_snapshotting.js] +skip-if = tsan # Unreasonably slow, bug 1612707 +requesttimeoutfactor = 4 +[test_stringLength.js] +[test_stringLength2.js] +[test_unicodeCharacters.js] +[test_uri_encoding_edge_cases.js] +[test_usage.js] +[test_usageAfterMigration.js] |