summaryrefslogtreecommitdiffstats
path: root/dom/localstorage/test
diff options
context:
space:
mode:
Diffstat (limited to 'dom/localstorage/test')
-rw-r--r--dom/localstorage/test/browser.ini6
-rw-r--r--dom/localstorage/test/browser_private_ls.js44
-rw-r--r--dom/localstorage/test/gtest/TestLocalStorage.cpp140
-rw-r--r--dom/localstorage/test/gtest/moz.build17
-rw-r--r--dom/localstorage/test/helpers.js78
-rw-r--r--dom/localstorage/test/mochitest.ini10
-rw-r--r--dom/localstorage/test/page_private_ls.html9
-rw-r--r--dom/localstorage/test/test_largeItems.html19
-rw-r--r--dom/localstorage/test/unit/archive_profile.zipbin0 -> 1162 bytes
-rw-r--r--dom/localstorage/test/unit/corruptedDatabase_missingUsageFile_profile.zipbin0 -> 3873 bytes
-rw-r--r--dom/localstorage/test/unit/corruptedDatabase_profile.zipbin0 -> 4096 bytes
-rw-r--r--dom/localstorage/test/unit/databaseShadowing-shared.js130
-rw-r--r--dom/localstorage/test/unit/groupMismatch_profile.zipbin0 -> 1706 bytes
-rw-r--r--dom/localstorage/test/unit/head.js332
-rw-r--r--dom/localstorage/test/unit/make_migration_emptyValue.js23
-rw-r--r--dom/localstorage/test/unit/migration_emptyValue_profile.zipbin0 -> 824 bytes
-rw-r--r--dom/localstorage/test/unit/migration_profile.zipbin0 -> 2026 bytes
-rw-r--r--dom/localstorage/test/unit/schema3upgrade_profile.zipbin0 -> 4092 bytes
-rw-r--r--dom/localstorage/test/unit/schema4upgrade_profile.zipbin0 -> 5385 bytes
-rw-r--r--dom/localstorage/test/unit/stringLength2_profile.zipbin0 -> 46237 bytes
-rw-r--r--dom/localstorage/test/unit/stringLength_profile.zipbin0 -> 13919 bytes
-rw-r--r--dom/localstorage/test/unit/test_archive.js78
-rw-r--r--dom/localstorage/test/unit/test_clientValidation.js32
-rw-r--r--dom/localstorage/test/unit/test_corruptedDatabase.js73
-rw-r--r--dom/localstorage/test/unit/test_databaseShadowing1.js23
-rw-r--r--dom/localstorage/test/unit/test_databaseShadowing2.js17
-rw-r--r--dom/localstorage/test/unit/test_databaseShadowing_clearOrigin1.js30
-rw-r--r--dom/localstorage/test/unit/test_databaseShadowing_clearOrigin2.js17
-rw-r--r--dom/localstorage/test/unit/test_databaseShadowing_clearOriginsByPattern1.js29
-rw-r--r--dom/localstorage/test/unit/test_databaseShadowing_clearOriginsByPattern2.js21
-rw-r--r--dom/localstorage/test/unit/test_databaseShadowing_clearOriginsByPrefix1.js28
-rw-r--r--dom/localstorage/test/unit/test_databaseShadowing_clearOriginsByPrefix2.js21
-rw-r--r--dom/localstorage/test/unit/test_eviction.js91
-rw-r--r--dom/localstorage/test/unit/test_flushing.js72
-rw-r--r--dom/localstorage/test/unit/test_groupLimit.js85
-rw-r--r--dom/localstorage/test/unit/test_groupMismatch.js45
-rw-r--r--dom/localstorage/test/unit/test_largeItems.js88
-rw-r--r--dom/localstorage/test/unit/test_lsng_enabled.js13
-rw-r--r--dom/localstorage/test/unit/test_migration.js127
-rw-r--r--dom/localstorage/test/unit/test_migration_emptyValue.js37
-rw-r--r--dom/localstorage/test/unit/test_old_lsng_pref.js17
-rw-r--r--dom/localstorage/test/unit/test_orderingAfterRemoveAdd.js70
-rw-r--r--dom/localstorage/test/unit/test_originInit.js372
-rw-r--r--dom/localstorage/test/unit/test_preloading.js87
-rw-r--r--dom/localstorage/test/unit/test_schema3upgrade.js39
-rw-r--r--dom/localstorage/test/unit/test_schema4upgrade.js39
-rw-r--r--dom/localstorage/test/unit/test_snapshotting.js330
-rw-r--r--dom/localstorage/test/unit/test_stringLength.js74
-rw-r--r--dom/localstorage/test/unit/test_stringLength2.js79
-rw-r--r--dom/localstorage/test/unit/test_unicodeCharacters.js202
-rw-r--r--dom/localstorage/test/unit/test_uri_encoding_edge_cases.js69
-rw-r--r--dom/localstorage/test/unit/test_usage.js69
-rw-r--r--dom/localstorage/test/unit/test_usageAfterMigration.js164
-rw-r--r--dom/localstorage/test/unit/usageAfterMigration_profile.zipbin0 -> 1227 bytes
-rw-r--r--dom/localstorage/test/unit/xpcshell.ini73
55 files changed, 3419 insertions, 0 deletions
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..5b375c90e1
--- /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..f9dbdd21dc
--- /dev/null
+++ b/dom/localstorage/test/helpers.js
@@ -0,0 +1,78 @@
+/**
+ * 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());
+
+ SimpleTest.registerCleanupFunction(async function () {
+ await requestFinished(clearAllDatabases());
+ });
+ };
+}
+
+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
new file mode 100644
index 0000000000..71b2d1e5f9
--- /dev/null
+++ b/dom/localstorage/test/unit/archive_profile.zip
Binary files differ
diff --git a/dom/localstorage/test/unit/corruptedDatabase_missingUsageFile_profile.zip b/dom/localstorage/test/unit/corruptedDatabase_missingUsageFile_profile.zip
new file mode 100644
index 0000000000..8cfa6e3d43
--- /dev/null
+++ b/dom/localstorage/test/unit/corruptedDatabase_missingUsageFile_profile.zip
Binary files differ
diff --git a/dom/localstorage/test/unit/corruptedDatabase_profile.zip b/dom/localstorage/test/unit/corruptedDatabase_profile.zip
new file mode 100644
index 0000000000..2f60db2a45
--- /dev/null
+++ b/dom/localstorage/test/unit/corruptedDatabase_profile.zip
Binary files differ
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
new file mode 100644
index 0000000000..182b013de0
--- /dev/null
+++ b/dom/localstorage/test/unit/groupMismatch_profile.zip
Binary files differ
diff --git a/dom/localstorage/test/unit/head.js b/dom/localstorage/test/unit/head.js
new file mode 100644
index 0000000000..8ddc42ac4f
--- /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
new file mode 100644
index 0000000000..b829beae77
--- /dev/null
+++ b/dom/localstorage/test/unit/migration_emptyValue_profile.zip
Binary files differ
diff --git a/dom/localstorage/test/unit/migration_profile.zip b/dom/localstorage/test/unit/migration_profile.zip
new file mode 100644
index 0000000000..19dc3d4805
--- /dev/null
+++ b/dom/localstorage/test/unit/migration_profile.zip
Binary files differ
diff --git a/dom/localstorage/test/unit/schema3upgrade_profile.zip b/dom/localstorage/test/unit/schema3upgrade_profile.zip
new file mode 100644
index 0000000000..1ee9bfbf2e
--- /dev/null
+++ b/dom/localstorage/test/unit/schema3upgrade_profile.zip
Binary files differ
diff --git a/dom/localstorage/test/unit/schema4upgrade_profile.zip b/dom/localstorage/test/unit/schema4upgrade_profile.zip
new file mode 100644
index 0000000000..ae8ba09606
--- /dev/null
+++ b/dom/localstorage/test/unit/schema4upgrade_profile.zip
Binary files differ
diff --git a/dom/localstorage/test/unit/stringLength2_profile.zip b/dom/localstorage/test/unit/stringLength2_profile.zip
new file mode 100644
index 0000000000..de4d0fc3aa
--- /dev/null
+++ b/dom/localstorage/test/unit/stringLength2_profile.zip
Binary files differ
diff --git a/dom/localstorage/test/unit/stringLength_profile.zip b/dom/localstorage/test/unit/stringLength_profile.zip
new file mode 100644
index 0000000000..6cac890860
--- /dev/null
+++ b/dom/localstorage/test/unit/stringLength_profile.zip
Binary files differ
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..17f015d5ef
--- /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..c3b7c808eb
--- /dev/null
+++ b/dom/localstorage/test/unit/test_preloading.js
@@ -0,0 +1,87 @@
+/**
+ * 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,
+ principal.privateBrowsingId > 0 ? "private" : "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..3ea8923e0a
--- /dev/null
+++ b/dom/localstorage/test/unit/test_unicodeCharacters.js
@@ -0,0 +1,202 @@
+/**
+ * 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
new file mode 100644
index 0000000000..30a73292c3
--- /dev/null
+++ b/dom/localstorage/test/unit/usageAfterMigration_profile.zip
Binary files differ
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]