diff options
Diffstat (limited to 'dom/fs/test/gtest/parent')
-rw-r--r-- | dom/fs/test/gtest/parent/TestFileSystemHashSource.cpp | 183 | ||||
-rw-r--r-- | dom/fs/test/gtest/parent/TestFileSystemQuotaClient.cpp | 474 | ||||
-rw-r--r-- | dom/fs/test/gtest/parent/datamodel/TestFileSystemDataManager.cpp | 185 | ||||
-rw-r--r-- | dom/fs/test/gtest/parent/datamodel/TestFileSystemDataManagerVersions.cpp | 1059 | ||||
-rw-r--r-- | dom/fs/test/gtest/parent/datamodel/moz.build | 22 | ||||
-rw-r--r-- | dom/fs/test/gtest/parent/moz.build | 21 |
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", +] |