aboutsummaryrefslogtreecommitdiff
path: root/src/zenserver-test/workspace-tests.cpp
diff options
context:
space:
mode:
authorStefan Boberg <[email protected]>2025-09-29 13:15:16 +0200
committerGitHub Enterprise <[email protected]>2025-09-29 13:15:16 +0200
commitd4c6e547a7081b1562a69dc9839d24cb82681c5d (patch)
tree3ffe43dcf09bb6d01c2fb860bb1f73882f44827d /src/zenserver-test/workspace-tests.cpp
parentgracefully handle missing chunks when exporting an oplog (#526) (diff)
downloadzen-d4c6e547a7081b1562a69dc9839d24cb82681c5d.tar.xz
zen-d4c6e547a7081b1562a69dc9839d24cb82681c5d.zip
split zenserver-test monolith into multiple source files (#528)
Diffstat (limited to 'src/zenserver-test/workspace-tests.cpp')
-rw-r--r--src/zenserver-test/workspace-tests.cpp541
1 files changed, 541 insertions, 0 deletions
diff --git a/src/zenserver-test/workspace-tests.cpp b/src/zenserver-test/workspace-tests.cpp
new file mode 100644
index 000000000..f299b6dcf
--- /dev/null
+++ b/src/zenserver-test/workspace-tests.cpp
@@ -0,0 +1,541 @@
+// Copyright Epic Games, Inc. All Rights Reserved.
+
+#if ZEN_WITH_TESTS
+# include "zenserver-test.h"
+# include <zencore/testing.h>
+# include <zencore/testutils.h>
+# include <zencore/workthreadpool.h>
+# include <zencore/compactbinarybuilder.h>
+# include <zencore/compactbinarypackage.h>
+# include <zencore/compress.h>
+# include <zencore/fmtutils.h>
+# include <zencore/filesystem.h>
+# include <zencore/stream.h>
+# include <zencore/string.h>
+# include <zenutil/zenserverprocess.h>
+# include <zenutil/chunkrequests.h>
+# include <zenhttp/httpclient.h>
+
+namespace zen::tests {
+
+using namespace std::literals;
+
+std::vector<std::pair<std::filesystem::path, IoBuffer>>
+GenerateFolderContent(const std::filesystem::path& RootPath)
+{
+ CreateDirectories(RootPath);
+ std::vector<std::pair<std::filesystem::path, IoBuffer>> Result;
+ Result.push_back(std::make_pair(RootPath / "root_blob_1.bin", CreateRandomBlob(4122)));
+ Result.push_back(std::make_pair(RootPath / "root_blob_2.bin", CreateRandomBlob(2122)));
+
+ std::filesystem::path EmptyFolder(RootPath / "empty_folder");
+
+ std::filesystem::path FirstFolder(RootPath / "first_folder");
+ CreateDirectories(FirstFolder);
+ Result.push_back(std::make_pair(FirstFolder / "first_folder_blob1.bin", CreateRandomBlob(22)));
+ Result.push_back(std::make_pair(FirstFolder / "first_folder_blob2.bin", CreateRandomBlob(122)));
+
+ std::filesystem::path SecondFolder(RootPath / "second_folder");
+ CreateDirectories(SecondFolder);
+ Result.push_back(std::make_pair(SecondFolder / "second_folder_blob1.bin", CreateRandomBlob(522)));
+ Result.push_back(std::make_pair(SecondFolder / "second_folder_blob2.bin", CreateRandomBlob(122)));
+ Result.push_back(std::make_pair(SecondFolder / "second_folder_blob3.bin", CreateRandomBlob(225)));
+
+ std::filesystem::path SecondFolderChild(SecondFolder / "child_in_second");
+ CreateDirectories(SecondFolderChild);
+ Result.push_back(std::make_pair(SecondFolderChild / "second_child_folder_blob1.bin", CreateRandomBlob(622)));
+
+ for (const auto& It : Result)
+ {
+ WriteFile(It.first, It.second);
+ }
+
+ return Result;
+}
+
+std::vector<std::pair<std::filesystem::path, IoBuffer>>
+GenerateFolderContent2(const std::filesystem::path& RootPath)
+{
+ std::vector<std::pair<std::filesystem::path, IoBuffer>> Result;
+ Result.push_back(std::make_pair(RootPath / "root_blob_3.bin", CreateRandomBlob(312)));
+ std::filesystem::path FirstFolder(RootPath / "first_folder");
+ Result.push_back(std::make_pair(FirstFolder / "first_folder_blob3.bin", CreateRandomBlob(722)));
+ std::filesystem::path SecondFolder(RootPath / "second_folder");
+ std::filesystem::path SecondFolderChild(SecondFolder / "child_in_second");
+ Result.push_back(std::make_pair(SecondFolderChild / "second_child_folder_blob2.bin", CreateRandomBlob(962)));
+ Result.push_back(std::make_pair(SecondFolderChild / "second_child_folder_blob3.bin", CreateRandomBlob(561)));
+
+ for (const auto& It : Result)
+ {
+ WriteFile(It.first, It.second);
+ }
+
+ return Result;
+}
+
+TEST_CASE("workspaces.create")
+{
+ using namespace std::literals;
+
+ std::filesystem::path SystemRootPath = TestEnv.CreateNewTestDir();
+
+ std::filesystem::path TestDir = TestEnv.CreateNewTestDir();
+ ZenServerInstance Instance(TestEnv);
+ Instance.SetTestDir(TestDir);
+ const uint16_t PortNumber = Instance.SpawnServerAndWaitUntilReady(
+ fmt::format("--workspaces-enabled --workspaces-allow-changes --system-dir {}", SystemRootPath));
+ CHECK(PortNumber != 0);
+
+ ScopedTemporaryDirectory TempDir;
+ std::filesystem::path Root1Path = TempDir.Path() / "root1";
+ std::filesystem::path Root2Path = TempDir.Path() / "root2";
+ DeleteDirectories(Root1Path);
+ DeleteDirectories(Root2Path);
+
+ std::filesystem::path Share1Path = "shared_1";
+ std::filesystem::path Share2Path = "shared_2";
+ CreateDirectories(Root1Path / Share1Path);
+ CreateDirectories(Root1Path / Share2Path);
+ CreateDirectories(Root2Path / Share1Path);
+ CreateDirectories(Root2Path / Share2Path);
+
+ Oid Root1Id = Oid::Zero;
+ Oid Root2Id = Oid::NewOid();
+
+ HttpClient Client(Instance.GetBaseUri());
+
+ CHECK(Client.Put(fmt::format("/ws/{}", Root1Id)).StatusCode == HttpResponseCode::BadRequest);
+
+ if (HttpClient::Response Root1Response =
+ Client.Put(fmt::format("/ws/{}", Oid::Zero), HttpClient::KeyValueMap{{"root_path", Root1Path.string()}});
+ Root1Response.StatusCode == HttpResponseCode::Created)
+ {
+ Root1Id = Oid::TryFromHexString(Root1Response.AsText());
+ CHECK(Root1Id != Oid::Zero);
+ }
+ else
+ {
+ CHECK(false);
+ }
+ if (HttpClient::Response Root1Response =
+ Client.Put(fmt::format("/ws/{}", Oid::Zero), HttpClient::KeyValueMap{{"root_path", Root1Path.string()}});
+ Root1Response.StatusCode == HttpResponseCode::OK)
+ {
+ CHECK(Root1Id == Oid::TryFromHexString(Root1Response.AsText()));
+ }
+ else
+ {
+ CHECK(false);
+ }
+ if (HttpClient::Response Root1Response =
+ Client.Put(fmt::format("/ws/{}", Root1Id), HttpClient::KeyValueMap{{"root_path", Root1Path.string()}});
+ Root1Response.StatusCode == HttpResponseCode::OK)
+ {
+ CHECK(Root1Id == Oid::TryFromHexString(Root1Response.AsText()));
+ }
+ else
+ {
+ CHECK(false);
+ }
+ CHECK(Client.Put(fmt::format("/ws/{}", Root1Id), HttpClient::KeyValueMap{{"root_path", Root2Path.string()}}).StatusCode ==
+ HttpResponseCode::Conflict);
+
+ CHECK(
+ Client.Put(fmt::format("/ws/{}/{}", Root1Id, Oid::Zero), HttpClient::KeyValueMap{{"share_path", Share2Path.string()}}).StatusCode ==
+ HttpResponseCode::Created);
+
+ CHECK(
+ Client.Put(fmt::format("/ws/{}/{}", Root2Id, Oid::Zero), HttpClient::KeyValueMap{{"share_path", Share2Path.string()}}).StatusCode ==
+ HttpResponseCode::NotFound);
+
+ CHECK(Client.Put(fmt::format("/ws/{}", Root2Id), HttpClient::KeyValueMap{{"root_path", Root1Path.string()}}).StatusCode ==
+ HttpResponseCode::Conflict);
+
+ if (HttpClient::Response Root2Response =
+ Client.Put(fmt::format("/ws/{}", Root2Id), HttpClient::KeyValueMap{{"root_path", Root2Path.string()}});
+ Root2Response.StatusCode == HttpResponseCode::Created)
+ {
+ CHECK(Root2Id == Oid::TryFromHexString(Root2Response.AsText()));
+ }
+ else
+ {
+ CHECK(false);
+ }
+
+ CHECK(Client.Put(fmt::format("/ws/{}/{}", Root2Id, Oid::Zero)).StatusCode == HttpResponseCode::BadRequest);
+
+ Oid Share2Id = Oid::Zero;
+ if (HttpClient::Response Share2Response =
+ Client.Put(fmt::format("/ws/{}/{}", Root2Id, Share2Id), HttpClient::KeyValueMap{{"share_path", Share2Path.string()}});
+ Share2Response.StatusCode == HttpResponseCode::Created)
+ {
+ Share2Id = Oid::TryFromHexString(Share2Response.AsText());
+ CHECK(Share2Id != Oid::Zero);
+ }
+ else
+ {
+ CHECK(false);
+ }
+
+ CHECK(
+ Client.Put(fmt::format("/ws/{}/{}", Root2Id, Oid::Zero), HttpClient::KeyValueMap{{"share_path", Share2Path.string()}}).StatusCode ==
+ HttpResponseCode::OK);
+
+ CHECK(
+ Client.Put(fmt::format("/ws/{}/{}", Root2Id, Share2Id), HttpClient::KeyValueMap{{"share_path", Share2Path.string()}}).StatusCode ==
+ HttpResponseCode::OK);
+
+ CHECK(
+ Client.Put(fmt::format("/ws/{}/{}", Root2Id, Share2Id), HttpClient::KeyValueMap{{"share_path", Share1Path.string()}}).StatusCode ==
+ HttpResponseCode::Conflict);
+
+ CHECK(Client.Put(fmt::format("/ws/{}/{}", Root2Id, Oid::NewOid()), HttpClient::KeyValueMap{{"share_path", Share2Path.string()}})
+ .StatusCode == HttpResponseCode::Conflict);
+
+ CHECK(Client.Put(fmt::format("/ws/{}/{}", Root2Id, Oid::Zero), HttpClient::KeyValueMap{{"share_path", "idonotexist"}}).StatusCode !=
+ HttpResponseCode::OK);
+
+ while (true)
+ {
+ std::error_code Ec;
+ DeleteDirectories(Root2Path / Share2Path, Ec);
+ if (!Ec)
+ break;
+ }
+
+ CHECK(Client.Get(fmt::format("/ws/{}/{}/files", Root2Id, Share2Id)).StatusCode == HttpResponseCode::NotFound);
+}
+
+TEST_CASE("workspaces.restricted")
+{
+ using namespace std::literals;
+
+ std::filesystem::path SystemRootPath = TestEnv.CreateNewTestDir();
+
+ std::filesystem::path TestDir = TestEnv.CreateNewTestDir();
+ ZenServerInstance Instance(TestEnv);
+ Instance.SetTestDir(TestDir);
+ const uint16_t PortNumber = Instance.SpawnServerAndWaitUntilReady(fmt::format("--workspaces-enabled --system-dir {}", SystemRootPath));
+ CHECK(PortNumber != 0);
+
+ ScopedTemporaryDirectory TempDir;
+ std::filesystem::path Root1Path = TempDir.Path() / "root1";
+ std::filesystem::path Root2Path = TempDir.Path() / "root2";
+ DeleteDirectories(Root1Path);
+ DeleteDirectories(Root2Path);
+
+ std::filesystem::path Share1Path = "shared_1";
+ std::filesystem::path Share2Path = "shared_2";
+ CreateDirectories(Root1Path / Share1Path);
+ CreateDirectories(Root1Path / Share2Path);
+ CreateDirectories(Root2Path / Share1Path);
+ CreateDirectories(Root2Path / Share2Path);
+
+ Oid Root1Id = Oid::NewOid();
+ Oid Root2Id = Oid::NewOid();
+ Oid Share1Id = Oid::NewOid();
+ Oid Share2Id = Oid::NewOid();
+
+ HttpClient Client(Instance.GetBaseUri());
+ CHECK(Client.Put(fmt::format("/ws/{}", Oid::Zero), HttpClient::KeyValueMap{{"root_path", Root1Path.string()}}).StatusCode ==
+ HttpResponseCode::Unauthorized);
+
+ CHECK_EQ(Client.Get(fmt::format("/ws/{}", Root1Id)).StatusCode, HttpResponseCode::NotFound);
+
+ std::string Config1;
+ {
+ CbObjectWriter Config;
+ Config.BeginArray("workspaces");
+ Config.BeginObject();
+ Config << "id"sv << Root1Id.ToString();
+ Config << "root_path"sv << Root1Path.string();
+ Config << "allow_share_creation_from_http"sv << false;
+ Config.EndObject();
+ Config.EndArray();
+ ExtendableStringBuilder<256> SB;
+ CompactBinaryToJson(Config.Save(), SB);
+ Config1 = SB.ToString();
+ }
+ WriteFile(SystemRootPath / "workspaces" / "config.json", IoBuffer(IoBuffer::Wrap, Config1.data(), Config1.size()));
+
+ CHECK(IsHttpSuccessCode(Client.Get("/ws/refresh").StatusCode));
+
+ CHECK_EQ(Client.Get(fmt::format("/ws/{}", Root1Id)).StatusCode, HttpResponseCode::OK);
+
+ CHECK(Client.Get(fmt::format("/ws/{}/{}", Root1Id, Share1Id)).StatusCode == HttpResponseCode::NotFound);
+ CHECK(
+ Client.Put(fmt::format("/ws/{}/{}", Root1Id, Oid::Zero), HttpClient::KeyValueMap{{"share_path", Share1Path.string()}}).StatusCode ==
+ HttpResponseCode::Unauthorized);
+
+ std::string Config2;
+ {
+ CbObjectWriter Config;
+ Config.BeginArray("workspaces");
+ Config.BeginObject();
+ Config << "id"sv << Root1Id.ToString();
+ Config << "root_path"sv << Root1Path.string();
+ Config << "allow_share_creation_from_http"sv << false;
+ Config.EndObject();
+ Config.BeginObject();
+ Config << "id"sv << Root2Id.ToString();
+ Config << "root_path"sv << Root2Path.string();
+ Config << "allow_share_creation_from_http"sv << true;
+ Config.EndObject();
+ Config.EndArray();
+ ExtendableStringBuilder<256> SB;
+ CompactBinaryToJson(Config.Save(), SB);
+ Config2 = SB.ToString();
+ }
+ WriteFile(SystemRootPath / "workspaces" / "config.json", IoBuffer(IoBuffer::Wrap, Config2.data(), Config2.size()));
+
+ CHECK(IsHttpSuccessCode(Client.Get("/ws/refresh").StatusCode));
+
+ CHECK_EQ(Client.Get(fmt::format("/ws/{}", Root2Id)).StatusCode, HttpResponseCode::OK);
+
+ CHECK(Client.Get(fmt::format("/ws/{}/{}", Root2Id, Share2Id)).StatusCode == HttpResponseCode::NotFound);
+ CHECK(
+ Client.Put(fmt::format("/ws/{}/{}", Root2Id, Share2Id), HttpClient::KeyValueMap{{"share_path", Share2Path.string()}}).StatusCode ==
+ HttpResponseCode::Created);
+ CHECK(Client.Get(fmt::format("/ws/{}/{}", Root2Id, Share2Id)).StatusCode == HttpResponseCode::OK);
+
+ CHECK(IsHttpSuccessCode(Client.Delete(fmt::format("/ws/{}/{}", Root2Id, Share2Id)).StatusCode));
+}
+
+TEST_CASE("workspaces.lifetimes")
+{
+ using namespace std::literals;
+
+ std::filesystem::path SystemRootPath = TestEnv.CreateNewTestDir();
+
+ Oid WorkspaceId = Oid::NewOid();
+ Oid ShareId = Oid::NewOid();
+
+ ScopedTemporaryDirectory TempDir;
+ std::filesystem::path RootPath = TempDir.Path();
+ DeleteDirectories(RootPath);
+ std::filesystem::path SharePath = RootPath / "shared_folder";
+ CreateDirectories(SharePath);
+
+ {
+ std::filesystem::path TestDir = TestEnv.CreateNewTestDir();
+ ZenServerInstance Instance(TestEnv);
+ Instance.SetTestDir(TestDir);
+ const uint16_t PortNumber = Instance.SpawnServerAndWaitUntilReady(
+ fmt::format("--workspaces-enabled --workspaces-allow-changes --system-dir {}", SystemRootPath));
+ CHECK(PortNumber != 0);
+
+ HttpClient Client(Instance.GetBaseUri());
+ CHECK(Client.Put(fmt::format("/ws/{}", WorkspaceId), HttpClient::KeyValueMap{{"root_path", RootPath.string()}}).StatusCode ==
+ HttpResponseCode::Created);
+ CHECK(Client.Get(fmt::format("/ws/{}", WorkspaceId)).AsObject()["id"sv].AsObjectId() == WorkspaceId);
+ CHECK(Client.Put(fmt::format("/ws/{}", WorkspaceId), HttpClient::KeyValueMap{{"root_path", RootPath.string()}}).StatusCode ==
+ HttpResponseCode::OK);
+
+ CHECK(Client.Put(fmt::format("/ws/{}/{}", WorkspaceId, ShareId), HttpClient::KeyValueMap{{"share_path", "shared_folder"}})
+ .StatusCode == HttpResponseCode::Created);
+ CHECK(Client.Get(fmt::format("/ws/{}/{}", WorkspaceId, ShareId)).AsObject()["id"sv].AsObjectId() == ShareId);
+ CHECK(Client.Put(fmt::format("/ws/{}/{}", WorkspaceId, ShareId), HttpClient::KeyValueMap{{"share_path", "shared_folder"}})
+ .StatusCode == HttpResponseCode::OK);
+ }
+
+ // Restart
+
+ {
+ std::filesystem::path TestDir = TestEnv.CreateNewTestDir();
+ ZenServerInstance Instance(TestEnv);
+ Instance.SetTestDir(TestDir);
+ const uint16_t PortNumber =
+ Instance.SpawnServerAndWaitUntilReady(fmt::format("--workspaces-enabled --system-dir {}", SystemRootPath));
+ CHECK(PortNumber != 0);
+
+ HttpClient Client(Instance.GetBaseUri());
+ CHECK(Client.Get(fmt::format("/ws/{}", WorkspaceId)).AsObject()["id"sv].AsObjectId() == WorkspaceId);
+
+ CHECK(Client.Get(fmt::format("/ws/{}/{}", WorkspaceId, ShareId)).AsObject()["id"sv].AsObjectId() == ShareId);
+ }
+
+ // Wipe system config
+ DeleteDirectories(SystemRootPath);
+
+ // Restart
+
+ {
+ std::filesystem::path TestDir = TestEnv.CreateNewTestDir();
+ ZenServerInstance Instance(TestEnv);
+ Instance.SetTestDir(TestDir);
+ const uint16_t PortNumber =
+ Instance.SpawnServerAndWaitUntilReady(fmt::format("--workspaces-enabled --system-dir {}", SystemRootPath));
+ CHECK(PortNumber != 0);
+
+ HttpClient Client(Instance.GetBaseUri());
+ CHECK(Client.Get(fmt::format("/ws/{}", WorkspaceId)).StatusCode == HttpResponseCode::NotFound);
+ CHECK(Client.Get(fmt::format("/ws/{}/{}", WorkspaceId, ShareId)).StatusCode == HttpResponseCode::NotFound);
+ }
+}
+
+TEST_CASE("workspaces.share")
+{
+ std::filesystem::path SystemRootPath = TestEnv.CreateNewTestDir();
+
+ ZenServerInstance Instance(TestEnv);
+
+ const uint16_t PortNumber = Instance.SpawnServerAndWaitUntilReady(
+ fmt::format("--workspaces-enabled --workspaces-allow-changes --system-dir {}", SystemRootPath));
+ CHECK(PortNumber != 0);
+
+ ScopedTemporaryDirectory TempDir;
+ std::filesystem::path RootPath = TempDir.Path();
+ DeleteDirectories(RootPath);
+ std::filesystem::path SharePath = RootPath / "shared_folder";
+ GenerateFolderContent(SharePath);
+
+ HttpClient Client(Instance.GetBaseUri());
+
+ Oid WorkspaceId = Oid::NewOid();
+ CHECK(Client.Put(fmt::format("/ws/{}", WorkspaceId), HttpClient::KeyValueMap{{"root_path", RootPath.string()}}).StatusCode ==
+ HttpResponseCode::Created);
+ CHECK(Client.Get(fmt::format("/ws/{}", WorkspaceId)).AsObject()["id"sv].AsObjectId() == WorkspaceId);
+
+ Oid ShareId = Oid::NewOid();
+ CHECK(Client.Put(fmt::format("/ws/{}/{}", WorkspaceId, ShareId), HttpClient::KeyValueMap{{"share_path", "shared_folder"}}).StatusCode ==
+ HttpResponseCode::Created);
+ CHECK(Client.Get(fmt::format("/ws/{}/{}", WorkspaceId, ShareId)).AsObject()["id"sv].AsObjectId() == ShareId);
+
+ CHECK(Client.Get(fmt::format("/ws/{}/{}/files", WorkspaceId, ShareId)).AsObject()["files"sv].AsArrayView().Num() == 8);
+ GenerateFolderContent2(SharePath);
+ CHECK(Client.Get(fmt::format("/ws/{}/{}/files", WorkspaceId, ShareId)).AsObject()["files"sv].AsArrayView().Num() == 8);
+ HttpClient::Response FilesResponse =
+ Client.Get(fmt::format("/ws/{}/{}/files", WorkspaceId, ShareId),
+ {},
+ HttpClient::KeyValueMap{{"refresh", ToString(true)}, {"fieldnames", "id,clientpath,size"}});
+ CHECK(FilesResponse);
+ std::unordered_map<Oid, std::pair<std::filesystem::path, uint64_t>, Oid::Hasher> Files;
+ {
+ CbArrayView FilesArray = FilesResponse.AsObject()["files"sv].AsArrayView();
+ CHECK(FilesArray.Num() == 12);
+ for (CbFieldView Field : FilesArray)
+ {
+ CbObjectView FileObject = Field.AsObjectView();
+ Oid ChunkId = FileObject["id"sv].AsObjectId();
+ CHECK(ChunkId != Oid::Zero);
+ uint64_t Size = FileObject["size"sv].AsUInt64();
+ std::u8string_view Path = FileObject["clientpath"sv].AsU8String();
+ std::filesystem::path AbsFilePath = SharePath / Path;
+ CHECK(IsFile(AbsFilePath));
+ CHECK(FileSizeFromPath(AbsFilePath) == Size);
+ Files.insert_or_assign(ChunkId, std::make_pair(AbsFilePath, Size));
+ }
+ }
+
+ HttpClient::Response EntriesResponse =
+ Client.Get(fmt::format("/ws/{}/{}/entries", WorkspaceId, ShareId), {}, HttpClient::KeyValueMap{{"fieldfilter", "id,clientpath"}});
+ CHECK(EntriesResponse);
+ {
+ CbArrayView EntriesArray = EntriesResponse.AsObject()["entries"sv].AsArrayView();
+ CHECK(EntriesArray.Num() == 1);
+ for (CbFieldView EntryField : EntriesArray)
+ {
+ CbObjectView EntryObject = EntryField.AsObjectView();
+ CbArrayView FilesArray = EntryObject["files"sv].AsArrayView();
+ CHECK(FilesArray.Num() == 12);
+ for (CbFieldView FileField : FilesArray)
+ {
+ CbObjectView FileObject = FileField.AsObjectView();
+ Oid ChunkId = FileObject["id"sv].AsObjectId();
+ CHECK(ChunkId != Oid::Zero);
+ std::u8string_view Path = FileObject["clientpath"sv].AsU8String();
+ std::filesystem::path AbsFilePath = SharePath / Path;
+ CHECK(IsFile(AbsFilePath));
+ }
+ }
+ }
+
+ HttpClient::Response FileManifestResponse =
+ Client.Get(fmt::format("/ws/{}/{}/entries", WorkspaceId, ShareId),
+ {},
+ HttpClient::KeyValueMap{{"opkey", "file_manifest"}, {"fieldfilter", "id,clientpath"}});
+ CHECK(FileManifestResponse);
+ {
+ CbArrayView EntriesArray = FileManifestResponse.AsObject()["entry"sv].AsObjectView()["files"sv].AsArrayView();
+ CHECK(EntriesArray.Num() == 12);
+ for (CbFieldView Field : EntriesArray)
+ {
+ CbObjectView FileObject = Field.AsObjectView();
+ Oid ChunkId = FileObject["id"sv].AsObjectId();
+ CHECK(ChunkId != Oid::Zero);
+ std::u8string_view Path = FileObject["clientpath"sv].AsU8String();
+ std::filesystem::path AbsFilePath = SharePath / Path;
+ CHECK(IsFile(AbsFilePath));
+ }
+ }
+
+ for (auto It : Files)
+ {
+ const Oid& ChunkId = It.first;
+ const std::filesystem::path& Path = It.second.first;
+ const uint64_t Size = It.second.second;
+
+ CHECK(Client.Get(fmt::format("/ws/{}/{}/{}/info", WorkspaceId, ShareId, ChunkId)).AsObject()["size"sv].AsUInt64() == Size);
+
+ {
+ IoBuffer Payload = Client.Get(fmt::format("/ws/{}/{}/{}", WorkspaceId, ShareId, ChunkId)).ResponsePayload;
+ CHECK(Payload);
+ CHECK(Payload.GetSize() == Size);
+ IoBuffer FileContent = IoBufferBuilder::MakeFromFile(Path);
+ CHECK(FileContent);
+ CHECK(FileContent.GetView().EqualBytes(Payload.GetView()));
+ }
+
+ {
+ IoBuffer Payload =
+ Client
+ .Get(fmt::format("/ws/{}/{}/{}", WorkspaceId, ShareId, ChunkId),
+ {},
+ HttpClient::KeyValueMap{{"offset", fmt::format("{}", Size / 4)}, {"size", fmt::format("{}", Size / 2)}})
+ .ResponsePayload;
+ CHECK(Payload);
+ CHECK(Payload.GetSize() == Size / 2);
+ IoBuffer FileContent = IoBufferBuilder::MakeFromFile(Path, Size / 4, Size / 2);
+ CHECK(FileContent);
+ CHECK(FileContent.GetView().EqualBytes(Payload.GetView()));
+ }
+ }
+
+ {
+ uint32_t CorrelationId = gsl::narrow<uint32_t>(Files.size());
+ std::vector<RequestChunkEntry> BatchEntries;
+ for (auto It : Files)
+ {
+ const Oid& ChunkId = It.first;
+ const uint64_t Size = It.second.second;
+
+ BatchEntries.push_back(
+ RequestChunkEntry{.ChunkId = ChunkId, .CorrelationId = --CorrelationId, .Offset = Size / 4, .RequestBytes = Size / 2});
+ }
+ IoBuffer BatchResponse =
+ Client.Post(fmt::format("/ws/{}/{}/batch", WorkspaceId, ShareId), BuildChunkBatchRequest(BatchEntries)).ResponsePayload;
+ CHECK(BatchResponse);
+ std::vector<IoBuffer> BatchResult = ParseChunkBatchResponse(BatchResponse);
+ CHECK(BatchResult.size() == Files.size());
+ for (const RequestChunkEntry& Request : BatchEntries)
+ {
+ IoBuffer Result = BatchResult[Request.CorrelationId];
+ auto It = Files.find(Request.ChunkId);
+ const std::filesystem::path& Path = It->second.first;
+ CHECK(Result.GetSize() == Request.RequestBytes);
+ IoBuffer FileContent = IoBufferBuilder::MakeFromFile(Path, Request.Offset, Request.RequestBytes);
+ CHECK(FileContent);
+ CHECK(FileContent.GetView().EqualBytes(Result.GetView()));
+ }
+ }
+
+ CHECK(Client.Delete(fmt::format("/ws/{}/{}", WorkspaceId, ShareId)));
+ CHECK(Client.Get(fmt::format("/ws/{}/{}", WorkspaceId, ShareId)).StatusCode == HttpResponseCode::NotFound);
+ CHECK(Client.Get(fmt::format("/ws/{}", WorkspaceId)));
+
+ CHECK(Client.Delete(fmt::format("/ws/{}", WorkspaceId)));
+ CHECK(Client.Get(fmt::format("/ws/{}", WorkspaceId)).StatusCode == HttpResponseCode::NotFound);
+}
+
+} // namespace zen::tests
+#endif