diff options
Diffstat (limited to '')
48 files changed, 2958 insertions, 0 deletions
diff --git a/dom/localstorage/test/.eslintrc.js b/dom/localstorage/test/.eslintrc.js new file mode 100644 index 0000000000..735f687ed1 --- /dev/null +++ b/dom/localstorage/test/.eslintrc.js @@ -0,0 +1,5 @@ +"use strict"; + +module.exports = { + extends: ["plugin:mozilla/mochitest-test", "plugin:mozilla/browser-test"], +}; diff --git a/dom/localstorage/test/browser.ini b/dom/localstorage/test/browser.ini new file mode 100644 index 0000000000..8119342ecb --- /dev/null +++ b/dom/localstorage/test/browser.ini @@ -0,0 +1,6 @@ +[DEFAULT] +skip-if = (buildapp != "browser") +support-files = + page_private_ls.html + +[browser_private_ls.js] diff --git a/dom/localstorage/test/browser_private_ls.js b/dom/localstorage/test/browser_private_ls.js new file mode 100644 index 0000000000..d4708c8e23 --- /dev/null +++ b/dom/localstorage/test/browser_private_ls.js @@ -0,0 +1,44 @@ +/** + * This test is mainly to verify that datastores are cleared when the last + * private browsing context exited. + */ + +async function lsCheckFunc() { + let storage = content.localStorage; + + if (storage.length) { + return false; + } + + // Store non-ASCII value to verify bug 1552428. + storage.setItem("foo", "úžasné"); + + return true; +} + +function checkTabWindowLS(tab) { + return SpecialPowers.spawn(tab.linkedBrowser, [], lsCheckFunc); +} + +add_task(async function() { + const pageUrl = + "http://example.com/browser/dom/localstorage/test/page_private_ls.html"; + + for (let i = 0; i < 2; i++) { + let privateWin = await BrowserTestUtils.openNewBrowserWindow({ + private: true, + }); + + let privateTab = await BrowserTestUtils.openNewForegroundTab( + privateWin.gBrowser, + pageUrl + ); + + ok( + await checkTabWindowLS(privateTab), + "LS works correctly in a private-browsing page." + ); + + await BrowserTestUtils.closeWindow(privateWin); + } +}); diff --git a/dom/localstorage/test/gtest/TestLocalStorage.cpp b/dom/localstorage/test/gtest/TestLocalStorage.cpp new file mode 100644 index 0000000000..6c5621f248 --- /dev/null +++ b/dom/localstorage/test/gtest/TestLocalStorage.cpp @@ -0,0 +1,140 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "gtest/gtest.h" +#include "mozilla/NullPrincipal.h" +#include "mozilla/dom/LocalStorageCommon.h" +#include "mozilla/dom/StorageUtils.h" +#include "mozilla/ipc/BackgroundUtils.h" +#include "mozilla/ipc/PBackgroundSharedTypes.h" +#include "nsCOMPtr.h" +#include "nsContentUtils.h" +#include "nsIURI.h" +#include "nsNetUtil.h" + +using namespace mozilla; +using namespace mozilla::dom; +using namespace mozilla::dom::StorageUtils; +using namespace mozilla::ipc; + +namespace { + +struct OriginKeyTest { + const char* mSpec; + const char* mOriginKey; + const char* mQuotaKey; +}; + +already_AddRefed<nsIPrincipal> GetContentPrincipal(const char* aSpec) { + nsCOMPtr<nsIURI> uri; + nsresult rv = NS_NewURI(getter_AddRefs(uri), nsDependentCString(aSpec)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return nullptr; + } + + OriginAttributes attrs; + + nsCOMPtr<nsIPrincipal> principal = + BasePrincipal::CreateContentPrincipal(uri, attrs); + + return principal.forget(); +} + +void CheckGeneratedOriginKey(nsIPrincipal* aPrincipal, const char* aOriginKey, + const char* aQuotaKey) { + nsCString originAttrSuffix; + nsCString originKey; + nsCString quotaKey; + + aPrincipal->OriginAttributesRef().CreateSuffix(originAttrSuffix); + + nsresult rv = aPrincipal->GetStorageOriginKey(originKey); + if (aOriginKey) { + ASSERT_EQ(rv, NS_OK) << "GetStorageOriginKey should not fail"; + EXPECT_TRUE(originKey == nsDependentCString(aOriginKey)); + } else { + ASSERT_NE(rv, NS_OK) << "GetStorageOriginKey should fail"; + } + + rv = aPrincipal->GetLocalStorageQuotaKey(quotaKey); + if (aQuotaKey) { + ASSERT_EQ(rv, NS_OK) << "GetLocalStorageQuotaKey should not fail"; + EXPECT_TRUE(quotaKey == nsDependentCString(aQuotaKey)); + } else { + ASSERT_NE(rv, NS_OK) << "GetLocalStorageQuotaKey should fail"; + } + + PrincipalInfo principalInfo; + rv = PrincipalToPrincipalInfo(aPrincipal, &principalInfo); + ASSERT_EQ(rv, NS_OK) << "PrincipalToPrincipalInfo should not fail"; + + const auto res = GenerateOriginKey2(principalInfo); + if (aOriginKey) { + ASSERT_TRUE(res.isOk()) + << "GenerateOriginKey2 should not fail"; + EXPECT_TRUE(res.inspect().second == nsDependentCString(aOriginKey)); + } else { + ASSERT_TRUE(res.isErr()) + << "GenerateOriginKey2 should fail"; + } +} + +} // namespace + +TEST(LocalStorage, OriginKey) +{ + // Check the system principal. + nsCOMPtr<nsIScriptSecurityManager> secMan = + nsContentUtils::GetSecurityManager(); + ASSERT_TRUE(secMan) + << "GetSecurityManager() should not fail"; + + nsCOMPtr<nsIPrincipal> principal; + secMan->GetSystemPrincipal(getter_AddRefs(principal)); + ASSERT_TRUE(principal) + << "GetSystemPrincipal() should not fail"; + + CheckGeneratedOriginKey(principal, nullptr, nullptr); + + // Check the null principal. + principal = NullPrincipal::CreateWithoutOriginAttributes(); + ASSERT_TRUE(principal) + << "CreateWithoutOriginAttributes() should not fail"; + + CheckGeneratedOriginKey(principal, nullptr, nullptr); + + // Check content principals. + static const OriginKeyTest tests[] = { + {"http://localhost", "tsohlacol.:http:80", ":tsohlacol."}, + {"http://www.mozilla.org", "gro.allizom.www.:http:80", ":gro.allizom."}, + {"https://www.mozilla.org", "gro.allizom.www.:https:443", + ":gro.allizom."}, + {"http://www.mozilla.org:32400", "gro.allizom.www.:http:32400", + ":gro.allizom."}, + {"file:///Users/Joe/Sites/", "/setiS/eoJ/sresU/.:file", + ":/setiS/eoJ/sresU/."}, + {"file:///Users/Joe/Sites/#foo", "/setiS/eoJ/sresU/.:file", + ":/setiS/eoJ/sresU/."}, + {"file:///Users/Joe/Sites/?foo", "/setiS/eoJ/sresU/.:file", + ":/setiS/eoJ/sresU/."}, + {"file:///Users/Joe/Sites", "/eoJ/sresU/.:file", ":/eoJ/sresU/."}, + {"file:///Users/Joe/Sites#foo", "/eoJ/sresU/.:file", ":/eoJ/sresU/."}, + {"file:///Users/Joe/Sites?foo", "/eoJ/sresU/.:file", ":/eoJ/sresU/."}, + {"moz-extension://53711a8f-65ed-e742-9671-1f02e267c0bc/" + "_generated_background_page.html", + "cb0c762e20f1-1769-247e-de56-f8a11735.:moz-extension", + ":cb0c762e20f1-1769-247e-de56-f8a11735."}, + {"http://[::1]:8/test.html", "1::.:http:8", ":1::."}, + }; + + for (const auto& test : tests) { + principal = GetContentPrincipal(test.mSpec); + ASSERT_TRUE(principal) + << "GetContentPrincipal() should not fail"; + + CheckGeneratedOriginKey(principal, test.mOriginKey, test.mQuotaKey); + } +} diff --git a/dom/localstorage/test/gtest/moz.build b/dom/localstorage/test/gtest/moz.build new file mode 100644 index 0000000000..f9d3c7bb0b --- /dev/null +++ b/dom/localstorage/test/gtest/moz.build @@ -0,0 +1,17 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +UNIFIED_SOURCES = [ + "TestLocalStorage.cpp", +] + +include("/ipc/chromium/chromium-config.mozbuild") + +FINAL_LIBRARY = "xul-gtest" + +LOCAL_INCLUDES += [ + "/dom/localstorage", +] diff --git a/dom/localstorage/test/helpers.js b/dom/localstorage/test/helpers.js new file mode 100644 index 0000000000..69281b09df --- /dev/null +++ b/dom/localstorage/test/helpers.js @@ -0,0 +1,86 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +// testSteps is expected to be defined by the test using this file. +/* global testSteps:false */ + +function executeSoon(aFun) { + SpecialPowers.Services.tm.dispatchToMainThread({ + run() { + aFun(); + }, + }); +} + +function clearAllDatabases() { + let qms = SpecialPowers.Services.qms; + let principal = SpecialPowers.wrap(document).nodePrincipal; + let request = qms.clearStoragesForPrincipal(principal); + return request; +} + +if (!window.runTest) { + window.runTest = async function() { + SimpleTest.waitForExplicitFinish(); + + info("Pushing preferences"); + + await SpecialPowers.pushPrefEnv({ + set: [ + ["dom.storage.testing", true], + ["dom.quotaManager.testing", true], + ], + }); + + info("Clearing old databases"); + + await requestFinished(clearAllDatabases()); + + ok(typeof testSteps === "function", "There should be a testSteps function"); + ok( + testSteps.constructor.name === "AsyncFunction", + "testSteps should be an async function" + ); + + SimpleTest.registerCleanupFunction(async function() { + await requestFinished(clearAllDatabases()); + }); + + add_task(testSteps); + }; +} + +function returnToEventLoop() { + return new Promise(function(resolve) { + executeSoon(resolve); + }); +} + +function getLocalStorage() { + return localStorage; +} + +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 = SpecialPowers.wrapCallback(function() { + resolve(); + }); + }); + + if (request.resultCode !== SpecialPowers.Cr.NS_OK) { + throw new RequestError(request.resultCode, request.resultName); + } + + return request.result; +} diff --git a/dom/localstorage/test/mochitest.ini b/dom/localstorage/test/mochitest.ini new file mode 100644 index 0000000000..500bdd0a1c --- /dev/null +++ b/dom/localstorage/test/mochitest.ini @@ -0,0 +1,10 @@ +# 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] +support-files = + helpers.js + unit/test_largeItems.js + +[test_largeItems.html] diff --git a/dom/localstorage/test/page_private_ls.html b/dom/localstorage/test/page_private_ls.html new file mode 100644 index 0000000000..795a814981 --- /dev/null +++ b/dom/localstorage/test/page_private_ls.html @@ -0,0 +1,9 @@ +<!doctype html> +<html> +<head> + <meta charset="utf-8"> +</head> +<body> + All the interesting stuff happens in ContentTask.spawn() calls. +</body> +</html> diff --git a/dom/localstorage/test/test_largeItems.html b/dom/localstorage/test/test_largeItems.html new file mode 100644 index 0000000000..92316085fc --- /dev/null +++ b/dom/localstorage/test/test_largeItems.html @@ -0,0 +1,19 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<html> +<head> + <title>Large Items Test</title> + + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript" src="unit/test_largeItems.js"></script> + <script type="text/javascript" src="helpers.js"></script> + +</head> + +<body onload="runTest();"></body> + +</html> 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..03c6114ae9 --- /dev/null +++ b/dom/localstorage/test/unit/databaseShadowing-shared.js @@ -0,0 +1,110 @@ +/* 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 } }, +]; + +function enableNextGenLocalStorage() { + info("Setting pref"); + + Services.prefs.setBoolPref("dom.storage.next_gen", true); +} + +function disableNextGenLocalStorage() { + info("Setting pref"); + + Services.prefs.setBoolPref("dom.storage.next_gen", false); +} + +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"); + + 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) { + 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"); + } else { + ok(storage.getItem("key0") == null, "Correct value"); + ok(storage.getItem("key1") == null, "Correct value"); + ok(storage.getItem("key2") == "value2", "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..7c42b82e9a --- /dev/null +++ b/dom/localstorage/test/unit/head.js @@ -0,0 +1,335 @@ +/** + * 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; + +var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); + +function is(a, b, msg) { + Assert.equal(a, b, msg); +} + +function ok(cond, msg) { + Assert.ok(!!cond, msg); +} + +function run_test() { + runTest(); +} + +if (!this.runTest) { + this.runTest = function() { + do_get_profile(); + + enableTesting(); + + Cu.importGlobalProperties(["crypto"]); + + Assert.ok( + typeof testSteps === "function", + "There should be a testSteps function" + ); + Assert.ok( + testSteps.constructor.name === "AsyncFunction", + "testSteps should be an async function" + ); + + registerCleanupFunction(resetTesting); + + add_task(testSteps); + + // Since we defined run_test, we must invoke run_next_test() to start the + // async test. + run_next_test(); + }; +} + +function returnToEventLoop() { + return new Promise(function(resolve) { + executeSoon(resolve); + }); +} + +function enableTesting() { + 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"); +} + +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 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 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/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/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..5b3f118cef --- /dev/null +++ b/dom/localstorage/test/unit/test_archive.js @@ -0,0 +1,75 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +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); + + // Profile 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(); + + // Profile 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(); + + // Profile 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..1725315f05 --- /dev/null +++ b/dom/localstorage/test/unit/test_clientValidation.js @@ -0,0 +1,29 @@ +/** + * 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. + */ +async function testSteps() { + const principal = getPrincipal("http://example.com"); + + info("Setting prefs"); + + Services.prefs.setBoolPref("dom.storage.next_gen", true); + 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..6c28957305 --- /dev/null +++ b/dom/localstorage/test/unit/test_corruptedDatabase.js @@ -0,0 +1,70 @@ +/** + * 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"); +} + +async function testSteps() { + info("Setting pref"); + + Services.prefs.setBoolPref("dom.storage.next_gen", true); + + // 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..12c4a646e4 --- /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"); + +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..476de87d26 --- /dev/null +++ b/dom/localstorage/test/unit/test_databaseShadowing2.js @@ -0,0 +1,19 @@ +/** + * 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"); + +async function testSteps() { + // The shadow database was prepared in test_databaseShadowing1.js + + disableNextGenLocalStorage(); + + if (!importShadowDatabase("shadowdb.sqlite")) { + return; + } + + verifyData([]); +} 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..e71e5d2b78 --- /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"); + +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..8f6680fca9 --- /dev/null +++ b/dom/localstorage/test/unit/test_databaseShadowing_clearOrigin2.js @@ -0,0 +1,19 @@ +/** + * 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"); + +async function testSteps() { + // The shadow database was prepared in test_databaseShadowing_clearOrigin1.js + + disableNextGenLocalStorage(); + + if (!importShadowDatabase("shadowdb-clearedOrigin.sqlite")) { + return; + } + + verifyData([1]); +} 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..961da5aaf7 --- /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"); + +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..1726792f1e --- /dev/null +++ b/dom/localstorage/test/unit/test_databaseShadowing_clearOriginsByPattern2.js @@ -0,0 +1,20 @@ +/** + * 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"); + +async function testSteps() { + // The shadow database was prepared in + // test_databaseShadowing_clearOriginsByPattern1.js + + disableNextGenLocalStorage(); + + if (!importShadowDatabase("shadowdb-clearedOriginsByPattern.sqlite")) { + return; + } + + verifyData([4, 5, 6]); +} 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..6680fda937 --- /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"); + +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..bea8e1bbb2 --- /dev/null +++ b/dom/localstorage/test/unit/test_databaseShadowing_clearOriginsByPrefix2.js @@ -0,0 +1,20 @@ +/** + * 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"); + +async function testSteps() { + // The shadow database was prepared in + // test_databaseShadowing_clearOriginsByPrefix1.js + + disableNextGenLocalStorage(); + + if (!importShadowDatabase("shadowdb-clearedOriginsByPrefix.sqlite")) { + return; + } + + verifyData([2, 3]); +} diff --git a/dom/localstorage/test/unit/test_eviction.js b/dom/localstorage/test/unit/test_eviction.js new file mode 100644 index 0000000000..0ed44f3927 --- /dev/null +++ b/dom/localstorage/test/unit/test_eviction.js @@ -0,0 +1,88 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +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.next_gen", true); + 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(ex instanceof DOMException, "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..2f7d631722 --- /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. + */ + +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..6036c24717 --- /dev/null +++ b/dom/localstorage/test/unit/test_groupLimit.js @@ -0,0 +1,82 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +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.next_gen", true); + 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(ex instanceof DOMException, "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..ba0fe6f6ad --- /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. + */ + +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..f274724a88 --- /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. + */ + +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.hasActiveSnapshot, "Snapshot successfully finished"); +} diff --git a/dom/localstorage/test/unit/test_migration.js b/dom/localstorage/test/unit/test_migration.js new file mode 100644 index 0000000000..fd21ffe282 --- /dev/null +++ b/dom/localstorage/test/unit/test_migration.js @@ -0,0 +1,124 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +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.next_gen", true); + + 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_orderingAfterRemoveAdd.js b/dom/localstorage/test/unit/test_orderingAfterRemoveAdd.js new file mode 100644 index 0000000000..d9c1ec914a --- /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/ + */ + +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..0980eafc2b --- /dev/null +++ b/dom/localstorage/test/unit/test_originInit.js @@ -0,0 +1,302 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +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 = "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); + } + + function removeFile(file) { + file.remove(false); + } + + function createEmptyFile(file) { + file.create(Ci.nsIFile.NORMAL_FILE_TYPE, parseInt("0644", 8)); + } + + 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 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); + } + + info("Setting prefs"); + + Services.prefs.setBoolPref("dom.storage.next_gen", true); + + 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(); +} diff --git a/dom/localstorage/test/unit/test_preloading.js b/dom/localstorage/test/unit/test_preloading.js new file mode 100644 index 0000000000..bf7bad6ae3 --- /dev/null +++ b/dom/localstorage/test/unit/test_preloading.js @@ -0,0 +1,81 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +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.next_gen", true); + 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..7a219ec9c4 --- /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/ + */ + +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_snapshotting.js b/dom/localstorage/test/unit/test_snapshotting.js new file mode 100644 index 0000000000..d29cbf020c --- /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/ + */ + +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..a43673682e --- /dev/null +++ b/dom/localstorage/test/unit/test_stringLength.js @@ -0,0 +1,71 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +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.next_gen", true); + + 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..830890fff7 --- /dev/null +++ b/dom/localstorage/test/unit/test_stringLength2.js @@ -0,0 +1,76 @@ +/** + * 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. + */ + +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.next_gen", true); + + 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_uri_encoding_edge_cases.js b/dom/localstorage/test/unit/test_uri_encoding_edge_cases.js new file mode 100644 index 0000000000..c711b7124b --- /dev/null +++ b/dom/localstorage/test/unit/test_uri_encoding_edge_cases.js @@ -0,0 +1,66 @@ +/** + * 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. + */ + +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.next_gen", true); + + 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..f910760ab4 --- /dev/null +++ b/dom/localstorage/test/unit/test_usage.js @@ -0,0 +1,66 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +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.next_gen", true); + + 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..71be71abf4 --- /dev/null +++ b/dom/localstorage/test/unit/test_usageAfterMigration.js @@ -0,0 +1,161 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +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.next_gen", true); + + 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..0a53f60156 --- /dev/null +++ b/dom/localstorage/test/unit/xpcshell.ini @@ -0,0 +1,54 @@ +# 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 +support-files = + archive_profile.zip + corruptedDatabase_profile.zip + corruptedDatabase_missingUsageFile_profile.zip + groupMismatch_profile.zip + migration_profile.zip + schema3upgrade_profile.zip + stringLength2_profile.zip + stringLength_profile.zip + usageAfterMigration_profile.zip + +[test_archive.js] +[test_clientValidation.js] +[test_corruptedDatabase.js] +[test_databaseShadowing1.js] +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] +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] +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] +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_migration.js] +[test_orderingAfterRemoveAdd.js] +[test_originInit.js] +[test_preloading.js] +[test_schema3upgrade.js] +[test_snapshotting.js] +skip-if = tsan # Unreasonably slow, bug 1612707 +requesttimeoutfactor = 4 +[test_stringLength.js] +[test_stringLength2.js] +[test_uri_encoding_edge_cases.js] +[test_usage.js] +[test_usageAfterMigration.js] |