summaryrefslogtreecommitdiffstats
path: root/dom/fs/test/gtest/parent
diff options
context:
space:
mode:
Diffstat (limited to 'dom/fs/test/gtest/parent')
-rw-r--r--dom/fs/test/gtest/parent/TestFileSystemHashSource.cpp183
-rw-r--r--dom/fs/test/gtest/parent/TestFileSystemQuotaClient.cpp474
-rw-r--r--dom/fs/test/gtest/parent/datamodel/TestFileSystemDataManager.cpp185
-rw-r--r--dom/fs/test/gtest/parent/datamodel/TestFileSystemDataManagerVersions.cpp1059
-rw-r--r--dom/fs/test/gtest/parent/datamodel/moz.build22
-rw-r--r--dom/fs/test/gtest/parent/moz.build21
6 files changed, 1944 insertions, 0 deletions
diff --git a/dom/fs/test/gtest/parent/TestFileSystemHashSource.cpp b/dom/fs/test/gtest/parent/TestFileSystemHashSource.cpp
new file mode 100644
index 0000000000..1764763dd5
--- /dev/null
+++ b/dom/fs/test/gtest/parent/TestFileSystemHashSource.cpp
@@ -0,0 +1,183 @@
+/* -*- 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 "FileSystemHashSource.h"
+#include "FileSystemParentTypes.h"
+#include "TestHelpers.h"
+#include "gtest/gtest.h"
+#include "mozilla/Array.h"
+#include "mozilla/dom/FileSystemTypes.h"
+#include "nsContentUtils.h"
+#include "nsLiteralString.h"
+#include "nsString.h"
+#include "nsStringFwd.h"
+#include "nsTArray.h"
+#include "nsTHashSet.h"
+
+namespace mozilla::dom::fs::test {
+
+using mozilla::dom::fs::data::FileSystemHashSource;
+
+namespace {
+
+constexpr size_t sha256ByteLength = 32u;
+
+constexpr size_t kExpectedLength = 52u;
+
+std::wstring asWide(const nsString& aStr) {
+ std::wstring result;
+ result.reserve(aStr.Length());
+ for (const auto* it = aStr.BeginReading(); it != aStr.EndReading(); ++it) {
+ result.push_back(static_cast<wchar_t>(*it));
+ }
+ return result;
+}
+
+} // namespace
+
+TEST(TestFileSystemHashSource, isHashLengthAsExpected)
+{
+ EntryId parent = "a"_ns;
+ Name name = u"b"_ns;
+ TEST_TRY_UNWRAP(EntryId result,
+ FileSystemHashSource::GenerateHash(parent, name));
+ ASSERT_EQ(sha256ByteLength, result.Length());
+};
+
+TEST(TestFileSystemHashSource, areNestedNameHashesValidAndUnequal)
+{
+ EntryId emptyParent = ""_ns;
+ Name name = u"a"_ns;
+ const size_t nestingNumber = 500u;
+
+ nsTHashSet<EntryId> results;
+ nsTHashSet<Name> names;
+
+ auto previousParent = emptyParent;
+ for (size_t i = 0; i < nestingNumber; ++i) {
+ TEST_TRY_UNWRAP(EntryId result,
+ FileSystemHashSource::GenerateHash(previousParent, name));
+
+ TEST_TRY_UNWRAP(Name encoded,
+ FileSystemHashSource::EncodeHash(FileId(result)));
+
+ // Validity checks
+ ASSERT_TRUE(mozilla::IsAscii(encoded))
+ << encoded;
+ Name upperCaseVersion;
+ nsContentUtils::ASCIIToUpper(encoded, upperCaseVersion);
+ ASSERT_STREQ(asWide(upperCaseVersion).c_str(), asWide(encoded).c_str());
+
+ // Is the same hash encountered?
+ ASSERT_FALSE(results.Contains(result));
+ ASSERT_TRUE(results.Insert(result, mozilla::fallible));
+
+ // Is the same name encountered?
+ ASSERT_FALSE(names.Contains(encoded));
+ ASSERT_TRUE(names.Insert(encoded, mozilla::fallible));
+
+ previousParent = result;
+ }
+};
+
+TEST(TestFileSystemHashSource, areNameCombinationHashesUnequal)
+{
+ EntryId emptyParent = ""_ns;
+
+ mozilla::Array<Name, 2> inputs = {u"a"_ns, u"b"_ns};
+ nsTArray<EntryId> results;
+ nsTArray<Name> names;
+
+ for (const auto& name : inputs) {
+ TEST_TRY_UNWRAP(EntryId result,
+ FileSystemHashSource::GenerateHash(emptyParent, name));
+ TEST_TRY_UNWRAP(Name encoded,
+ FileSystemHashSource::EncodeHash(FileId(result)));
+
+ // Validity checks
+ ASSERT_TRUE(mozilla::IsAscii(encoded))
+ << encoded;
+ Name upperCaseVersion;
+ nsContentUtils::ASCIIToUpper(encoded, upperCaseVersion);
+ ASSERT_STREQ(asWide(upperCaseVersion).c_str(), asWide(encoded).c_str());
+
+ results.AppendElement(result);
+ names.AppendElement(encoded);
+ }
+
+ nsTArray<EntryId> more_results;
+ nsTArray<Name> more_names;
+ for (const auto& parent : results) {
+ for (const auto& name : inputs) {
+ TEST_TRY_UNWRAP(EntryId result,
+ FileSystemHashSource::GenerateHash(parent, name));
+ TEST_TRY_UNWRAP(Name encoded,
+ FileSystemHashSource::EncodeHash(FileId(result)));
+
+ // Validity checks
+ ASSERT_TRUE(mozilla::IsAscii(encoded))
+ << encoded;
+ Name upperCaseVersion;
+ nsContentUtils::ASCIIToUpper(encoded, upperCaseVersion);
+ ASSERT_STREQ(asWide(upperCaseVersion).c_str(), asWide(encoded).c_str());
+
+ more_results.AppendElement(result);
+ more_names.AppendElement(encoded);
+ }
+ }
+
+ results.AppendElements(more_results);
+ names.AppendElements(more_names);
+
+ // Is the same hash encountered?
+ for (size_t i = 0; i < results.Length(); ++i) {
+ for (size_t j = i + 1; j < results.Length(); ++j) {
+ ASSERT_STRNE(results[i].get(), results[j].get());
+ }
+ }
+
+ // Is the same name encountered?
+ for (size_t i = 0; i < names.Length(); ++i) {
+ for (size_t j = i + 1; j < names.Length(); ++j) {
+ ASSERT_STRNE(asWide(names[i]).c_str(), asWide(names[j]).c_str());
+ }
+ }
+};
+
+TEST(TestFileSystemHashSource, encodeGeneratedHash)
+{
+ Name expected = u"HF6FOFV72G3NMDEJKYMVRIFJO4X5ZNZCF2GM7Q4Y5Q3E7NPQKSLA"_ns;
+ ASSERT_EQ(kExpectedLength, expected.Length());
+
+ EntryId parent = "a"_ns;
+ Name name = u"b"_ns;
+ TEST_TRY_UNWRAP(EntryId entry,
+ FileSystemHashSource::GenerateHash(parent, name));
+ ASSERT_EQ(sha256ByteLength, entry.Length());
+
+ TEST_TRY_UNWRAP(Name result, FileSystemHashSource::EncodeHash(FileId(entry)));
+ ASSERT_EQ(kExpectedLength, result.Length());
+ ASSERT_STREQ(asWide(expected).c_str(), asWide(result).c_str());
+
+ // Generate further hashes
+ TEST_TRY_UNWRAP(entry, FileSystemHashSource::GenerateHash(entry, result));
+ ASSERT_EQ(sha256ByteLength, entry.Length());
+
+ TEST_TRY_UNWRAP(result, FileSystemHashSource::EncodeHash(FileId(entry)));
+
+ // Always the same length
+ ASSERT_EQ(kExpectedLength, result.Length());
+
+ // Encoded versions should differ
+ ASSERT_STRNE(asWide(expected).c_str(), asWide(result).c_str());
+
+ // Padding length should have been stripped
+ char16_t padding = u"="_ns[0];
+ const int32_t paddingStart = result.FindChar(padding);
+ ASSERT_EQ(-1, paddingStart);
+};
+
+} // namespace mozilla::dom::fs::test
diff --git a/dom/fs/test/gtest/parent/TestFileSystemQuotaClient.cpp b/dom/fs/test/gtest/parent/TestFileSystemQuotaClient.cpp
new file mode 100644
index 0000000000..d62dfb1229
--- /dev/null
+++ b/dom/fs/test/gtest/parent/TestFileSystemQuotaClient.cpp
@@ -0,0 +1,474 @@
+/* -*- 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 "FileSystemParentTypes.h"
+#include "TestHelpers.h"
+#include "datamodel/FileSystemDataManager.h"
+#include "datamodel/FileSystemDatabaseManager.h"
+#include "datamodel/FileSystemFileManager.h"
+#include "gtest/gtest.h"
+#include "mozIStorageService.h"
+#include "mozStorageCID.h"
+#include "mozilla/SpinEventLoopUntil.h"
+#include "mozilla/dom/FileSystemQuotaClientFactory.h"
+#include "mozilla/dom/PFileSystemManager.h"
+#include "mozilla/dom/ScriptSettings.h"
+#include "mozilla/dom/quota/FileStreams.h"
+#include "mozilla/dom/quota/QuotaCommon.h"
+#include "mozilla/dom/quota/QuotaManager.h"
+#include "mozilla/dom/quota/QuotaManagerService.h"
+#include "mozilla/dom/quota/ResultExtensions.h"
+#include "mozilla/dom/quota/UsageInfo.h"
+#include "mozilla/dom/quota/test/QuotaManagerDependencyFixture.h"
+#include "mozilla/gtest/MozAssertions.h"
+#include "nsIFile.h"
+#include "nsIFileURL.h"
+#include "nsIPrefBranch.h"
+#include "nsIPrefService.h"
+#include "nsIQuotaCallbacks.h"
+#include "nsIQuotaRequests.h"
+#include "nsNetUtil.h"
+#include "nsScriptSecurityManager.h"
+
+namespace mozilla::dom::fs::test {
+
+quota::OriginMetadata GetTestQuotaOriginMetadata() {
+ return quota::OriginMetadata{""_ns,
+ "quotaexample.com"_ns,
+ "http://quotaexample.com"_ns,
+ "http://quotaexample.com"_ns,
+ /* aIsPrivate */ false,
+ quota::PERSISTENCE_TYPE_DEFAULT};
+}
+
+class TestFileSystemQuotaClient
+ : public quota::test::QuotaManagerDependencyFixture {
+ public:
+ static const int sPage = 64 * 512;
+ // ExceedsPreallocation value may depend on platform and sqlite version!
+ static const int sExceedsPreallocation = sPage;
+
+ protected:
+ void SetUp() override { ASSERT_NO_FATAL_FAILURE(InitializeFixture()); }
+
+ void TearDown() override {
+ EXPECT_NO_FATAL_FAILURE(
+ ClearStoragesForOrigin(GetTestQuotaOriginMetadata()));
+ ASSERT_NO_FATAL_FAILURE(ShutdownFixture());
+ }
+
+ static const Name& GetTestFileName() {
+ static Name testFileName = []() {
+ nsCString testCFileName;
+ testCFileName.SetLength(sExceedsPreallocation);
+ std::fill(testCFileName.BeginWriting(), testCFileName.EndWriting(), 'x');
+ return NS_ConvertASCIItoUTF16(testCFileName.BeginReading(),
+ sExceedsPreallocation);
+ }();
+
+ return testFileName;
+ }
+
+ static uint64_t BytesOfName(const Name& aName) {
+ return static_cast<uint64_t>(aName.Length() * sizeof(Name::char_type));
+ }
+
+ static const nsCString& GetTestData() {
+ static const nsCString sTestData = "There is a way out of every box"_ns;
+ return sTestData;
+ }
+
+ static void CreateNewEmptyFile(
+ data::FileSystemDatabaseManager* const aDatabaseManager,
+ const FileSystemChildMetadata& aFileSlot, EntryId& aEntryId) {
+ // The file should not exist yet
+ Result<EntryId, QMResult> existingTestFile =
+ aDatabaseManager->GetOrCreateFile(aFileSlot, /* create */ false);
+ ASSERT_TRUE(existingTestFile.isErr());
+ ASSERT_NSEQ(NS_ERROR_DOM_NOT_FOUND_ERR,
+ ToNSResult(existingTestFile.unwrapErr()));
+
+ // Create a new file
+ TEST_TRY_UNWRAP(aEntryId, aDatabaseManager->GetOrCreateFile(
+ aFileSlot, /* create */ true));
+ }
+
+ static void WriteDataToFile(
+ data::FileSystemDatabaseManager* const aDatabaseManager,
+ const EntryId& aEntryId, const nsCString& aData) {
+ TEST_TRY_UNWRAP(FileId fileId, aDatabaseManager->EnsureFileId(aEntryId));
+ ASSERT_FALSE(fileId.IsEmpty());
+
+ ContentType type;
+ TimeStamp lastModMilliS = 0;
+ Path path;
+ nsCOMPtr<nsIFile> fileObj;
+ ASSERT_NSEQ(NS_OK,
+ aDatabaseManager->GetFile(aEntryId, fileId, FileMode::EXCLUSIVE,
+ type, lastModMilliS, path, fileObj));
+
+ uint32_t written = 0;
+ ASSERT_NE(written, aData.Length());
+
+ const quota::OriginMetadata& testOriginMeta = GetTestQuotaOriginMetadata();
+
+ TEST_TRY_UNWRAP(nsCOMPtr<nsIOutputStream> fileStream,
+ quota::CreateFileOutputStream(
+ quota::PERSISTENCE_TYPE_DEFAULT, testOriginMeta,
+ quota::Client::FILESYSTEM, fileObj));
+
+ auto finallyClose = MakeScopeExit(
+ [&fileStream]() { ASSERT_NSEQ(NS_OK, fileStream->Close()); });
+ ASSERT_NSEQ(NS_OK,
+ fileStream->Write(aData.get(), aData.Length(), &written));
+
+ ASSERT_EQ(aData.Length(), written);
+ }
+
+ /* Static for use in callbacks */
+ static void CreateRegisteredDataManager(
+ Registered<data::FileSystemDataManager>& aRegisteredDataManager) {
+ bool done = false;
+
+ data::FileSystemDataManager::GetOrCreateFileSystemDataManager(
+ GetTestQuotaOriginMetadata())
+ ->Then(
+ GetCurrentSerialEventTarget(), __func__,
+ [&aRegisteredDataManager,
+ &done](Registered<data::FileSystemDataManager>
+ registeredDataManager) mutable {
+ auto doneOnReturn = MakeScopeExit([&done]() { done = true; });
+
+ ASSERT_TRUE(registeredDataManager->IsOpen());
+ aRegisteredDataManager = std::move(registeredDataManager);
+ },
+ [&done](nsresult rejectValue) {
+ auto doneOnReturn = MakeScopeExit([&done]() { done = true; });
+
+ ASSERT_NSEQ(NS_OK, rejectValue);
+ });
+
+ SpinEventLoopUntil("Promise is fulfilled"_ns, [&done]() { return done; });
+
+ ASSERT_TRUE(aRegisteredDataManager);
+ ASSERT_TRUE(aRegisteredDataManager->IsOpen());
+ ASSERT_TRUE(aRegisteredDataManager->MutableDatabaseManagerPtr());
+ }
+
+ static void CheckUsageEqualTo(const quota::UsageInfo& aUsage,
+ uint64_t expected) {
+ EXPECT_TRUE(aUsage.FileUsage().isNothing());
+ auto dbUsage = aUsage.DatabaseUsage();
+ ASSERT_TRUE(dbUsage.isSome());
+ const auto actual = dbUsage.value();
+ ASSERT_EQ(actual, expected);
+ }
+
+ static void CheckUsageGreaterThan(const quota::UsageInfo& aUsage,
+ uint64_t expected) {
+ EXPECT_TRUE(aUsage.FileUsage().isNothing());
+ auto dbUsage = aUsage.DatabaseUsage();
+ ASSERT_TRUE(dbUsage.isSome());
+ const auto actual = dbUsage.value();
+ ASSERT_GT(actual, expected);
+ }
+};
+
+TEST_F(TestFileSystemQuotaClient, CheckUsageBeforeAnyFilesOnDisk) {
+ auto backgroundTask = []() {
+ mozilla::Atomic<bool> isCanceled{false};
+ auto ioTask = [&isCanceled](const RefPtr<quota::Client>& quotaClient,
+ data::FileSystemDatabaseManager* dbm) {
+ ASSERT_FALSE(isCanceled);
+ const quota::OriginMetadata& testOriginMeta =
+ GetTestQuotaOriginMetadata();
+ const Origin& testOrigin = testOriginMeta.mOrigin;
+
+ // After initialization,
+ // * database size is not zero
+ // * GetUsageForOrigin and InitOrigin should agree
+ TEST_TRY_UNWRAP(quota::UsageInfo usageNow,
+ quotaClient->InitOrigin(quota::PERSISTENCE_TYPE_DEFAULT,
+ testOriginMeta, isCanceled));
+ ASSERT_NO_FATAL_FAILURE(CheckUsageGreaterThan(usageNow, 0u));
+ const auto initialDbUsage = usageNow.DatabaseUsage().value();
+
+ TEST_TRY_UNWRAP(usageNow, quotaClient->GetUsageForOrigin(
+ quota::PERSISTENCE_TYPE_DEFAULT,
+ testOriginMeta, isCanceled));
+ ASSERT_NO_FATAL_FAILURE(CheckUsageEqualTo(usageNow, initialDbUsage));
+
+ // Create a new file
+ TEST_TRY_UNWRAP(const EntryId rootId, data::GetRootHandle(testOrigin));
+ FileSystemChildMetadata fileData(rootId, GetTestFileName());
+
+ EntryId testFileId;
+ ASSERT_NO_FATAL_FAILURE(CreateNewEmptyFile(dbm, fileData, testFileId));
+
+ // After a new file has been created (only in the database),
+ // * database size has increased
+ // * GetUsageForOrigin and InitOrigin should agree
+ const auto expectedUse = initialDbUsage + 2 * sPage;
+
+ TEST_TRY_UNWRAP(usageNow, quotaClient->GetUsageForOrigin(
+ quota::PERSISTENCE_TYPE_DEFAULT,
+ testOriginMeta, isCanceled));
+ ASSERT_NO_FATAL_FAILURE(CheckUsageEqualTo(usageNow, expectedUse));
+
+ TEST_TRY_UNWRAP(usageNow,
+ quotaClient->InitOrigin(quota::PERSISTENCE_TYPE_DEFAULT,
+ testOriginMeta, isCanceled));
+ ASSERT_NO_FATAL_FAILURE(CheckUsageEqualTo(usageNow, expectedUse));
+ };
+
+ RefPtr<mozilla::dom::quota::Client> quotaClient = fs::CreateQuotaClient();
+ ASSERT_TRUE(quotaClient);
+
+ // For uninitialized database, file usage is nothing
+ auto checkTask =
+ [&isCanceled](const RefPtr<mozilla::dom::quota::Client>& quotaClient) {
+ TEST_TRY_UNWRAP(quota::UsageInfo usageNow,
+ quotaClient->GetUsageForOrigin(
+ quota::PERSISTENCE_TYPE_DEFAULT,
+ GetTestQuotaOriginMetadata(), isCanceled));
+
+ ASSERT_TRUE(usageNow.DatabaseUsage().isNothing());
+ EXPECT_TRUE(usageNow.FileUsage().isNothing());
+ };
+
+ PerformOnIOThread(std::move(checkTask),
+ RefPtr<mozilla::dom::quota::Client>{quotaClient});
+
+ // Initialize database
+ Registered<data::FileSystemDataManager> rdm;
+ ASSERT_NO_FATAL_FAILURE(CreateRegisteredDataManager(rdm));
+
+ // Run tests with an initialized database
+ PerformOnIOThread(std::move(ioTask), std::move(quotaClient),
+ rdm->MutableDatabaseManagerPtr());
+ };
+
+ PerformOnBackgroundThread(std::move(backgroundTask));
+}
+
+TEST_F(TestFileSystemQuotaClient, WritesToFilesShouldIncreaseUsage) {
+ auto backgroundTask = []() {
+ mozilla::Atomic<bool> isCanceled{false};
+ auto ioTask = [&isCanceled](
+ const RefPtr<mozilla::dom::quota::Client>& quotaClient,
+ data::FileSystemDatabaseManager* dbm) {
+ const quota::OriginMetadata& testOriginMeta =
+ GetTestQuotaOriginMetadata();
+ const Origin& testOrigin = testOriginMeta.mOrigin;
+
+ TEST_TRY_UNWRAP(const EntryId rootId, data::GetRootHandle(testOrigin));
+ FileSystemChildMetadata fileData(rootId, GetTestFileName());
+
+ EntryId testFileId;
+ ASSERT_NO_FATAL_FAILURE(CreateNewEmptyFile(dbm, fileData, testFileId));
+ // const auto testFileDbUsage = usageNow.DatabaseUsage().value();
+
+ TEST_TRY_UNWRAP(
+ quota::UsageInfo usageNow,
+ quotaClient->GetUsageForOrigin(quota::PERSISTENCE_TYPE_DEFAULT,
+ testOriginMeta, isCanceled));
+ ASSERT_TRUE(usageNow.DatabaseUsage().isSome());
+ const auto testFileDbUsage = usageNow.DatabaseUsage().value();
+
+ TEST_TRY_UNWRAP(usageNow,
+ quotaClient->InitOrigin(quota::PERSISTENCE_TYPE_DEFAULT,
+ testOriginMeta, isCanceled));
+ ASSERT_NO_FATAL_FAILURE(CheckUsageEqualTo(usageNow, testFileDbUsage));
+
+ // Fill the file with some content
+ const nsCString& testData = GetTestData();
+
+ ASSERT_NO_FATAL_FAILURE(WriteDataToFile(dbm, testFileId, testData));
+
+ // In this test we don't lock the file -> no rescan is expected
+ // and InitOrigin should return the previous value
+ TEST_TRY_UNWRAP(usageNow,
+ quotaClient->InitOrigin(quota::PERSISTENCE_TYPE_DEFAULT,
+ testOriginMeta, isCanceled));
+ ASSERT_NO_FATAL_FAILURE(CheckUsageEqualTo(usageNow, testFileDbUsage));
+
+ // When data manager unlocks the file, it should call update
+ // but in this test we call it directly
+ ASSERT_NSEQ(NS_OK, dbm->UpdateUsage(FileId(testFileId)));
+
+ const auto expectedTotalUsage = testFileDbUsage + testData.Length();
+
+ // Disk usage should have increased after writing
+ TEST_TRY_UNWRAP(usageNow,
+ quotaClient->InitOrigin(quota::PERSISTENCE_TYPE_DEFAULT,
+ testOriginMeta, isCanceled));
+ ASSERT_NO_FATAL_FAILURE(CheckUsageEqualTo(usageNow, expectedTotalUsage));
+
+ // The usage values should now agree
+ TEST_TRY_UNWRAP(usageNow, quotaClient->GetUsageForOrigin(
+ quota::PERSISTENCE_TYPE_DEFAULT,
+ testOriginMeta, isCanceled));
+ ASSERT_NO_FATAL_FAILURE(CheckUsageEqualTo(usageNow, expectedTotalUsage));
+ };
+
+ RefPtr<mozilla::dom::quota::Client> quotaClient = fs::CreateQuotaClient();
+ ASSERT_TRUE(quotaClient);
+
+ // Initialize database
+ Registered<data::FileSystemDataManager> rdm;
+ ASSERT_NO_FATAL_FAILURE(CreateRegisteredDataManager(rdm));
+
+ // Run tests with an initialized database
+ PerformOnIOThread(std::move(ioTask), std::move(quotaClient),
+ rdm->MutableDatabaseManagerPtr());
+ };
+
+ PerformOnBackgroundThread(std::move(backgroundTask));
+}
+
+TEST_F(TestFileSystemQuotaClient, TrackedFilesOnInitOriginShouldCauseRescan) {
+ auto backgroundTask = []() {
+ mozilla::Atomic<bool> isCanceled{false};
+ EntryId* testFileId = new EntryId();
+ auto cleanupFileId = MakeScopeExit([&testFileId] { delete testFileId; });
+
+ auto fileCreation = [&testFileId](data::FileSystemDatabaseManager* dbm) {
+ const Origin& testOrigin = GetTestQuotaOriginMetadata().mOrigin;
+
+ TEST_TRY_UNWRAP(const EntryId rootId, data::GetRootHandle(testOrigin));
+ FileSystemChildMetadata fileData(rootId, GetTestFileName());
+
+ EntryId someId;
+ ASSERT_NO_FATAL_FAILURE(CreateNewEmptyFile(dbm, fileData, someId));
+ testFileId->Append(someId);
+ };
+
+ auto writingToFile =
+ [&isCanceled, testFileId](
+ const RefPtr<mozilla::dom::quota::Client>& quotaClient,
+ data::FileSystemDatabaseManager* dbm) {
+ const quota::OriginMetadata& testOriginMeta =
+ GetTestQuotaOriginMetadata();
+ TEST_TRY_UNWRAP(
+ quota::UsageInfo usageNow,
+ quotaClient->GetUsageForOrigin(quota::PERSISTENCE_TYPE_DEFAULT,
+ testOriginMeta, isCanceled));
+ ASSERT_TRUE(usageNow.DatabaseUsage().isSome());
+ const auto testFileDbUsage = usageNow.DatabaseUsage().value();
+
+ // Fill the file with some content
+ const auto& testData = GetTestData();
+ const auto expectedTotalUsage = testFileDbUsage + testData.Length();
+
+ ASSERT_NO_FATAL_FAILURE(WriteDataToFile(dbm, *testFileId, testData));
+
+ // We don't call update now - because the file is tracked and
+ // InitOrigin should perform a rescan
+ TEST_TRY_UNWRAP(
+ usageNow, quotaClient->InitOrigin(quota::PERSISTENCE_TYPE_DEFAULT,
+ testOriginMeta, isCanceled));
+ ASSERT_NO_FATAL_FAILURE(
+ CheckUsageEqualTo(usageNow, expectedTotalUsage));
+
+ // As always, the cached and scanned values should agree
+ TEST_TRY_UNWRAP(usageNow, quotaClient->GetUsageForOrigin(
+ quota::PERSISTENCE_TYPE_DEFAULT,
+ testOriginMeta, isCanceled));
+ ASSERT_NO_FATAL_FAILURE(
+ CheckUsageEqualTo(usageNow, expectedTotalUsage));
+ };
+
+ RefPtr<mozilla::dom::quota::Client> quotaClient = fs::CreateQuotaClient();
+ ASSERT_TRUE(quotaClient);
+
+ // Initialize database
+ Registered<data::FileSystemDataManager> rdm;
+ ASSERT_NO_FATAL_FAILURE(CreateRegisteredDataManager(rdm));
+
+ PerformOnIOThread(std::move(fileCreation),
+ rdm->MutableDatabaseManagerPtr());
+
+ // This should force a rescan
+ TEST_TRY_UNWRAP(FileId fileId, rdm->LockExclusive(*testFileId));
+ ASSERT_FALSE(fileId.IsEmpty());
+ PerformOnIOThread(std::move(writingToFile), std::move(quotaClient),
+ rdm->MutableDatabaseManagerPtr());
+ };
+
+ PerformOnBackgroundThread(std::move(backgroundTask));
+}
+
+TEST_F(TestFileSystemQuotaClient, RemovingFileShouldDecreaseUsage) {
+ auto backgroundTask = []() {
+ mozilla::Atomic<bool> isCanceled{false};
+ auto ioTask = [&isCanceled](
+ const RefPtr<mozilla::dom::quota::Client>& quotaClient,
+ data::FileSystemDatabaseManager* dbm) {
+ const quota::OriginMetadata& testOriginMeta =
+ GetTestQuotaOriginMetadata();
+ const Origin& testOrigin = testOriginMeta.mOrigin;
+
+ TEST_TRY_UNWRAP(const EntryId rootId, data::GetRootHandle(testOrigin));
+ FileSystemChildMetadata fileData(rootId, GetTestFileName());
+
+ EntryId testFileId;
+ ASSERT_NO_FATAL_FAILURE(CreateNewEmptyFile(dbm, fileData, testFileId));
+ TEST_TRY_UNWRAP(quota::UsageInfo usageNow,
+ quotaClient->InitOrigin(quota::PERSISTENCE_TYPE_DEFAULT,
+ testOriginMeta, isCanceled));
+ ASSERT_TRUE(usageNow.DatabaseUsage().isSome());
+ const auto testFileDbUsage = usageNow.DatabaseUsage().value();
+
+ // Fill the file with some content
+ const nsCString& testData = GetTestData();
+ const auto expectedTotalUsage = testFileDbUsage + testData.Length();
+
+ ASSERT_NO_FATAL_FAILURE(WriteDataToFile(dbm, testFileId, testData));
+
+ // Currently, usage is expected to be updated on unlock by data manager
+ // but here UpdateUsage() is called directly
+ ASSERT_NSEQ(NS_OK, dbm->UpdateUsage(FileId(testFileId)));
+
+ // At least some file disk usage should have appeared after unlocking
+ TEST_TRY_UNWRAP(usageNow, quotaClient->GetUsageForOrigin(
+ quota::PERSISTENCE_TYPE_DEFAULT,
+ testOriginMeta, isCanceled));
+ ASSERT_NO_FATAL_FAILURE(CheckUsageEqualTo(usageNow, expectedTotalUsage));
+
+ TEST_TRY_UNWRAP(bool wasRemoved,
+ dbm->RemoveFile({rootId, GetTestFileName()}));
+ ASSERT_TRUE(wasRemoved);
+
+ // Removes cascade and usage table should be up to date immediately
+ TEST_TRY_UNWRAP(usageNow,
+ quotaClient->InitOrigin(quota::PERSISTENCE_TYPE_DEFAULT,
+ testOriginMeta, isCanceled));
+ ASSERT_NO_FATAL_FAILURE(CheckUsageEqualTo(usageNow, testFileDbUsage));
+
+ // GetUsageForOrigin should agree
+ TEST_TRY_UNWRAP(usageNow, quotaClient->GetUsageForOrigin(
+ quota::PERSISTENCE_TYPE_DEFAULT,
+ testOriginMeta, isCanceled));
+
+ ASSERT_NO_FATAL_FAILURE(CheckUsageEqualTo(usageNow, testFileDbUsage));
+ };
+
+ RefPtr<mozilla::dom::quota::Client> quotaClient = fs::CreateQuotaClient();
+ ASSERT_TRUE(quotaClient);
+
+ // Initialize database
+ Registered<data::FileSystemDataManager> rdm;
+ ASSERT_NO_FATAL_FAILURE(CreateRegisteredDataManager(rdm));
+
+ // Run tests with an initialized database
+ PerformOnIOThread(std::move(ioTask), std::move(quotaClient),
+ rdm->MutableDatabaseManagerPtr());
+ };
+
+ PerformOnBackgroundThread(std::move(backgroundTask));
+}
+
+} // namespace mozilla::dom::fs::test
diff --git a/dom/fs/test/gtest/parent/datamodel/TestFileSystemDataManager.cpp b/dom/fs/test/gtest/parent/datamodel/TestFileSystemDataManager.cpp
new file mode 100644
index 0000000000..484def797f
--- /dev/null
+++ b/dom/fs/test/gtest/parent/datamodel/TestFileSystemDataManager.cpp
@@ -0,0 +1,185 @@
+/* -*- 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 "FileSystemDataManager.h"
+#include "TestHelpers.h"
+#include "mozilla/MozPromise.h"
+#include "mozilla/SpinEventLoopUntil.h"
+#include "mozilla/dom/quota/ForwardDecls.h"
+#include "mozilla/dom/quota/test/QuotaManagerDependencyFixture.h"
+
+namespace mozilla::dom::fs::test {
+
+class TestFileSystemDataManager
+ : public quota::test::QuotaManagerDependencyFixture {
+ public:
+ static void SetUpTestCase() { ASSERT_NO_FATAL_FAILURE(InitializeFixture()); }
+
+ static void TearDownTestCase() {
+ EXPECT_NO_FATAL_FAILURE(ClearStoragesForOrigin(GetTestOriginMetadata()));
+ ASSERT_NO_FATAL_FAILURE(ShutdownFixture());
+ }
+};
+
+TEST_F(TestFileSystemDataManager, GetOrCreateFileSystemDataManager) {
+ auto backgroundTask = []() {
+ bool done = false;
+
+ data::FileSystemDataManager::GetOrCreateFileSystemDataManager(
+ GetTestOriginMetadata())
+ ->Then(
+ GetCurrentSerialEventTarget(), __func__,
+ [](Registered<data::FileSystemDataManager> registeredDataManager) {
+ RefPtr<data::FileSystemDataManager> dataManager =
+ registeredDataManager.get();
+
+ registeredDataManager = nullptr;
+
+ return dataManager->OnClose();
+ },
+ [](nsresult rejectValue) {
+ return BoolPromise::CreateAndReject(rejectValue, __func__);
+ })
+ ->Then(
+ GetCurrentSerialEventTarget(), __func__,
+ [&done](const BoolPromise::ResolveOrRejectValue&) { done = true; });
+
+ SpinEventLoopUntil("Promise is fulfilled"_ns, [&done]() { return done; });
+ };
+
+ PerformOnBackgroundThread(std::move(backgroundTask));
+}
+
+TEST_F(TestFileSystemDataManager,
+ GetOrCreateFileSystemDataManager_PendingOpen) {
+ auto backgroundTask = []() {
+ Registered<data::FileSystemDataManager> rdm1;
+
+ Registered<data::FileSystemDataManager> rdm2;
+
+ {
+ bool done1 = false;
+
+ data::FileSystemDataManager::GetOrCreateFileSystemDataManager(
+ GetTestOriginMetadata())
+ ->Then(
+ GetCurrentSerialEventTarget(), __func__,
+ [&rdm1, &done1](Registered<data::FileSystemDataManager>
+ registeredDataManager) {
+ ASSERT_TRUE(registeredDataManager->IsOpen());
+
+ rdm1 = std::move(registeredDataManager);
+
+ done1 = true;
+ },
+ [&done1](nsresult rejectValue) { done1 = true; });
+
+ bool done2 = false;
+
+ data::FileSystemDataManager::GetOrCreateFileSystemDataManager(
+ GetTestOriginMetadata())
+ ->Then(
+ GetCurrentSerialEventTarget(), __func__,
+ [&rdm2, &done2](Registered<data::FileSystemDataManager>
+ registeredDataManager) {
+ ASSERT_TRUE(registeredDataManager->IsOpen());
+
+ rdm2 = std::move(registeredDataManager);
+
+ done2 = true;
+ },
+ [&done2](nsresult rejectValue) { done2 = true; });
+
+ SpinEventLoopUntil("Promise is fulfilled"_ns,
+ [&done1, &done2]() { return done1 && done2; });
+ }
+
+ RefPtr<data::FileSystemDataManager> dm1 = rdm1.unwrap();
+
+ RefPtr<data::FileSystemDataManager> dm2 = rdm2.unwrap();
+
+ {
+ bool done1 = false;
+
+ dm1->OnClose()->Then(
+ GetCurrentSerialEventTarget(), __func__,
+ [&done1](const BoolPromise::ResolveOrRejectValue&) { done1 = true; });
+
+ bool done2 = false;
+
+ dm2->OnClose()->Then(
+ GetCurrentSerialEventTarget(), __func__,
+ [&done2](const BoolPromise::ResolveOrRejectValue&) { done2 = true; });
+
+ SpinEventLoopUntil("Promise is fulfilled"_ns,
+ [&done1, &done2]() { return done1 && done2; });
+ }
+ };
+
+ PerformOnBackgroundThread(std::move(backgroundTask));
+}
+
+TEST_F(TestFileSystemDataManager,
+ GetOrCreateFileSystemDataManager_PendingClose) {
+ auto backgroundTask = []() {
+ Registered<data::FileSystemDataManager> rdm;
+
+ {
+ bool done = false;
+
+ data::FileSystemDataManager::GetOrCreateFileSystemDataManager(
+ GetTestOriginMetadata())
+ ->Then(
+ GetCurrentSerialEventTarget(), __func__,
+ [&rdm, &done](Registered<data::FileSystemDataManager>
+ registeredDataManager) {
+ ASSERT_TRUE(registeredDataManager->IsOpen());
+
+ rdm = std::move(registeredDataManager);
+
+ done = true;
+ },
+ [&done](nsresult rejectValue) { done = true; });
+
+ SpinEventLoopUntil("Promise is fulfilled"_ns, [&done]() { return done; });
+ }
+
+ RefPtr<data::FileSystemDataManager> dm = rdm.unwrap();
+
+ Unused << dm;
+
+ {
+ bool done = false;
+
+ data::FileSystemDataManager::GetOrCreateFileSystemDataManager(
+ GetTestOriginMetadata())
+ ->Then(
+ GetCurrentSerialEventTarget(), __func__,
+ [](Registered<data::FileSystemDataManager>
+ registeredDataManager) {
+ RefPtr<data::FileSystemDataManager> dataManager =
+ registeredDataManager.get();
+
+ registeredDataManager = nullptr;
+
+ return dataManager->OnClose();
+ },
+ [](nsresult rejectValue) {
+ return BoolPromise::CreateAndReject(rejectValue, __func__);
+ })
+ ->Then(GetCurrentSerialEventTarget(), __func__,
+ [&done](const BoolPromise::ResolveOrRejectValue&) {
+ done = true;
+ });
+
+ SpinEventLoopUntil("Promise is fulfilled"_ns, [&done]() { return done; });
+ }
+ };
+
+ PerformOnBackgroundThread(std::move(backgroundTask));
+}
+
+} // namespace mozilla::dom::fs::test
diff --git a/dom/fs/test/gtest/parent/datamodel/TestFileSystemDataManagerVersions.cpp b/dom/fs/test/gtest/parent/datamodel/TestFileSystemDataManagerVersions.cpp
new file mode 100644
index 0000000000..9782e534e9
--- /dev/null
+++ b/dom/fs/test/gtest/parent/datamodel/TestFileSystemDataManagerVersions.cpp
@@ -0,0 +1,1059 @@
+/* -*- 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 <algorithm>
+
+#include "ErrorList.h"
+#include "FileSystemDataManager.h"
+#include "FileSystemDatabaseManagerVersion001.h"
+#include "FileSystemDatabaseManagerVersion002.h"
+#include "FileSystemFileManager.h"
+#include "FileSystemHashSource.h"
+#include "ResultStatement.h"
+#include "SchemaVersion001.h"
+#include "SchemaVersion002.h"
+#include "TestHelpers.h"
+#include "gtest/gtest.h"
+#include "mozIStorageService.h"
+#include "mozStorageCID.h"
+#include "mozStorageHelper.h"
+#include "mozilla/Array.h"
+#include "mozilla/ErrorNames.h"
+#include "mozilla/Result.h"
+#include "mozilla/dom/FileSystemTypes.h"
+#include "mozilla/dom/PFileSystemManager.h"
+#include "mozilla/dom/quota/CommonMetadata.h"
+#include "mozilla/dom/quota/test/QuotaManagerDependencyFixture.h"
+#include "nsContentUtils.h"
+#include "nsIFile.h"
+#include "nsLiteralString.h"
+#include "nsNetCID.h"
+#include "nsReadableUtils.h"
+#include "nsString.h"
+#include "nsStringFwd.h"
+#include "nsTArray.h"
+#include "nsTHashSet.h"
+
+namespace mozilla::dom::fs::test {
+
+using data::FileSystemDatabaseManagerVersion001;
+using data::FileSystemDatabaseManagerVersion002;
+using data::FileSystemFileManager;
+
+quota::OriginMetadata GetOriginMetadataSample() {
+ return quota::OriginMetadata{""_ns,
+ "firefox.com"_ns,
+ "http://firefox.com"_ns,
+ "http://firefox.com"_ns,
+ /* aIsPrivate */ false,
+ quota::PERSISTENCE_TYPE_DEFAULT};
+}
+
+class TestFileSystemDatabaseManagerVersionsBase
+ : public quota::test::QuotaManagerDependencyFixture {
+ public:
+ void SetUp() override { ASSERT_NO_FATAL_FAILURE(InitializeFixture()); }
+
+ void TearDown() override {
+ EXPECT_NO_FATAL_FAILURE(ClearStoragesForOrigin(GetOriginMetadataSample()));
+ ASSERT_NO_FATAL_FAILURE(ShutdownFixture());
+ }
+};
+
+class TestFileSystemDatabaseManagerVersions
+ : public TestFileSystemDatabaseManagerVersionsBase,
+ public ::testing::WithParamInterface<DatabaseVersion> {
+ public:
+ static void AssertEntryIdMoved(const EntryId& aOriginal,
+ const EntryId& aMoved) {
+ switch (sVersion) {
+ case 1: {
+ ASSERT_EQ(aOriginal, aMoved);
+ break;
+ }
+ case 2: {
+ ASSERT_NE(aOriginal, aMoved);
+ break;
+ }
+ default: {
+ ASSERT_FALSE(false)
+ << "Unknown database version";
+ }
+ }
+ }
+
+ static void AssertEntryIdCollision(const EntryId& aOriginal,
+ const EntryId& aMoved) {
+ switch (sVersion) {
+ case 1: {
+ // We generated a new entryId
+ ASSERT_NE(aOriginal, aMoved);
+ break;
+ }
+ case 2: {
+ // We get the same entryId for the same input
+ ASSERT_EQ(aOriginal, aMoved);
+ break;
+ }
+ default: {
+ ASSERT_FALSE(false)
+ << "Unknown database version";
+ }
+ }
+ }
+
+ static DatabaseVersion sVersion;
+};
+
+// This is a minimal mock to allow us to safely call the lock methods
+// while avoiding assertions
+class MockFileSystemDataManager final : public data::FileSystemDataManager {
+ public:
+ MockFileSystemDataManager(const quota::OriginMetadata& aOriginMetadata,
+ MovingNotNull<nsCOMPtr<nsIEventTarget>> aIOTarget,
+ MovingNotNull<RefPtr<TaskQueue>> aIOTaskQueue)
+ : FileSystemDataManager(aOriginMetadata, nullptr, std::move(aIOTarget),
+ std::move(aIOTaskQueue)) {}
+
+ void SetDatabaseManager(data::FileSystemDatabaseManager* aDatabaseManager) {
+ mDatabaseManager =
+ UniquePtr<data::FileSystemDatabaseManager>(aDatabaseManager);
+ }
+
+ virtual ~MockFileSystemDataManager() {
+ mDatabaseManager->Close();
+ mDatabaseManager = nullptr;
+
+ // Need to avoid assertions
+ mState = State::Closed;
+ }
+};
+
+static void MakeDatabaseManagerVersions(
+ const DatabaseVersion aVersion,
+ RefPtr<MockFileSystemDataManager>& aDataManager,
+ FileSystemDatabaseManagerVersion001*& aDatabaseManager) {
+ TEST_TRY_UNWRAP(auto storageService,
+ MOZ_TO_RESULT_GET_TYPED(nsCOMPtr<mozIStorageService>,
+ MOZ_SELECT_OVERLOAD(do_GetService),
+ MOZ_STORAGE_SERVICE_CONTRACTID));
+
+ const auto flags = mozIStorageService::CONNECTION_DEFAULT;
+ ResultConnection connection;
+
+ nsresult rv = storageService->OpenSpecialDatabase(kMozStorageMemoryStorageKey,
+ VoidCString(), flags,
+ getter_AddRefs(connection));
+ ASSERT_NSEQ(NS_OK, rv);
+
+ auto fmRes = FileSystemFileManager::CreateFileSystemFileManager(
+ GetOriginMetadataSample());
+ ASSERT_FALSE(fmRes.isErr());
+
+ const Origin& testOrigin = GetTestOrigin();
+
+ if (1 == aVersion) {
+ TEST_TRY_UNWRAP(
+ TestFileSystemDatabaseManagerVersions::sVersion,
+ SchemaVersion001::InitializeConnection(connection, testOrigin));
+ } else {
+ ASSERT_EQ(2, aVersion);
+
+ TEST_TRY_UNWRAP(TestFileSystemDatabaseManagerVersions::sVersion,
+ SchemaVersion002::InitializeConnection(
+ connection, *fmRes.inspect(), testOrigin));
+ }
+ ASSERT_NE(0, TestFileSystemDatabaseManagerVersions::sVersion);
+
+ TEST_TRY_UNWRAP(EntryId rootId, data::GetRootHandle(GetTestOrigin()));
+
+ QM_TRY_UNWRAP(auto streamTransportService,
+ MOZ_TO_RESULT_GET_TYPED(nsCOMPtr<nsIEventTarget>,
+ MOZ_SELECT_OVERLOAD(do_GetService),
+ NS_STREAMTRANSPORTSERVICE_CONTRACTID),
+ QM_VOID);
+
+ quota::OriginMetadata originMetadata = GetOriginMetadataSample();
+
+ nsCString taskQueueName("OPFS "_ns + originMetadata.mOrigin);
+
+ RefPtr<TaskQueue> ioTaskQueue =
+ TaskQueue::Create(do_AddRef(streamTransportService), taskQueueName.get());
+
+ aDataManager = MakeRefPtr<MockFileSystemDataManager>(
+ originMetadata, WrapMovingNotNull(streamTransportService),
+ WrapMovingNotNull(ioTaskQueue));
+
+ if (1 == aVersion) {
+ aDatabaseManager = new FileSystemDatabaseManagerVersion001(
+ aDataManager, std::move(connection), fmRes.unwrap(), rootId);
+ } else {
+ ASSERT_EQ(2, aVersion);
+
+ aDatabaseManager = new FileSystemDatabaseManagerVersion002(
+ aDataManager, std::move(connection), fmRes.unwrap(), rootId);
+ }
+
+ aDataManager->SetDatabaseManager(aDatabaseManager);
+}
+
+DatabaseVersion TestFileSystemDatabaseManagerVersions::sVersion = 0;
+
+TEST_P(TestFileSystemDatabaseManagerVersions,
+ smokeTestCreateRemoveDirectories) {
+ const DatabaseVersion version = GetParam();
+
+ auto ioTask = [version]() {
+ nsresult rv = NS_OK;
+ // Ensure that FileSystemDataManager lives for the lifetime of the test
+ RefPtr<MockFileSystemDataManager> dataManager;
+ FileSystemDatabaseManagerVersion001* dm = nullptr;
+ ASSERT_NO_FATAL_FAILURE(
+ MakeDatabaseManagerVersions(version, dataManager, dm));
+ ASSERT_TRUE(dm);
+ // if any of these exit early, we have to close
+ auto autoClose = MakeScopeExit([dm] { dm->Close(); });
+
+ TEST_TRY_UNWRAP(EntryId rootId, data::GetRootHandle(GetTestOrigin()));
+
+ FileSystemChildMetadata firstChildMeta(rootId, u"First"_ns);
+ TEST_TRY_UNWRAP_ERR(
+ rv, dm->GetOrCreateDirectory(firstChildMeta, /* create */ false));
+ ASSERT_NSEQ(NS_ERROR_DOM_NOT_FOUND_ERR, rv);
+
+ TEST_TRY_UNWRAP(EntryId firstChild, dm->GetOrCreateDirectory(
+ firstChildMeta, /* create */ true));
+
+ int32_t dbVersion = 0;
+ TEST_TRY_UNWRAP(FileSystemDirectoryListing entries,
+ dm->GetDirectoryEntries(rootId, dbVersion));
+ ASSERT_EQ(1u, entries.directories().Length());
+ ASSERT_EQ(0u, entries.files().Length());
+
+ const auto& firstItemRef = entries.directories()[0];
+ ASSERT_TRUE(u"First"_ns == firstItemRef.entryName())
+ << firstItemRef.entryName();
+ ASSERT_EQ(firstChild, firstItemRef.entryId());
+
+ TEST_TRY_UNWRAP(
+ EntryId firstChildClone,
+ dm->GetOrCreateDirectory(firstChildMeta, /* create */ true));
+ ASSERT_EQ(firstChild, firstChildClone);
+
+ FileSystemChildMetadata secondChildMeta(firstChild, u"Second"_ns);
+ TEST_TRY_UNWRAP(
+ EntryId secondChild,
+ dm->GetOrCreateDirectory(secondChildMeta, /* create */ true));
+
+ FileSystemEntryPair shortPair(firstChild, secondChild);
+ TEST_TRY_UNWRAP(Path shortPath, dm->Resolve(shortPair));
+ ASSERT_EQ(1u, shortPath.Length());
+ ASSERT_EQ(u"Second"_ns, shortPath[0]);
+
+ FileSystemEntryPair longPair(rootId, secondChild);
+ TEST_TRY_UNWRAP(Path longPath, dm->Resolve(longPair));
+ ASSERT_EQ(2u, longPath.Length());
+ ASSERT_EQ(u"First"_ns, longPath[0]);
+ ASSERT_EQ(u"Second"_ns, longPath[1]);
+
+ FileSystemEntryPair wrongPair(secondChild, rootId);
+ TEST_TRY_UNWRAP(Path emptyPath, dm->Resolve(wrongPair));
+ ASSERT_TRUE(emptyPath.IsEmpty());
+
+ PageNumber page = 0;
+ TEST_TRY_UNWRAP(FileSystemDirectoryListing fEntries,
+ dm->GetDirectoryEntries(firstChild, page));
+ ASSERT_EQ(1u, fEntries.directories().Length());
+ ASSERT_EQ(0u, fEntries.files().Length());
+
+ const auto& secItemRef = fEntries.directories()[0];
+ ASSERT_TRUE(u"Second"_ns == secItemRef.entryName())
+ << secItemRef.entryName();
+ ASSERT_EQ(secondChild, secItemRef.entryId());
+
+ TEST_TRY_UNWRAP_ERR(
+ rv, dm->RemoveDirectory(firstChildMeta, /* recursive */ false));
+ ASSERT_NSEQ(NS_ERROR_DOM_INVALID_MODIFICATION_ERR, rv);
+
+ TEST_TRY_UNWRAP(bool isDeleted,
+ dm->RemoveDirectory(firstChildMeta, /* recursive */ true));
+ ASSERT_TRUE(isDeleted);
+
+ FileSystemChildMetadata thirdChildMeta(secondChild, u"Second"_ns);
+ TEST_TRY_UNWRAP_ERR(
+ rv, dm->GetOrCreateDirectory(thirdChildMeta, /* create */ true));
+ ASSERT_NSEQ(NS_ERROR_STORAGE_CONSTRAINT, rv); // Is this a good error?
+
+ dm->Close();
+ };
+
+ PerformOnIOThread(std::move(ioTask));
+}
+
+TEST_P(TestFileSystemDatabaseManagerVersions, smokeTestCreateRemoveFiles) {
+ const DatabaseVersion version = GetParam();
+
+ auto ioTask = [version]() {
+ nsresult rv = NS_OK;
+ // Ensure that FileSystemDataManager lives for the lifetime of the test
+ RefPtr<MockFileSystemDataManager> datamanager;
+ FileSystemDatabaseManagerVersion001* dm = nullptr;
+ ASSERT_NO_FATAL_FAILURE(
+ MakeDatabaseManagerVersions(version, datamanager, dm));
+
+ TEST_TRY_UNWRAP(EntryId rootId, data::GetRootHandle(GetTestOrigin()));
+
+ FileSystemChildMetadata firstChildMeta(rootId, u"First"_ns);
+ // If creating is not allowed, getting a file from empty root fails
+ TEST_TRY_UNWRAP_ERR(
+ rv, dm->GetOrCreateFile(firstChildMeta, /* create */ false));
+ ASSERT_NSEQ(NS_ERROR_DOM_NOT_FOUND_ERR, rv);
+
+ // Creating a file under empty root succeeds
+ TEST_TRY_UNWRAP(EntryId firstChild,
+ dm->GetOrCreateFile(firstChildMeta, /* create */ true));
+
+ // Second time, the same file is returned
+ TEST_TRY_UNWRAP(EntryId firstChildClone,
+ dm->GetOrCreateFile(firstChildMeta, /* create */ true));
+ ASSERT_EQ(firstChild, firstChildClone);
+
+ // Directory listing returns the created file
+ PageNumber page = 0;
+ TEST_TRY_UNWRAP(FileSystemDirectoryListing entries,
+ dm->GetDirectoryEntries(rootId, page));
+ ASSERT_EQ(0u, entries.directories().Length());
+ ASSERT_EQ(1u, entries.files().Length());
+
+ auto& firstItemRef = entries.files()[0];
+ ASSERT_TRUE(u"First"_ns == firstItemRef.entryName())
+ << firstItemRef.entryName();
+ ASSERT_EQ(firstChild, firstItemRef.entryId());
+
+ FileId fileId = FileId(firstItemRef.entryId()); // Default
+
+ ContentType type;
+ TimeStamp lastModifiedMilliSeconds;
+ Path path;
+ nsCOMPtr<nsIFile> file;
+ rv = dm->GetFile(firstItemRef.entryId(), fileId, FileMode::EXCLUSIVE, type,
+ lastModifiedMilliSeconds, path, file);
+ ASSERT_NSEQ(NS_OK, rv);
+
+ const int64_t nowMilliSeconds = PR_Now() / 1000;
+ ASSERT_GE(nowMilliSeconds, lastModifiedMilliSeconds);
+ const int64_t expectedMaxDelayMilliSeconds = 100;
+ const int64_t actualDelay = nowMilliSeconds - lastModifiedMilliSeconds;
+ ASSERT_LT(actualDelay, expectedMaxDelayMilliSeconds);
+
+ ASSERT_EQ(1u, path.Length());
+ ASSERT_STREQ(u"First"_ns, path[0]);
+
+ ASSERT_NE(nullptr, file);
+
+ // Getting the file entry as directory fails
+ TEST_TRY_UNWRAP_ERR(
+ rv, dm->GetOrCreateDirectory(firstChildMeta, /* create */ false));
+ ASSERT_NSEQ(NS_ERROR_DOM_TYPE_MISMATCH_ERR, rv);
+
+ // Getting or creating the file entry as directory also fails
+ TEST_TRY_UNWRAP_ERR(
+ rv, dm->GetOrCreateDirectory(firstChildMeta, /* create */ true));
+ ASSERT_NSEQ(NS_ERROR_DOM_TYPE_MISMATCH_ERR, rv);
+
+ // Creating a file with non existing parent hash fails
+
+ EntryId notAChildHash = "0123456789abcdef0123456789abcdef"_ns;
+ FileSystemChildMetadata notAChildMeta(notAChildHash, u"Dummy"_ns);
+ TEST_TRY_UNWRAP_ERR(rv,
+ dm->GetOrCreateFile(notAChildMeta, /* create */ true));
+ ASSERT_NSEQ(NS_ERROR_STORAGE_CONSTRAINT, rv); // Is this a good error?
+
+ // We create a directory under root
+ FileSystemChildMetadata secondChildMeta(rootId, u"Second"_ns);
+ TEST_TRY_UNWRAP(
+ EntryId secondChild,
+ dm->GetOrCreateDirectory(secondChildMeta, /* create */ true));
+
+ // The root should now contain the existing file and the new directory
+ TEST_TRY_UNWRAP(FileSystemDirectoryListing fEntries,
+ dm->GetDirectoryEntries(rootId, page));
+ ASSERT_EQ(1u, fEntries.directories().Length());
+ ASSERT_EQ(1u, fEntries.files().Length());
+
+ const auto& secItemRef = fEntries.directories()[0];
+ ASSERT_TRUE(u"Second"_ns == secItemRef.entryName())
+ << secItemRef.entryName();
+ ASSERT_EQ(secondChild, secItemRef.entryId());
+
+ // Create a file under the new directory
+ FileSystemChildMetadata thirdChildMeta(secondChild, u"Third"_ns);
+ TEST_TRY_UNWRAP(EntryId thirdChild,
+ dm->GetOrCreateFile(thirdChildMeta, /* create */ true));
+
+ FileSystemEntryPair entryPair(rootId, thirdChild);
+ TEST_TRY_UNWRAP(Path entryPath, dm->Resolve(entryPair));
+ ASSERT_EQ(2u, entryPath.Length());
+ ASSERT_EQ(u"Second"_ns, entryPath[0]);
+ ASSERT_EQ(u"Third"_ns, entryPath[1]);
+
+ // If recursion is not allowed, the non-empty new directory may not be
+ // removed
+ TEST_TRY_UNWRAP_ERR(
+ rv, dm->RemoveDirectory(secondChildMeta, /* recursive */ false));
+ ASSERT_NSEQ(NS_ERROR_DOM_INVALID_MODIFICATION_ERR, rv);
+
+ // If recursion is allowed, the new directory goes away.
+ TEST_TRY_UNWRAP(bool isDeleted,
+ dm->RemoveDirectory(secondChildMeta, /* recursive */ true));
+ ASSERT_TRUE(isDeleted);
+
+ // The file under the removed directory is no longer accessible.
+ TEST_TRY_UNWRAP_ERR(rv,
+ dm->GetOrCreateFile(thirdChildMeta, /* create */ true));
+ ASSERT_NSEQ(NS_ERROR_STORAGE_CONSTRAINT, rv); // Is this a good error?
+
+ // The deletion is reflected by the root directory listing
+ TEST_TRY_UNWRAP(FileSystemDirectoryListing nEntries,
+ dm->GetDirectoryEntries(rootId, 0));
+ ASSERT_EQ(0u, nEntries.directories().Length());
+ ASSERT_EQ(1u, nEntries.files().Length());
+
+ const auto& fileItemRef = nEntries.files()[0];
+ ASSERT_TRUE(u"First"_ns == fileItemRef.entryName())
+ << fileItemRef.entryName();
+ ASSERT_EQ(firstChild, fileItemRef.entryId());
+
+ dm->Close();
+ };
+
+ PerformOnIOThread(std::move(ioTask));
+}
+
+TEST_P(TestFileSystemDatabaseManagerVersions, smokeTestCreateMoveDirectories) {
+ const DatabaseVersion version = GetParam();
+
+ auto ioTask = [version]() {
+ // Ensure that FileSystemDataManager lives for the lifetime of the test
+ RefPtr<MockFileSystemDataManager> datamanager;
+ FileSystemDatabaseManagerVersion001* dm = nullptr;
+ ASSERT_NO_FATAL_FAILURE(
+ MakeDatabaseManagerVersions(version, datamanager, dm));
+ auto closeAtExit = MakeScopeExit([&dm]() { dm->Close(); });
+
+ TEST_TRY_UNWRAP(EntryId rootId, data::GetRootHandle(GetTestOrigin()));
+
+ FileSystemEntryMetadata rootMeta{rootId, u"root"_ns,
+ /* is directory */ true};
+
+ {
+ // Sanity check: no existing items should be found
+ TEST_TRY_UNWRAP(FileSystemDirectoryListing contents,
+ dm->GetDirectoryEntries(rootId, /* page */ 0u));
+ ASSERT_TRUE(contents.directories().IsEmpty());
+ ASSERT_TRUE(contents.files().IsEmpty());
+ }
+
+ // Create subdirectory
+ FileSystemChildMetadata firstChildMeta(rootId, u"First"_ns);
+ TEST_TRY_UNWRAP(
+ EntryId firstChildDir,
+ dm->GetOrCreateDirectory(firstChildMeta, /* create */ true));
+
+ {
+ // Check that directory listing is as expected
+ TEST_TRY_UNWRAP(FileSystemDirectoryListing contents,
+ dm->GetDirectoryEntries(rootId, /* page */ 0u));
+ ASSERT_TRUE(contents.files().IsEmpty());
+ ASSERT_EQ(1u, contents.directories().Length());
+ ASSERT_STREQ(firstChildMeta.childName(),
+ contents.directories()[0].entryName());
+ }
+
+ {
+ // Try to move subdirectory to its current location
+ FileSystemEntryMetadata src{firstChildDir, firstChildMeta.childName(),
+ /* is directory */ true};
+ FileSystemChildMetadata dest{rootId, src.entryName()};
+ TEST_TRY_UNWRAP(EntryId moved, dm->MoveEntry(src, dest));
+ ASSERT_FALSE(moved.IsEmpty());
+ firstChildDir = moved;
+ }
+
+ {
+ // Try to move subdirectory under itself
+ FileSystemEntryMetadata src{firstChildDir, firstChildMeta.childName(),
+ /* is directory */ true};
+ FileSystemChildMetadata dest{src.entryId(), src.entryName()};
+ TEST_TRY_UNWRAP_ERR(nsresult rv, dm->MoveEntry(src, dest));
+ ASSERT_NSEQ(NS_ERROR_DOM_INVALID_MODIFICATION_ERR, rv);
+ }
+
+ {
+ // Try to move root under its subdirectory
+ FileSystemEntryMetadata src{rootId, rootMeta.entryName(),
+ /* is directory */ true};
+ FileSystemChildMetadata dest{firstChildDir, src.entryName()};
+ TEST_TRY_UNWRAP_ERR(nsresult rv, dm->MoveEntry(src, dest));
+ ASSERT_NSEQ(NS_ERROR_DOM_NOT_FOUND_ERR, rv);
+ }
+
+ // Create subsubdirectory
+ FileSystemChildMetadata firstChildDescendantMeta(firstChildDir,
+ u"Descendant"_ns);
+ TEST_TRY_UNWRAP(EntryId firstChildDescendant,
+ dm->GetOrCreateDirectory(firstChildDescendantMeta,
+ /* create */ true));
+
+ {
+ TEST_TRY_UNWRAP(FileSystemDirectoryListing contents,
+ dm->GetDirectoryEntries(firstChildDir, /* page */ 0u));
+ ASSERT_TRUE(contents.files().IsEmpty());
+ ASSERT_EQ(1u, contents.directories().Length());
+ ASSERT_STREQ(firstChildDescendantMeta.childName(),
+ contents.directories()[0].entryName());
+
+ TEST_TRY_UNWRAP(
+ Path subSubDirPath,
+ dm->Resolve({rootId, contents.directories()[0].entryId()}));
+ ASSERT_EQ(2u, subSubDirPath.Length());
+ ASSERT_STREQ(firstChildMeta.childName(), subSubDirPath[0]);
+ ASSERT_STREQ(firstChildDescendantMeta.childName(), subSubDirPath[1]);
+ }
+
+ {
+ // Try to move subsubdirectory to its current location
+ FileSystemEntryMetadata src{firstChildDescendant,
+ firstChildDescendantMeta.childName(),
+ /* is directory */ true};
+ FileSystemChildMetadata dest{firstChildDir, src.entryName()};
+ TEST_TRY_UNWRAP(EntryId moved, dm->MoveEntry(src, dest));
+ ASSERT_FALSE(moved.IsEmpty());
+ firstChildDescendant = moved;
+ }
+
+ {
+ // Try to move subsubdirectory under itself
+ FileSystemEntryMetadata src{firstChildDescendant,
+ firstChildDescendantMeta.childName(),
+ /* is directory */ true};
+ FileSystemChildMetadata dest{src.entryId(), src.entryName()};
+ TEST_TRY_UNWRAP_ERR(nsresult rv, dm->MoveEntry(src, dest));
+ ASSERT_NSEQ(NS_ERROR_DOM_INVALID_MODIFICATION_ERR, rv);
+ }
+
+ {
+ // Try to move subdirectory under its descendant
+ FileSystemEntryMetadata src{firstChildDir, firstChildMeta.childName(),
+ /* is directory */ true};
+ FileSystemChildMetadata dest{firstChildDescendant, src.entryName()};
+ TEST_TRY_UNWRAP_ERR(nsresult rv, dm->MoveEntry(src, dest));
+ ASSERT_NSEQ(NS_ERROR_DOM_INVALID_MODIFICATION_ERR, rv);
+ }
+
+ {
+ // Try to move root under its subsubdirectory
+ FileSystemEntryMetadata src{rootId, rootMeta.entryName(),
+ /* is directory */ true};
+ FileSystemChildMetadata dest{firstChildDescendant, src.entryName()};
+ TEST_TRY_UNWRAP_ERR(nsresult rv, dm->MoveEntry(src, dest));
+ ASSERT_NSEQ(NS_ERROR_DOM_NOT_FOUND_ERR, rv);
+ }
+
+ // Create file in the subdirectory with already existing subsubdirectory
+ FileSystemChildMetadata testFileMeta(firstChildDir, u"Subfile"_ns);
+ TEST_TRY_UNWRAP(EntryId testFile,
+ dm->GetOrCreateFile(testFileMeta, /* create */ true));
+
+ // Get handles to the original locations of the entries
+ FileSystemEntryMetadata subSubDir;
+ FileSystemEntryMetadata subSubFile;
+
+ {
+ TEST_TRY_UNWRAP(FileSystemDirectoryListing contents,
+ dm->GetDirectoryEntries(firstChildDir, /* page */ 0u));
+ ASSERT_EQ(1u, contents.files().Length());
+ ASSERT_EQ(1u, contents.directories().Length());
+
+ subSubDir = contents.directories()[0];
+ ASSERT_STREQ(firstChildDescendantMeta.childName(), subSubDir.entryName());
+
+ subSubFile = contents.files()[0];
+ ASSERT_STREQ(testFileMeta.childName(), subSubFile.entryName());
+ ASSERT_EQ(testFile, subSubFile.entryId());
+ }
+
+ {
+ TEST_TRY_UNWRAP(Path entryPath,
+ dm->Resolve({rootId, subSubFile.entryId()}));
+ ASSERT_EQ(2u, entryPath.Length());
+ ASSERT_STREQ(firstChildMeta.childName(), entryPath[0]);
+ ASSERT_STREQ(testFileMeta.childName(), entryPath[1]);
+ }
+
+ {
+ // Try to move file to its current location
+ FileSystemEntryMetadata src{testFile, testFileMeta.childName(),
+ /* is directory */ false};
+ FileSystemChildMetadata dest{firstChildDir, src.entryName()};
+ TEST_TRY_UNWRAP(EntryId moved, dm->MoveEntry(src, dest));
+ ASSERT_FALSE(moved.IsEmpty());
+ testFile = moved;
+ }
+
+ {
+ // Try to move subsubdirectory under a file
+ FileSystemEntryMetadata src{firstChildDescendant,
+ firstChildDescendantMeta.childName(),
+ /* is directory */ true};
+ FileSystemChildMetadata dest{testFile,
+ firstChildDescendantMeta.childName()};
+ TEST_TRY_UNWRAP_ERR(nsresult rv, dm->MoveEntry(src, dest));
+ ASSERT_NSEQ(NS_ERROR_STORAGE_CONSTRAINT, rv);
+ }
+
+ {
+ // Silently overwrite a directory with a file using rename
+ FileSystemEntryMetadata src{testFile, testFileMeta.childName(),
+ /* is directory */ false};
+ const FileSystemChildMetadata& dest = firstChildDescendantMeta;
+ TEST_TRY_UNWRAP(EntryId moved, dm->MoveEntry(src, dest));
+ ASSERT_FALSE(moved.IsEmpty());
+ testFile = moved;
+ }
+
+ {
+ // Move file back and recreate the directory
+ FileSystemEntryMetadata src{testFile,
+ firstChildDescendantMeta.childName(),
+ /* is directory */ false};
+
+ TEST_TRY_UNWRAP(EntryId moved, dm->MoveEntry(src, testFileMeta));
+ ASSERT_FALSE(moved.IsEmpty());
+ testFile = moved;
+
+ TEST_TRY_UNWRAP(EntryId firstChildDescendantCheck,
+ dm->GetOrCreateDirectory(firstChildDescendantMeta,
+ /* create */ true));
+ ASSERT_EQ(firstChildDescendant, firstChildDescendantCheck);
+ }
+
+ {
+ // Try to rename directory to quietly overwrite a file
+ FileSystemEntryMetadata src{firstChildDescendant,
+ firstChildDescendantMeta.childName(),
+ /* is directory */ true};
+ const FileSystemChildMetadata& dest = testFileMeta;
+ TEST_TRY_UNWRAP(EntryId moved, dm->MoveEntry(src, dest));
+ ASSERT_FALSE(moved.IsEmpty());
+ firstChildDescendant = moved;
+ }
+
+ {
+ // Move directory back and recreate the file
+ FileSystemEntryMetadata src{firstChildDescendant,
+ testFileMeta.childName(),
+ /* is directory */ true};
+
+ FileSystemChildMetadata dest{firstChildDir,
+ firstChildDescendantMeta.childName()};
+
+ TEST_TRY_UNWRAP(EntryId moved, dm->MoveEntry(src, dest));
+ ASSERT_FALSE(moved.IsEmpty());
+ firstChildDescendant = moved;
+
+ TEST_TRY_UNWRAP(EntryId testFileCheck,
+ dm->GetOrCreateFile(testFileMeta, /* create */ true));
+ ASSERT_EQ(testFile, testFileCheck);
+ }
+
+ {
+ // Move file one level up
+ FileSystemEntryMetadata src{testFile, testFileMeta.childName(),
+ /* is directory */ false};
+ FileSystemChildMetadata dest{rootId, src.entryName()};
+ TEST_TRY_UNWRAP(EntryId moved, dm->MoveEntry(src, dest));
+ ASSERT_FALSE(moved.IsEmpty());
+ testFile = moved;
+ }
+
+ {
+ // Check that listings are as expected
+ TEST_TRY_UNWRAP(FileSystemDirectoryListing contents,
+ dm->GetDirectoryEntries(firstChildDir, 0u));
+ ASSERT_TRUE(contents.files().IsEmpty());
+ ASSERT_EQ(1u, contents.directories().Length());
+ ASSERT_STREQ(firstChildDescendantMeta.childName(),
+ contents.directories()[0].entryName());
+ }
+
+ {
+ TEST_TRY_UNWRAP(FileSystemDirectoryListing contents,
+ dm->GetDirectoryEntries(rootId, 0u));
+ ASSERT_EQ(1u, contents.files().Length());
+ ASSERT_EQ(1u, contents.directories().Length());
+ ASSERT_STREQ(testFileMeta.childName(), contents.files()[0].entryName());
+ ASSERT_STREQ(firstChildMeta.childName(),
+ contents.directories()[0].entryName());
+ }
+
+ {
+ ASSERT_NO_FATAL_FAILURE(
+ AssertEntryIdMoved(subSubFile.entryId(), testFile));
+ TEST_TRY_UNWRAP(Path entryPath,
+ dm->Resolve({rootId, subSubFile.entryId()}));
+ if (1 == sVersion) {
+ ASSERT_EQ(1u, entryPath.Length());
+ ASSERT_STREQ(testFileMeta.childName(), entryPath[0]);
+ } else {
+ ASSERT_EQ(2, sVersion);
+ // Per spec, path result is empty when no path exists.
+ ASSERT_TRUE(entryPath.IsEmpty());
+ }
+ }
+
+ {
+ // Try to get a handle to the old item
+ TEST_TRY_UNWRAP_ERR(
+ nsresult rv, dm->GetOrCreateFile(testFileMeta, /* create */ false));
+ ASSERT_NSEQ(NS_ERROR_DOM_NOT_FOUND_ERR, rv);
+ }
+
+ {
+ // Try to move + rename file one level down to collide with a
+ // subSubDirectory, silently overwriting it
+ FileSystemEntryMetadata src{testFile, testFileMeta.childName(),
+ /* is directory */ false};
+ FileSystemChildMetadata dest{firstChildDir,
+ firstChildDescendantMeta.childName()};
+ TEST_TRY_UNWRAP(EntryId moved, dm->MoveEntry(src, dest));
+ ASSERT_FALSE(moved.IsEmpty());
+ testFile = moved;
+ }
+
+ {
+ // Restore filename, move file to its original location and recreate
+ // the overwritten directory
+ FileSystemEntryMetadata src{testFile,
+ firstChildDescendantMeta.childName(),
+ /* is directory */ false};
+ FileSystemChildMetadata dest{rootId, testFileMeta.childName()};
+ TEST_TRY_UNWRAP(EntryId moved, dm->MoveEntry(src, dest));
+ ASSERT_FALSE(moved.IsEmpty());
+ testFile = moved;
+
+ FileSystemChildMetadata oldLocation{firstChildDir,
+ firstChildDescendantMeta.childName()};
+
+ // Is there still something out there?
+ TEST_TRY_UNWRAP_ERR(nsresult rv,
+ dm->GetOrCreateFile(oldLocation, /* create */ false));
+ ASSERT_NSEQ(NS_ERROR_DOM_NOT_FOUND_ERR, rv);
+
+ TEST_TRY_UNWRAP(EntryId firstChildDescendantCheck,
+ dm->GetOrCreateDirectory(oldLocation, /* create */ true));
+ ASSERT_EQ(firstChildDescendant, firstChildDescendantCheck);
+ }
+
+ // Rename file first and then try to move it to collide with
+ // subSubDirectory, silently overwriting it
+ {
+ // Rename
+ FileSystemEntryMetadata src{testFile, testFileMeta.childName(),
+ /* is directory */ false};
+ FileSystemChildMetadata dest{rootId,
+ firstChildDescendantMeta.childName()};
+ TEST_TRY_UNWRAP(EntryId moved, dm->MoveEntry(src, dest));
+ ASSERT_FALSE(moved.IsEmpty());
+ testFile = moved;
+ }
+
+ {
+ // Try to move one level down
+ FileSystemEntryMetadata src{testFile,
+ firstChildDescendantMeta.childName(),
+ /* is directory */ false};
+ FileSystemChildMetadata dest{firstChildDir,
+ firstChildDescendantMeta.childName()};
+ TEST_TRY_UNWRAP(EntryId moved, dm->MoveEntry(src, dest));
+ ASSERT_FALSE(moved.IsEmpty());
+ testFile = moved;
+ }
+
+ {
+ // Move the file back and recreate the directory
+ FileSystemEntryMetadata src{testFile,
+ firstChildDescendantMeta.childName(),
+ /* is directory */ false};
+ FileSystemChildMetadata dest{rootId,
+ firstChildDescendantMeta.childName()};
+ TEST_TRY_UNWRAP(EntryId moved, dm->MoveEntry(src, dest));
+ ASSERT_FALSE(moved.IsEmpty());
+ testFile = moved;
+
+ FileSystemChildMetadata oldLocation{firstChildDir,
+ firstChildDescendantMeta.childName()};
+
+ // Is there still something out there?
+ TEST_TRY_UNWRAP_ERR(nsresult rv,
+ dm->GetOrCreateFile(oldLocation, /* create */ false));
+ ASSERT_NSEQ(NS_ERROR_DOM_NOT_FOUND_ERR, rv);
+
+ TEST_TRY_UNWRAP(EntryId firstChildDescendantCheck,
+ dm->GetOrCreateDirectory(oldLocation, /* create */ true));
+ ASSERT_EQ(firstChildDescendant, firstChildDescendantCheck);
+ }
+
+ {
+ // Try to move subSubDirectory one level up to quietly overwrite a file
+ FileSystemEntryMetadata src{firstChildDescendant,
+ firstChildDescendantMeta.childName(),
+ /* is directory */ true};
+ FileSystemChildMetadata dest{rootId,
+ firstChildDescendantMeta.childName()};
+ TEST_TRY_UNWRAP(EntryId moved, dm->MoveEntry(src, dest));
+ ASSERT_FALSE(moved.IsEmpty());
+ firstChildDescendant = moved;
+ }
+
+ {
+ // Move subSubDirectory back one level down and recreate the file
+ FileSystemEntryMetadata src{firstChildDescendant,
+ firstChildDescendantMeta.childName(),
+ /* is directory */ true};
+ FileSystemChildMetadata dest{firstChildDir,
+ firstChildDescendantMeta.childName()};
+ TEST_TRY_UNWRAP(EntryId moved, dm->MoveEntry(src, dest));
+ ASSERT_FALSE(moved.IsEmpty());
+ firstChildDescendant = moved;
+
+ FileSystemChildMetadata oldLocation{rootId,
+ firstChildDescendantMeta.childName()};
+
+ // We should no longer find anything there
+ TEST_TRY_UNWRAP_ERR(nsresult rv, dm->GetOrCreateDirectory(
+ oldLocation, /* create */ false));
+ ASSERT_NSEQ(NS_ERROR_DOM_NOT_FOUND_ERR, rv);
+
+ TEST_TRY_UNWRAP(EntryId testFileCheck,
+ dm->GetOrCreateFile(oldLocation, /* create */ true));
+ ASSERT_NO_FATAL_FAILURE(AssertEntryIdCollision(testFile, testFileCheck));
+ testFile = testFileCheck;
+ }
+
+ // Create a new file in the subsubdirectory
+ FileSystemChildMetadata newFileMeta{firstChildDescendant,
+ testFileMeta.childName()};
+ EntryId oldFirstChildDescendant = firstChildDescendant;
+
+ TEST_TRY_UNWRAP(EntryId newFile,
+ dm->GetOrCreateFile(newFileMeta, /* create */ true));
+
+ {
+ TEST_TRY_UNWRAP(Path entryPath, dm->Resolve({rootId, newFile}));
+ ASSERT_EQ(3u, entryPath.Length());
+ ASSERT_STREQ(firstChildMeta.childName(), entryPath[0]);
+ ASSERT_STREQ(firstChildDescendantMeta.childName(), entryPath[1]);
+ ASSERT_STREQ(testFileMeta.childName(), entryPath[2]);
+ }
+
+ {
+ // Move subSubDirectory one level up and rename it to testFile's old name
+ FileSystemEntryMetadata src{firstChildDescendant,
+ firstChildDescendantMeta.childName(),
+ /* is directory */ true};
+ FileSystemChildMetadata dest{rootId, testFileMeta.childName()};
+ TEST_TRY_UNWRAP(EntryId moved, dm->MoveEntry(src, dest));
+ ASSERT_FALSE(moved.IsEmpty());
+ firstChildDescendant = moved;
+ }
+
+ {
+ // Try to get handles to the moved items
+ TEST_TRY_UNWRAP_ERR(nsresult rv,
+ dm->GetOrCreateDirectory(firstChildDescendantMeta,
+ /* create */ false));
+ ASSERT_NSEQ(NS_ERROR_DOM_NOT_FOUND_ERR, rv);
+
+ // Still under the same parent which was moved
+ if (1 == sVersion) {
+ TEST_TRY_UNWRAP(EntryId handle,
+ dm->GetOrCreateFile(newFileMeta, /* create */ false));
+ ASSERT_EQ(handle, newFile);
+
+ TEST_TRY_UNWRAP(
+ handle, dm->GetOrCreateDirectory({rootId, testFileMeta.childName()},
+ /* create */ false));
+ ASSERT_EQ(handle, firstChildDescendant);
+ } else if (2 == sVersion) {
+ TEST_TRY_UNWRAP_ERR(
+ rv, dm->GetOrCreateFile(newFileMeta, /* create */ false));
+ ASSERT_NSEQ(NS_ERROR_DOM_NOT_FOUND_ERR, rv);
+
+ TEST_TRY_UNWRAP(
+ EntryId newFileCheck,
+ dm->GetOrCreateFile({firstChildDescendant, newFileMeta.childName()},
+ /* create */ false));
+ ASSERT_FALSE(newFileCheck.IsEmpty());
+ } else {
+ ASSERT_FALSE(false)
+ << "Unknown database version";
+ }
+ }
+
+ {
+ // Check that new file path is as expected
+ TEST_TRY_UNWRAP(
+ EntryId newFileCheck,
+ dm->GetOrCreateFile({firstChildDescendant, newFileMeta.childName()},
+ /* create */ false));
+ ASSERT_NO_FATAL_FAILURE(AssertEntryIdMoved(newFileCheck, newFile));
+ newFile = newFileCheck;
+
+ TEST_TRY_UNWRAP(Path entryPath, dm->Resolve({rootId, newFile}));
+ ASSERT_EQ(2u, entryPath.Length());
+ ASSERT_STREQ(testFileMeta.childName(), entryPath[0]);
+ ASSERT_STREQ(testFileMeta.childName(), entryPath[1]);
+ }
+
+ // Move first file and subSubDirectory back one level down keeping the names
+ {
+ FileSystemEntryMetadata src{testFile,
+ firstChildDescendantMeta.childName(),
+ /* is directory */ false};
+ FileSystemChildMetadata dest{firstChildDir,
+ firstChildDescendantMeta.childName()};
+
+ // Flag is ignored
+ TEST_TRY_UNWRAP(EntryId moved, dm->MoveEntry(src, dest));
+ ASSERT_FALSE(moved.IsEmpty());
+ testFile = moved;
+ }
+
+ {
+ // Then move the directory
+ FileSystemEntryMetadata src{firstChildDescendant,
+ testFileMeta.childName(),
+ /* is directory */ true};
+ FileSystemChildMetadata dest{firstChildDir, testFileMeta.childName()};
+
+ // Flag is ignored
+ TEST_TRY_UNWRAP(EntryId moved, dm->MoveEntry(src, dest));
+ ASSERT_FALSE(moved.IsEmpty());
+ firstChildDescendant = moved;
+ }
+
+ // Check that listings are as expected
+ {
+ TEST_TRY_UNWRAP(FileSystemDirectoryListing contents,
+ dm->GetDirectoryEntries(rootId, 0u));
+ ASSERT_TRUE(contents.files().IsEmpty());
+ ASSERT_EQ(1u, contents.directories().Length());
+ ASSERT_STREQ(firstChildMeta.childName(),
+ contents.directories()[0].entryName());
+ }
+
+ {
+ TEST_TRY_UNWRAP(FileSystemDirectoryListing contents,
+ dm->GetDirectoryEntries(firstChildDir, 0u));
+ ASSERT_EQ(1u, contents.files().Length());
+ ASSERT_EQ(1u, contents.directories().Length());
+ ASSERT_STREQ(firstChildDescendantMeta.childName(),
+ contents.files()[0].entryName());
+ ASSERT_STREQ(testFileMeta.childName(),
+ contents.directories()[0].entryName());
+ }
+
+ {
+ TEST_TRY_UNWRAP(FileSystemDirectoryListing contents,
+ dm->GetDirectoryEntries(firstChildDescendant, 0u));
+ ASSERT_EQ(1u, contents.files().Length());
+ ASSERT_TRUE(contents.directories().IsEmpty());
+ ASSERT_STREQ(testFileMeta.childName(), contents.files()[0].entryName());
+ }
+
+ // Names are swapped
+ {
+ TEST_TRY_UNWRAP(Path entryPath, dm->Resolve({rootId, testFile}));
+ ASSERT_EQ(2u, entryPath.Length());
+ ASSERT_STREQ(firstChildMeta.childName(), entryPath[0]);
+ ASSERT_STREQ(firstChildDescendantMeta.childName(), entryPath[1]);
+ }
+
+ {
+ TEST_TRY_UNWRAP(Path entryPath,
+ dm->Resolve({rootId, firstChildDescendant}));
+ ASSERT_EQ(2u, entryPath.Length());
+ ASSERT_STREQ(firstChildMeta.childName(), entryPath[0]);
+ ASSERT_STREQ(testFileMeta.childName(), entryPath[1]);
+ }
+
+ {
+ // Check that new file path is also as expected
+ TEST_TRY_UNWRAP(
+ EntryId newFileCheck,
+ dm->GetOrCreateFile({firstChildDescendant, newFileMeta.childName()},
+ /* create */ false));
+ ASSERT_NO_FATAL_FAILURE(AssertEntryIdMoved(newFileCheck, newFile));
+ newFile = newFileCheck;
+
+ TEST_TRY_UNWRAP(Path entryPath, dm->Resolve({rootId, newFile}));
+ ASSERT_EQ(3u, entryPath.Length());
+ ASSERT_STREQ(firstChildMeta.childName(), entryPath[0]);
+ ASSERT_STREQ(testFileMeta.childName(), entryPath[1]);
+ ASSERT_STREQ(testFileMeta.childName(), entryPath[2]);
+ }
+
+ {
+ // Try to get handles to the old items
+ TEST_TRY_UNWRAP_ERR(
+ nsresult rv, dm->GetOrCreateFile({rootId, testFileMeta.childName()},
+ /* create */ false));
+ ASSERT_NSEQ(NS_ERROR_DOM_NOT_FOUND_ERR, rv);
+
+ TEST_TRY_UNWRAP_ERR(
+ rv,
+ dm->GetOrCreateFile({rootId, firstChildDescendantMeta.childName()},
+ /* create */ false));
+ ASSERT_NSEQ(NS_ERROR_DOM_NOT_FOUND_ERR, rv);
+
+ TEST_TRY_UNWRAP_ERR(
+ rv, dm->GetOrCreateDirectory({rootId, testFileMeta.childName()},
+ /* create */ false));
+ ASSERT_NSEQ(NS_ERROR_DOM_NOT_FOUND_ERR, rv);
+
+ TEST_TRY_UNWRAP_ERR(rv,
+ dm->GetOrCreateDirectory(
+ {rootId, firstChildDescendantMeta.childName()},
+ /* create */ false));
+ ASSERT_NSEQ(NS_ERROR_DOM_NOT_FOUND_ERR, rv);
+
+ TEST_TRY_UNWRAP_ERR(
+ rv, dm->GetOrCreateFile({firstChildDir, testFileMeta.childName()},
+ /* create */ false));
+ ASSERT_NSEQ(NS_ERROR_DOM_TYPE_MISMATCH_ERR, rv);
+
+ TEST_TRY_UNWRAP_ERR(
+ rv, dm->GetOrCreateDirectory(
+ {firstChildDir, firstChildDescendantMeta.childName()},
+ /* create */ false));
+ ASSERT_NSEQ(NS_ERROR_DOM_TYPE_MISMATCH_ERR, rv);
+
+ TEST_TRY_UNWRAP_ERR(
+ rv, dm->GetOrCreateFile({testFile, newFileMeta.childName()},
+ /* create */ false));
+ ASSERT_NSEQ(NS_ERROR_DOM_NOT_FOUND_ERR, rv);
+ }
+ };
+
+ PerformOnIOThread(std::move(ioTask));
+}
+
+INSTANTIATE_TEST_SUITE_P(TestDatabaseManagerVersions,
+ TestFileSystemDatabaseManagerVersions,
+ testing::Values(1, 2));
+
+} // namespace mozilla::dom::fs::test
diff --git a/dom/fs/test/gtest/parent/datamodel/moz.build b/dom/fs/test/gtest/parent/datamodel/moz.build
new file mode 100644
index 0000000000..6369aec649
--- /dev/null
+++ b/dom/fs/test/gtest/parent/datamodel/moz.build
@@ -0,0 +1,22 @@
+# -*- 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 = [
+ "TestFileSystemDataManager.cpp",
+ "TestFileSystemDataManagerVersions.cpp",
+]
+
+include("/ipc/chromium/chromium-config.mozbuild")
+
+FINAL_LIBRARY = "xul-gtest"
+
+LOCAL_INCLUDES += [
+ "/dom/fs/include",
+ "/dom/fs/parent",
+ "/dom/fs/parent/datamodel",
+ "/dom/fs/test/gtest",
+ "/dom/fs/test/gtest/parent",
+]
diff --git a/dom/fs/test/gtest/parent/moz.build b/dom/fs/test/gtest/parent/moz.build
new file mode 100644
index 0000000000..9197c6ade2
--- /dev/null
+++ b/dom/fs/test/gtest/parent/moz.build
@@ -0,0 +1,21 @@
+# -*- 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/.
+
+TEST_DIRS += ["datamodel"]
+
+UNIFIED_SOURCES = [
+ "TestFileSystemHashSource.cpp",
+ "TestFileSystemQuotaClient.cpp",
+]
+
+include("/ipc/chromium/chromium-config.mozbuild")
+
+FINAL_LIBRARY = "xul-gtest"
+
+LOCAL_INCLUDES += [
+ "/dom/fs/parent",
+ "/dom/fs/test/gtest",
+]