// Copyright Epic Games, Inc. All Rights Reserved. #if ZEN_WITH_TESTS # include "zenserver-test.h" # include # include # include # include # include # include # include # include # include # include # include # include # include namespace zen::tests { using namespace std::literals; std::vector> GenerateFolderContent(const std::filesystem::path& RootPath) { CreateDirectories(RootPath); std::vector> 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> GenerateFolderContent2(const std::filesystem::path& RootPath) { std::vector> 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.SetDataDir(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.SetDataDir(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.SetDataDir(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.SetDataDir(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.SetDataDir(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::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(Files.size()); std::vector 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 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