diff --git a/tests/compat/CMakeLists.txt b/tests/compat/CMakeLists.txt index 1e60260ef..383554812 100644 --- a/tests/compat/CMakeLists.txt +++ b/tests/compat/CMakeLists.txt @@ -5,7 +5,7 @@ find_package(Qt6 COMPONENTS Core Concurrent QUIET) # ---- zrpc_tests: TcpBuffer / NetAddress / ZRpcCodeC / ZRpcController ---- if(TARGET zrpc) - add_executable(zrpc_tests zrpc_test.cpp) + add_executable(zrpc_tests zrpc_test.cpp zrpc_extra_test.cpp) target_link_libraries(zrpc_tests PRIVATE GTest::GTest GTest::Main @@ -26,12 +26,15 @@ endif() # ---- framework_tests: Event / EventChannel / EventSequence / EventDispatcher ---- if(TARGET dde-cooperation-framework) - add_executable(framework_tests event_test.cpp lifecycle_test.cpp) + add_executable(framework_tests event_test.cpp lifecycle_test.cpp lifecycle_private_test.cpp) target_link_libraries(framework_tests PRIVATE GTest::GTest GTest::Main dde-cooperation-framework ) + target_include_directories(framework_tests PRIVATE + ${CMAKE_SOURCE_DIR}/src/compat/framework/lifecycle/private + ) if(TARGET Qt6::Core) target_link_libraries(framework_tests PRIVATE Qt6::Core) endif() @@ -40,3 +43,18 @@ if(TARGET dde-cooperation-framework) endif() add_test(NAME framework_tests COMMAND framework_tests) endif() + +# ---- commonstruct_tests: commonstruct.h JSON serialization round-trips ---- +if(TARGET co) + add_executable(commonstruct_tests commonstruct_test.cpp) + target_link_libraries(commonstruct_tests PRIVATE + GTest::GTest + GTest::Main + co + ) + target_include_directories(commonstruct_tests PRIVATE + ${CMAKE_SOURCE_DIR}/src/compat/common + ${CMAKE_SOURCE_DIR}/3rdparty/coost/include + ) + add_test(NAME commonstruct_tests COMMAND commonstruct_tests) +endif() diff --git a/tests/compat/commonstruct_test.cpp b/tests/compat/commonstruct_test.cpp new file mode 100644 index 000000000..691c72f38 --- /dev/null +++ b/tests/compat/commonstruct_test.cpp @@ -0,0 +1,203 @@ +// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later + +#include + +#include "co/json.h" +#include "commonstruct.h" + +#include + +namespace { + +template +T roundtrip(const T& src) +{ + co::Json j = src.as_json(); + fastring dumped = j.str(); + co::Json parsed; + parsed.parse_from(dumped); + T dst; + dst.from_json(parsed); + return dst; +} + +} + +TEST(CommonStructTest, SendResultRoundtrip) +{ + SendResult src; + src.protocolType = 7u; + src.errorType = -3; + src.data = "payload"; + auto dst = roundtrip(src); + EXPECT_EQ(dst.protocolType, 7u); + EXPECT_EQ(dst.errorType, -3); + EXPECT_STREQ(dst.data.c_str(), "payload"); +} + +TEST(CommonStructTest, UserLoginInfoRoundtrip) +{ + UserLoginInfo src; + src.name = "alice"; + src.auth = "tok"; + src.my_uid = "1001"; + src.my_name = "me"; + src.session_id = "sess"; + src.selfappName = "coop"; + src.appName = "dde"; + src.version = "1.0"; + src.ip = "10.0.0.1"; + auto dst = roundtrip(src); + EXPECT_STREQ(dst.name.c_str(), "alice"); + EXPECT_STREQ(dst.auth.c_str(), "tok"); + EXPECT_STREQ(dst.ip.c_str(), "10.0.0.1"); + EXPECT_STREQ(dst.version.c_str(), "1.0"); +} + +TEST(CommonStructTest, PeerInfoRoundtrip) +{ + PeerInfo src; + src.username = "bob"; + src.hostname = "host"; + src.platform = "linux"; + src.version = "2.0"; + src.privacy_mode = true; + auto dst = roundtrip(src); + EXPECT_STREQ(dst.username.c_str(), "bob"); + EXPECT_TRUE(dst.privacy_mode); +} + +TEST(CommonStructTest, UserLoginResultInfoNestedRoundtrip) +{ + UserLoginResultInfo src; + src.peer.username = "carol"; + src.peer.privacy_mode = false; + src.token = "tkn"; + src.appName = "app"; + src.result = true; + auto dst = roundtrip(src); + EXPECT_STREQ(dst.peer.username.c_str(), "carol"); + EXPECT_FALSE(dst.peer.privacy_mode); + EXPECT_STREQ(dst.token.c_str(), "tkn"); + EXPECT_TRUE(dst.result); +} + +TEST(CommonStructTest, FileEntryRoundtrip) +{ + FileEntry src; + src.filetype = 1; + src.name = "a.txt"; + src.hidden = true; + src.size = 9999; + src.modified_time = 123456; + src.appName = "x"; + src.rcvappName = "y"; + auto dst = roundtrip(src); + EXPECT_EQ(src.filetype, dst.filetype); + EXPECT_EQ(src.size, dst.size); + EXPECT_TRUE(dst.hidden); + EXPECT_STREQ(dst.name.c_str(), "a.txt"); +} + +TEST(CommonStructTest, FileTransBlockRoundtrip) +{ + FileTransBlock src; + src.job_id = 5; + src.file_id = 6; + src.rootdir = "/r"; + src.filename = "f"; + src.blk_id = 11u; + src.flags = 2; + src.data_size = 4096; + auto dst = roundtrip(src); + EXPECT_EQ(dst.job_id, 5); + EXPECT_EQ(dst.blk_id, 11u); + EXPECT_EQ(dst.data_size, 4096); + EXPECT_STREQ(dst.rootdir.c_str(), "/r"); +} + +TEST(CommonStructTest, ShareEventsRoundtrip) +{ + ShareEvents src; + src.eventType = 42u; + src.data = "blob"; + auto dst = roundtrip(src); + EXPECT_EQ(dst.eventType, 42u); + EXPECT_STREQ(dst.data.c_str(), "blob"); +} + +TEST(CommonStructTest, PingPongRoundtrip) +{ + PingPong src; + src.appName = "a"; + src.tarAppname = "b"; + src.ip = "1.2.3.4"; + auto dst = roundtrip(src); + EXPECT_STREQ(dst.appName.c_str(), "a"); + EXPECT_STREQ(dst.tarAppname.c_str(), "b"); + EXPECT_STREQ(dst.ip.c_str(), "1.2.3.4"); +} + +TEST(CommonStructTest, DiscoverInfoRoundtrip) +{ + DiscoverInfo src; + src.ip = "9.9.9.9"; + src.msg = "hi"; + auto dst = roundtrip(src); + EXPECT_STREQ(dst.ip.c_str(), "9.9.9.9"); + EXPECT_STREQ(dst.msg.c_str(), "hi"); +} + +TEST(CommonStructTest, MiscInfoRoundtrip) +{ + MiscInfo src; + src.appName = "dde-cooperation"; + src.json = "{\"k\":1}"; + auto dst = roundtrip(src); + EXPECT_STREQ(dst.appName.c_str(), "dde-cooperation"); + EXPECT_STREQ(dst.json.c_str(), "{\"k\":1}"); +} + +TEST(CommonStructTest, FileTransResponseDefaultsAndRoundtrip) +{ + FileTransResponse src; + EXPECT_EQ(src.id, -1); + EXPECT_EQ(src.result, -1); + src.id = 99; + src.name = "resp"; + src.result = 0; + auto dst = roundtrip(src); + EXPECT_EQ(dst.id, 99); + EXPECT_EQ(dst.result, 0); + EXPECT_STREQ(dst.name.c_str(), "resp"); +} + +TEST(CommonStructTest, FileTransCreateNestedEntryRoundtrip) +{ + FileTransCreate src; + src.job_id = 3; + src.file_id = 4; + src.sub_dir = "sub"; + src.entry.name = "inner.txt"; + src.entry.size = 555; + auto dst = roundtrip(src); + EXPECT_EQ(dst.job_id, 3); + EXPECT_EQ(dst.file_id, 4); + EXPECT_STREQ(dst.sub_dir.c_str(), "sub"); + EXPECT_STREQ(dst.entry.name.c_str(), "inner.txt"); + EXPECT_EQ(dst.entry.size, 555); +} + +TEST(CommonStructTest, AsJsonProducesValidJsonString) +{ + ShareConnectApply src; + src.appName = "a"; + src.tarAppname = "b"; + src.ip = "1.1.1.1"; + src.tarIp = "2.2.2.2"; + src.data = "d"; + fastring s = src.as_json().str(); + EXPECT_NE(s.find("appName"), std::string::npos); + EXPECT_NE(s.find("tarIp"), std::string::npos); +} diff --git a/tests/compat/lifecycle_private_test.cpp b/tests/compat/lifecycle_private_test.cpp new file mode 100644 index 000000000..d8bba46f2 --- /dev/null +++ b/tests/compat/lifecycle_private_test.cpp @@ -0,0 +1,436 @@ +// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "pluginmanager_p.h" +#include "pluginmetaobject_p.h" + +DPF_USE_NAMESPACE + +namespace { +class DummyPlugin : public Plugin { +public: + bool start() override { return true; } +}; +} // namespace + +TEST(PluginBaseTest, InitializeAndStopAreSafe) +{ + DummyPlugin p; + EXPECT_NO_THROW(p.initialize()); + EXPECT_NO_THROW(p.stop()); +} + +TEST(PluginMetaObjectExtraTest, UncoveredDefaultGetters) +{ + PluginMetaObject obj; + EXPECT_TRUE(obj.description().isEmpty()); + EXPECT_TRUE(obj.category().isEmpty()); + EXPECT_TRUE(obj.urlLink().isEmpty()); + EXPECT_TRUE(obj.depends().isEmpty()); + EXPECT_EQ(obj.pluginState(), PluginMetaObject::kInvalid); + EXPECT_TRUE(obj.plugin().isNull()); +} + +TEST(PluginMetaObjectExtraTest, DebugStreamForMetaObjectAndPointer) +{ + PluginMetaObject obj; + QString captured; + { + QDebug dbg(&captured); + dbg << obj; + } + EXPECT_FALSE(captured.isEmpty()); + + PluginMetaObjectPointer ptr = PluginMetaObjectPointer::create(); + QString capturedPtr; + { + QDebug dbg(&capturedPtr); + dbg << ptr; + } + EXPECT_FALSE(capturedPtr.isEmpty()); +} + +TEST(PluginManagerPrivateJsonToMetaTest, FillsAllFieldsAndDepends) +{ + PluginMetaObjectPointer meta = PluginMetaObjectPointer::create(); + QJsonObject metaData; + metaData.insert(kPluginVersion, "1.2.3"); + metaData.insert(kPluginCategory, "util"); + metaData.insert(kPluginDescription, "a plugin"); + metaData.insert(kPluginUrlLink, "http://example.com"); + QJsonArray deps; + QJsonObject d1; + d1.insert(kPluginName, "core"); + d1.insert(kPluginVersion, "1.0"); + deps.append(d1); + metaData.insert(kPluginDepends, deps); + + PluginManagerPrivate::jsonToMeta(meta, metaData); + + EXPECT_EQ(meta->version(), "1.2.3"); + EXPECT_EQ(meta->category(), "util"); + EXPECT_EQ(meta->description(), "a plugin"); + EXPECT_EQ(meta->urlLink(), "http://example.com"); + ASSERT_EQ(meta->depends().size(), 1); + EXPECT_EQ(meta->depends().at(0).name(), "core"); + EXPECT_EQ(meta->depends().at(0).version(), "1.0"); + EXPECT_EQ(meta->pluginState(), PluginMetaObject::kReaded); +} + +TEST(PluginManagerPrivateJsonToMetaTest, EmptyJsonYieldsDefaultsButReaded) +{ + PluginMetaObjectPointer meta = PluginMetaObjectPointer::create(); + PluginManagerPrivate::jsonToMeta(meta, QJsonObject{}); + EXPECT_TRUE(meta->version().isEmpty()); + EXPECT_TRUE(meta->category().isEmpty()); + EXPECT_TRUE(meta->description().isEmpty()); + EXPECT_TRUE(meta->urlLink().isEmpty()); + EXPECT_TRUE(meta->depends().isEmpty()); + EXPECT_EQ(meta->pluginState(), PluginMetaObject::kReaded); +} + +TEST(PluginManagerPrivateJsonToMetaTest, MultipleDependsParsed) +{ + PluginMetaObjectPointer meta = PluginMetaObjectPointer::create(); + QJsonObject metaData; + QJsonArray deps; + QJsonObject d1; + d1.insert(kPluginName, "core"); + d1.insert(kPluginVersion, "1.0"); + QJsonObject d2; + d2.insert(kPluginName, "network"); + d2.insert(kPluginVersion, "2.3"); + deps.append(d1); + deps.append(d2); + metaData.insert(kPluginDepends, deps); + + PluginManagerPrivate::jsonToMeta(meta, metaData); + + ASSERT_EQ(meta->depends().size(), 2); + EXPECT_EQ(meta->depends().at(0).name(), "core"); + EXPECT_EQ(meta->depends().at(1).name(), "network"); + EXPECT_EQ(meta->depends().at(1).version(), "2.3"); +} + +TEST(PluginManagerPrivateScanTest, ScanfRealPluginAppendsAndSetsName) +{ + QQueue dest; + PluginMetaObjectPointer meta = PluginMetaObjectPointer::create(); + QJsonObject data; + data.insert(kPluginName, "myplugin"); + + PluginManagerPrivate::scanfRealPlugin(&dest, meta, data, {}); + + ASSERT_EQ(dest.size(), 1); + EXPECT_EQ(dest.head()->name(), "myplugin"); + EXPECT_FALSE(dest.head()->isVirtual()); + EXPECT_EQ(dest.head()->pluginState(), PluginMetaObject::kReaded); +} + +TEST(PluginManagerPrivateScanTest, ScanfRealPluginBlacklistedSkipped) +{ + QQueue dest; + PluginMetaObjectPointer meta = PluginMetaObjectPointer::create(); + QJsonObject data; + data.insert(kPluginName, "evil"); + + PluginManagerPrivate::scanfRealPlugin(&dest, meta, data, {"evil"}); + + EXPECT_TRUE(dest.isEmpty()); +} + +TEST(PluginManagerPrivateScanTest, ScanfRealPluginEmptyNameStillAppended) +{ + QQueue dest; + PluginMetaObjectPointer meta = PluginMetaObjectPointer::create(); + + PluginManagerPrivate::scanfRealPlugin(&dest, meta, QJsonObject{}, {}); + + ASSERT_EQ(dest.size(), 1); + EXPECT_TRUE(dest.head()->name().isEmpty()); +} + +TEST(PluginManagerPrivateScanTest, ScanfVirtualPluginAddsEachVirtual) +{ + QQueue dest; + QJsonObject metaDataJson; + metaDataJson.insert(kPluginName, "host"); + QJsonArray virtualList; + QJsonObject v1; + v1.insert(kPluginName, "v-one"); + QJsonObject v2; + v2.insert(kPluginName, "v-two"); + virtualList.append(v1); + virtualList.append(v2); + QJsonObject data; + data.insert(kVirtualPluginMeta, metaDataJson); + data.insert(kVirtualPluginList, virtualList); + + PluginManagerPrivate::scanfVirtualPlugin(&dest, "/path/to/host.so", data, {}); + + ASSERT_EQ(dest.size(), 2); + EXPECT_EQ(dest.at(0)->name(), "v-one"); + EXPECT_EQ(dest.at(1)->name(), "v-two"); + EXPECT_TRUE(dest.at(0)->isVirtual()); + EXPECT_TRUE(dest.at(1)->isVirtual()); +} + +TEST(PluginManagerPrivateScanTest, ScanfVirtualPluginBlacklistedRealSkipsAll) +{ + QQueue dest; + QJsonObject metaDataJson; + metaDataJson.insert(kPluginName, "host"); + QJsonArray virtualList; + QJsonObject v1; + v1.insert(kPluginName, "v-one"); + virtualList.append(v1); + QJsonObject data; + data.insert(kVirtualPluginMeta, metaDataJson); + data.insert(kVirtualPluginList, virtualList); + + PluginManagerPrivate::scanfVirtualPlugin(&dest, "/host.so", data, {"host"}); + + EXPECT_TRUE(dest.isEmpty()); +} + +TEST(PluginManagerPrivateScanTest, ScanfVirtualPluginBlacklistedSingleVirtualSkipped) +{ + QQueue dest; + QJsonObject metaDataJson; + metaDataJson.insert(kPluginName, "host"); + QJsonArray virtualList; + QJsonObject v1; + v1.insert(kPluginName, "v-keep"); + QJsonObject v2; + v2.insert(kPluginName, "v-drop"); + virtualList.append(v1); + virtualList.append(v2); + QJsonObject data; + data.insert(kVirtualPluginMeta, metaDataJson); + data.insert(kVirtualPluginList, virtualList); + + PluginManagerPrivate::scanfVirtualPlugin(&dest, "/host.so", data, {"v-drop"}); + + ASSERT_EQ(dest.size(), 1); + EXPECT_EQ(dest.at(0)->name(), "v-keep"); +} + +TEST(PluginManagerPrivateScanTest, ScanfVirtualPluginEmptyListAppendsNothing) +{ + QQueue dest; + QJsonObject metaDataJson; + metaDataJson.insert(kPluginName, "host"); + QJsonObject data; + data.insert(kVirtualPluginMeta, metaDataJson); + data.insert(kVirtualPluginList, QJsonArray{}); + + PluginManagerPrivate::scanfVirtualPlugin(&dest, "/host.so", data, {}); + + EXPECT_TRUE(dest.isEmpty()); +} + +namespace { +PluginMetaObjectPointer makeNamedPlugin(const QString &name) +{ + PluginMetaObjectPointer meta = PluginMetaObjectPointer::create(); + QQueue tmp; + QJsonObject data; + data.insert(kPluginName, name); + PluginManagerPrivate::scanfRealPlugin(&tmp, meta, data, {}); + return meta; +} + +void addDepend(const PluginMetaObjectPointer &meta, const QString &depName, const QString &depVer) +{ + QJsonObject metaData; + QJsonArray deps; + QJsonObject d; + d.insert(kPluginName, depName); + d.insert(kPluginVersion, depVer); + deps.append(d); + metaData.insert(kPluginDepends, deps); + PluginManagerPrivate::jsonToMeta(meta, metaData); +} +} // namespace + +TEST(PluginManagerPrivateSortTest, DependsSortPutsDependencyFirst) +{ + auto a = makeNamedPlugin("A"); + auto b = makeNamedPlugin("B"); + addDepend(b, "A", "1.0"); + ASSERT_EQ(b->depends().size(), 1); + + QQueue src; + src.append(b); + src.append(a); + + QQueue dst; + PluginManagerPrivate::dependsSort(&dst, &src); + + ASSERT_EQ(dst.size(), 2); + EXPECT_EQ(dst.at(0)->name(), "A"); + EXPECT_EQ(dst.at(1)->name(), "B"); +} + +TEST(PluginManagerPrivateSortTest, DependsSortChainOfThree) +{ + auto a = makeNamedPlugin("A"); + auto b = makeNamedPlugin("B"); + auto c = makeNamedPlugin("C"); + addDepend(b, "A", "1.0"); + addDepend(c, "B", "1.0"); + + QQueue src; + src.append(c); + src.append(b); + src.append(a); + + QQueue dst; + PluginManagerPrivate::dependsSort(&dst, &src); + + ASSERT_EQ(dst.size(), 3); + EXPECT_EQ(dst.at(0)->name(), "A"); + EXPECT_EQ(dst.at(1)->name(), "B"); + EXPECT_EQ(dst.at(2)->name(), "C"); +} + +TEST(PluginManagerPrivateSortTest, DependsSortNoDependsKeepsAll) +{ + auto a = makeNamedPlugin("A"); + auto b = makeNamedPlugin("B"); + + QQueue src; + src.append(a); + src.append(b); + + QQueue dst; + PluginManagerPrivate::dependsSort(&dst, &src); + + ASSERT_EQ(dst.size(), 2); +} + +TEST(PluginManagerPrivateSortTest, CircularDependsFallsBackToSourceOrder) +{ + auto a = makeNamedPlugin("A"); + auto b = makeNamedPlugin("B"); + addDepend(a, "B", "1.0"); + addDepend(b, "A", "1.0"); + + QQueue src; + src.append(a); + src.append(b); + + QQueue dst; + PluginManagerPrivate::dependsSort(&dst, &src); + + ASSERT_EQ(dst.size(), 2); + EXPECT_EQ(dst.at(0)->name(), src.at(0)->name()); + EXPECT_EQ(dst.at(1)->name(), src.at(1)->name()); +} + +TEST(PluginManagerPrivateSortTest, UnknownDependIgnoredAndOthersSorted) +{ + auto a = makeNamedPlugin("A"); + auto b = makeNamedPlugin("B"); + addDepend(b, "A", "1.0"); + addDepend(b, "Ghost", "9.9"); + + QQueue src; + src.append(b); + src.append(a); + + QQueue dst; + PluginManagerPrivate::dependsSort(&dst, &src); + + ASSERT_EQ(dst.size(), 2); + EXPECT_EQ(dst.at(0)->name(), "A"); + EXPECT_EQ(dst.at(1)->name(), "B"); +} + +TEST(PluginManagerPrivateReadJsonTest, EmptyMetaDataReturnsEarlyAndStaysReading) +{ + PluginMetaObjectPointer meta = PluginMetaObjectPointer::create(); + PluginManagerPrivate::readJsonToMeta(meta); + EXPECT_EQ(meta->pluginState(), PluginMetaObject::kReading); + EXPECT_TRUE(meta->iid().isEmpty()); +} + +TEST(PluginManagerPrivateSinglePluginTest, InvalidStateLoadInitStartStopReturnFalse) +{ + PluginManager pm; + PluginManagerPrivate p(&pm); + PluginMetaObjectPointer ptr = PluginMetaObjectPointer::create(); + EXPECT_EQ(ptr->pluginState(), PluginMetaObject::kInvalid); + EXPECT_FALSE(p.loadPlugin(ptr)); + EXPECT_FALSE(p.initPlugin(ptr)); + EXPECT_FALSE(p.startPlugin(ptr)); + EXPECT_FALSE(p.stopPlugin(ptr)); +} + +TEST(PluginManagerPrivateSinglePluginTest, PluginMetaObjMissOnEmptyQueueReturnsNull) +{ + PluginManager pm; + PluginManagerPrivate p(&pm); + EXPECT_EQ(p.pluginMetaObj("anything"), nullptr); +} + +TEST(LifeCycleNamespaceTest, InitializeRecordsIidsAndPaths) +{ + LifeCycle::shutdownPlugins(); + LifeCycle::initialize({"org.test.Lifecycle.IID"}, {"/tmp/no-such-lifecycle-path"}); + EXPECT_TRUE(LifeCycle::pluginIIDs().contains("org.test.Lifecycle.IID")); + EXPECT_TRUE(LifeCycle::pluginPaths().contains("/tmp/no-such-lifecycle-path")); +} + +TEST(LifeCycleNamespaceTest, InitializeWithBlackAndLazyNames) +{ + LifeCycle::shutdownPlugins(); + LifeCycle::initialize({"org.test.Lifecycle.IID2"}, + {"/tmp/no-such-lifecycle-path-2"}, + {"black-plugin-x"}, + {"lazy-plugin-y"}); + EXPECT_TRUE(LifeCycle::blackList().contains("black-plugin-x")); + EXPECT_TRUE(LifeCycle::lazyLoadList().contains("lazy-plugin-y")); +} + +TEST(LifeCycleNamespaceTest, PluginMetaObjLookupMissReturnsNull) +{ + EXPECT_EQ(LifeCycle::pluginMetaObj("does-not-exist"), nullptr); +} + +TEST(LifeCycleNamespaceTest, ReadPluginsOnEmptyIsSafe) +{ + EXPECT_NO_THROW((void)LifeCycle::readPlugins()); +} + +TEST(LifeCycleNamespaceTest, LoadAndShutdownOnEmptyIsSafe) +{ + LifeCycle::initialize({"org.test.Lifecycle.Load"}, {"/no/plugins/here"}); + EXPECT_NO_THROW((void)LifeCycle::loadPlugins()); + EXPECT_NO_THROW(LifeCycle::shutdownPlugins()); + SUCCEED(); +} + +TEST(LifeCycleNamespaceTest, StateQueriesAreSafe) +{ + EXPECT_NO_THROW((void)LifeCycle::isAllPluginsInitialized()); + EXPECT_NO_THROW((void)LifeCycle::isAllPluginsStarted()); +} diff --git a/tests/compat/zrpc_extra_test.cpp b/tests/compat/zrpc_extra_test.cpp new file mode 100644 index 000000000..30e5691de --- /dev/null +++ b/tests/compat/zrpc_extra_test.cpp @@ -0,0 +1,115 @@ +// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#include + +#include +#include + +#include "errorcode.h" +#include "netaddress.h" +#include "rpcchannel.h" +#include "rpccontroller.h" +#include "rpcdispatcher.h" +#include "rpcclosure.h" + +using namespace zrpc_ns; + +TEST(ZRpcDispatcherTest, ParseServiceFullNameSplitsOnFirstDot) +{ + ZRpcDispacther d; + std::string svc, method; + EXPECT_TRUE(d.parseServiceFullName("QueryService.query_name", svc, method)); + EXPECT_EQ(svc, "QueryService"); + EXPECT_EQ(method, "query_name"); +} + +TEST(ZRpcDispatcherTest, ParseServiceFullNameEmptyReturnsFalse) +{ + ZRpcDispacther d; + std::string svc, method; + EXPECT_FALSE(d.parseServiceFullName("", svc, method)); +} + +TEST(ZRpcDispatcherTest, ParseServiceFullNameNoDotReturnsFalse) +{ + ZRpcDispacther d; + std::string svc, method; + EXPECT_FALSE(d.parseServiceFullName("NoSeparator", svc, method)); +} + +TEST(ZRpcDispatcherTest, ParseServiceFullNameMultipleDots) +{ + ZRpcDispacther d; + std::string svc, method; + EXPECT_TRUE(d.parseServiceFullName("a.b.c", svc, method)); + EXPECT_EQ(svc, "a"); + EXPECT_EQ(method, "b.c"); +} + +TEST(ZRpcDispatcherTest, DispatcherDefaultServiceMapEmpty) +{ + ZRpcDispacther d; + EXPECT_TRUE(d.m_service_map.empty()); +} + +TEST(ZRpcControllerExtraTest, SetErrorCodeOnlyKeepsFailedFalse) +{ + ZRpcController c; + c.SetErrorCode(77); + EXPECT_EQ(c.ErrorCode(), 77); + EXPECT_FALSE(c.Failed()); +} + +TEST(ZRpcControllerExtraTest, SetErrorCombinesCodeAndInfo) +{ + ZRpcController c; + c.SetError(ERROR_FAILED_ENCODE, "encode broken"); + EXPECT_TRUE(c.Failed()); + EXPECT_EQ(c.ErrorCode(), ERROR_FAILED_ENCODE); + EXPECT_EQ(c.ErrorText(), "encode broken"); +} + +TEST(NetAddressToStringTest, FormatsIpAndPort) +{ + NetAddress addr("192.168.0.1", 9999, false); + EXPECT_EQ(addr.toString(), "192.168.0.1:9999"); +} + +TEST(NetAddressToStringTest, WildcardIpForNull) +{ + NetAddress addr(nullptr, 8080, false); + EXPECT_EQ(addr.toString(), "0.0.0.0:8080"); +} + +TEST(ZRpcClosureTest, RunInvokesCallback) +{ + int counter = 0; + ZRpcClosure cl([&counter]() { counter++; }); + cl.Run(); + EXPECT_EQ(counter, 1); +} + +TEST(ZRpcClosureTest, RunTwiceInvokesTwice) +{ + int counter = 0; + ZRpcClosure cl([&counter]() { counter += 10; }); + cl.Run(); + cl.Run(); + EXPECT_EQ(counter, 20); +} + +TEST(ZRpcChannelTest, ConstructShortConnectionDoesNotConnect) +{ + auto addr = std::make_shared("127.0.0.1", 12345, false); + ZRpcChannel ch(addr, false); + SUCCEED(); +} + +TEST(ZRpcChannelTest, DestructorLongConnectReleasesGlobalClient) +{ + auto addr = std::make_shared("127.0.0.1", 12346, false); + { ZRpcChannel ch(addr, true); } + SUCCEED(); +} diff --git a/tests/logic/httpweb_extra_test.cpp b/tests/logic/httpweb_extra_test.cpp new file mode 100644 index 000000000..3ac62ac48 --- /dev/null +++ b/tests/logic/httpweb_extra_test.cpp @@ -0,0 +1,308 @@ +// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#include + +#include "fileclient.h" +#include "fileserver.h" +#include "tokencache.h" +#include "webbinder.h" +#include "webproto.h" +#include "syncstatus.h" + +#include "asio/service.h" +#include "asio/ssl_context.h" + +#include +#include +#include +#include + +class HttpWebExtraTest : public ::testing::Test { +protected: + std::shared_ptr service; + std::shared_ptr context; + std::unique_ptr client; + std::unique_ptr server; + std::filesystem::path tmpDir; + + void SetUp() override + { + std::random_device rd; + tmpDir = std::filesystem::temp_directory_path() / ("hw_extra_" + std::to_string(rd())); + std::filesystem::create_directories(tmpDir); + + WebBinder::GetInstance().clear(); + TokenCache::GetInstance().clearTokens(); + + service = std::make_shared(); + service->Start(); + context = std::make_shared(asio::ssl::context::tlsv12); + + client = std::make_unique(service, context, "127.0.0.1", 13789); + server = std::make_unique(service, context, "127.0.0.1", 13790); + } + + void TearDown() override + { + client.reset(); + server.reset(); + service->Stop(); + WebBinder::GetInstance().clear(); + TokenCache::GetInstance().clearTokens(); + std::error_code ec; + std::filesystem::remove_all(tmpDir, ec); + } +}; + +TEST_F(HttpWebExtraTest, WebBinderReplaceAllSingle) +{ + std::string s = "hello world"; + WebBinder::GetInstance().replaceAll(s, "world", "there"); + EXPECT_EQ(s, "hello there"); +} + +TEST_F(HttpWebExtraTest, WebBinderReplaceAllMultiple) +{ + std::string s = "a.b.c.d"; + WebBinder::GetInstance().replaceAll(s, ".", "-"); + EXPECT_EQ(s, "a-b-c-d"); +} + +TEST_F(HttpWebExtraTest, WebBinderReplaceAllEmptyFrom) +{ + std::string s = "unchanged"; + WebBinder::GetInstance().replaceAll(s, "", "X"); + EXPECT_EQ(s, "unchanged"); +} + +TEST_F(HttpWebExtraTest, WebBinderReplaceAllToContainsFrom) +{ + std::string s = "x"; + WebBinder::GetInstance().replaceAll(s, "x", "yx"); + EXPECT_EQ(s, "yx"); +} + +TEST_F(HttpWebExtraTest, WebBinderReplaceAllNoMatch) +{ + std::string s = "abcdef"; + WebBinder::GetInstance().replaceAll(s, "zzz", "q"); + EXPECT_EQ(s, "abcdef"); +} + +TEST_F(HttpWebExtraTest, WebBinderReplaceFound) +{ + std::string s = "foo bar baz"; + bool ok = WebBinder::GetInstance().replace(s, "bar", "qux"); + EXPECT_TRUE(ok); + EXPECT_EQ(s, "foo qux baz"); +} + +TEST_F(HttpWebExtraTest, WebBinderReplaceNotFound) +{ + std::string s = "foo bar baz"; + bool ok = WebBinder::GetInstance().replace(s, "nope", "x"); + EXPECT_FALSE(ok); + EXPECT_EQ(s, "foo bar baz"); +} + +TEST_F(HttpWebExtraTest, WebBinderReplaceFirstOnly) +{ + std::string s = "a.a.a"; + bool ok = WebBinder::GetInstance().replace(s, "a", "Z"); + EXPECT_TRUE(ok); + EXPECT_EQ(s, "Z.a.a"); +} + +TEST_F(HttpWebExtraTest, FileClientStartFileDownloadNoConfig) +{ + client->startFileDownload({"file1.txt"}); + SUCCEED(); +} + +TEST_F(HttpWebExtraTest, FileClientStartFileDownloadEmptyTokenOnly) +{ + client->setConfig("", tmpDir.string()); + client->startFileDownload({"file1.txt"}); + SUCCEED(); +} + +TEST_F(HttpWebExtraTest, FileClientStartFileDownloadEmptySaveDirOnly) +{ + client->setConfig("sometoken", ""); + client->startFileDownload({"file1.txt"}); + SUCCEED(); +} + +TEST_F(HttpWebExtraTest, FileClientSendInfobyHeaderInvalidMask) +{ + client->sendInfobyHeader(INFO_WEB_MAX); + SUCCEED(); +} + +TEST_F(HttpWebExtraTest, FileClientSendInfobyHeaderLargeMask) +{ + client->sendInfobyHeader(255, "name"); + SUCCEED(); +} + +TEST_F(HttpWebExtraTest, FileClientStopMultipleTimesNoConfig) +{ + client->stop(); + client->stop(); + client->stop(); + SUCCEED(); +} + +TEST_F(HttpWebExtraTest, FileClientCreateNotExistPathLongNameTruncated) +{ + std::string longName(300, 'x'); + longName += ".txt"; + std::string path = (tmpDir / longName).string(); + bool created = client->createNotExistPath(path, true); + EXPECT_TRUE(created); + EXPECT_FALSE(path.empty()); + EXPECT_TRUE(std::filesystem::exists(path)); + EXPECT_LT(path.length(), (tmpDir / longName).string().length()); +} + +TEST_F(HttpWebExtraTest, FileClientCreateNotExistPathLongNameDirTruncated) +{ + std::string longName(300, 'd'); + std::string path = (tmpDir / longName).string(); + bool created = client->createNotExistPath(path, false); + EXPECT_TRUE(created); + EXPECT_FALSE(path.empty()); + EXPECT_TRUE(std::filesystem::exists(path)); +} + +TEST_F(HttpWebExtraTest, FileClientCreateNextAvailableNameTrailingSlashSaveDir) +{ + client->setConfig("token", tmpDir.string() + "/"); + std::string name = client->createNextAvailableName("trailfile.txt", true); + EXPECT_FALSE(name.empty()); + EXPECT_TRUE(std::filesystem::exists(name)); +} + +TEST_F(HttpWebExtraTest, FileClientCreateNextAvailableNameTrailingSlashDir) +{ + client->setConfig("token", tmpDir.string() + "/"); + std::string name = client->createNextAvailableName("traildir", false); + EXPECT_FALSE(name.empty()); + EXPECT_TRUE(std::filesystem::is_directory(name)); +} + +TEST_F(HttpWebExtraTest, FileClientCreateNextAvailableNameMultipleDupes) +{ + client->setConfig("token", tmpDir.string()); + std::string n1 = client->createNextAvailableName("multidup.txt", true); + ASSERT_FALSE(n1.empty()); + std::ofstream(n1) << "content one"; + std::string n2 = client->createNextAvailableName("multidup.txt", true); + ASSERT_FALSE(n2.empty()); + std::ofstream(n2) << "content two"; + std::string n3 = client->createNextAvailableName("multidup.txt", true); + ASSERT_FALSE(n3.empty()); + EXPECT_NE(n1, n2); + EXPECT_NE(n2, n3); + EXPECT_NE(n1, n3); +} + +TEST_F(HttpWebExtraTest, FileClientGetHeadKeyMultipleHeaders) +{ + std::string headers = + "Host: example.com\n" + "Content-Type: application/json\n" + "X-Custom: myvalue\n" + "Content-Length: 42\n"; + std::string v = client->getHeadKey(headers, "Content-Type"); + EXPECT_TRUE(v.find("application/json") != std::string::npos); + std::string v2 = client->getHeadKey(headers, "X-Custom"); + EXPECT_TRUE(v2.find("myvalue") != std::string::npos); +} + +TEST_F(HttpWebExtraTest, FileClientGetHeadKeyNoDelimiter) +{ + std::string headers = "this line has no colon\nanother\n"; + std::string v = client->getHeadKey(headers, "anything"); + EXPECT_TRUE(v.empty()); +} + +TEST_F(HttpWebExtraTest, FileServerVerifyTokenInvalid) +{ + std::string bad = "not.a.valid.jwt"; + EXPECT_FALSE(server->verifyToken(bad)); +} + +TEST_F(HttpWebExtraTest, FileServerVerifyTokenEmpty) +{ + std::string empty; + EXPECT_FALSE(server->verifyToken(empty)); +} + +TEST_F(HttpWebExtraTest, FileServerVerifyTokenGarbage) +{ + std::string garbage = "!!!garbage!!!"; + EXPECT_FALSE(server->verifyToken(garbage)); +} + +TEST_F(HttpWebExtraTest, FileServerVerifyTokenAfterClear) +{ + auto token = server->genToken(R"(["file1"])"); + ASSERT_FALSE(token.empty()); + server->clearBind(); + std::string mut = token; + EXPECT_FALSE(server->verifyToken(mut)); +} + +TEST_F(HttpWebExtraTest, FileServerGenTokenEmptyInfo) +{ + auto token = server->genToken("[]"); + EXPECT_FALSE(token.empty()); +} + +TEST_F(HttpWebExtraTest, FileServerGenTokenMultipleEntries) +{ + auto token = server->genToken(R"(["a","b","c"])"); + EXPECT_FALSE(token.empty()); + auto webs = client->parseWeb(token); + EXPECT_EQ(webs.size(), 3u); +} + +TEST_F(HttpWebExtraTest, FileServerWebBindMismatchedBothDirections) +{ + EXPECT_THROW(server->webBind("/dir/", "/home/user/docs"), std::invalid_argument); + EXPECT_THROW(server->webBind("/dir", "/home/user/docs/"), std::invalid_argument); +} + +TEST_F(HttpWebExtraTest, FileServerWebUnbindAfterClear) +{ + ASSERT_EQ(server->webBind("/files", "/home/user/docs"), 0); + server->clearBind(); + EXPECT_EQ(server->webUnbind("/files"), -1); +} + +TEST_F(HttpWebExtraTest, FileServerClearBindIdempotent) +{ + server->clearBind(); + server->clearBind(); + SUCCEED(); +} + +TEST_F(HttpWebExtraTest, FileServerVerifyValidTokenDelegation) +{ + auto token = server->genToken(R"(["/files/a.txt"])"); + std::string mut = token; + EXPECT_TRUE(server->verifyToken(mut)); +} + +TEST_F(HttpWebExtraTest, FileClientDestructorAfterSetConfig) +{ + { + FileClient fc(service, context, "127.0.0.1", 13791); + fc.setConfig("tk", tmpDir.string()); + fc.stop(); + } + SUCCEED(); +} diff --git a/tests/logic/httpweb_struct_test.cpp b/tests/logic/httpweb_struct_test.cpp new file mode 100644 index 000000000..5565f2ab7 --- /dev/null +++ b/tests/logic/httpweb_struct_test.cpp @@ -0,0 +1,254 @@ +// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later + +#include + +#include "webproto.h" +#include "webbinder.h" +#include "tokencache.h" +#include "fileclient.h" +#include "fileserver.h" + +#include "asio/service.h" +#include "asio/ssl_context.h" + +#include +#include +#include +#include +#include + +namespace { + +InfoEntry roundtripInfo(const InfoEntry& src) +{ + picojson::value j = src.as_json(); + std::string s; + j.serialize(std::back_inserter(s)); + picojson::value parsed; + std::string err = picojson::parse(parsed, s); + EXPECT_TRUE(err.empty()) << err; + InfoEntry dst; + dst.from_json(parsed); + return dst; +} + +} + +TEST(InfoEntryTest, FlatEntryRoundtrip) +{ + InfoEntry src; + src.name = "flat.txt"; + src.size = 42; + auto dst = roundtripInfo(src); + EXPECT_EQ(dst.name, "flat.txt"); + EXPECT_EQ(dst.size, 42); + EXPECT_TRUE(dst.datas.empty()); +} + +TEST(InfoEntryTest, NestedEntryRoundtrip) +{ + InfoEntry src; + src.name = "dir"; + src.size = 0; + InfoEntry child1; + child1.name = "a.txt"; + child1.size = 10; + InfoEntry child2; + child2.name = "b.txt"; + child2.size = 20; + src.datas.push_back(child1); + src.datas.push_back(child2); + + auto dst = roundtripInfo(src); + EXPECT_EQ(dst.name, "dir"); + EXPECT_EQ(dst.datas.size(), 2u); + EXPECT_EQ(dst.datas[0].name, "a.txt"); + EXPECT_EQ(dst.datas[0].size, 10); + EXPECT_EQ(dst.datas[1].name, "b.txt"); + EXPECT_EQ(dst.datas[1].size, 20); +} + +TEST(InfoEntryTest, DeeplyNestedEntryRoundtrip) +{ + InfoEntry src; + src.name = "root"; + InfoEntry mid; + mid.name = "mid"; + InfoEntry leaf; + leaf.name = "leaf.dat"; + leaf.size = 7; + mid.datas.push_back(leaf); + src.datas.push_back(mid); + + auto dst = roundtripInfo(src); + ASSERT_EQ(dst.datas.size(), 1u); + EXPECT_EQ(dst.datas[0].name, "mid"); + ASSERT_EQ(dst.datas[0].datas.size(), 1u); + EXPECT_EQ(dst.datas[0].datas[0].name, "leaf.dat"); + EXPECT_EQ(dst.datas[0].datas[0].size, 7); +} + +TEST(InfoEntryTest, FromJsonDatasNotArraySkips) +{ + std::string json = R"({"name":"x","size":1,"datas":"not-an-array"})"; + picojson::value v; + std::string err = picojson::parse(v, json); + ASSERT_TRUE(err.empty()) << err; + InfoEntry dst; + dst.from_json(v); + EXPECT_EQ(dst.name, "x"); + EXPECT_EQ(dst.size, 1); + EXPECT_TRUE(dst.datas.empty()); +} + +TEST(InfoEntryTest, FromJsonNonObjectElementSkipped) +{ + std::string json = R"({"name":"p","size":2,"datas":[42,"str",{"name":"ok","size":3}]})"; + picojson::value v; + std::string err = picojson::parse(v, json); + ASSERT_TRUE(err.empty()) << err; + InfoEntry dst; + dst.from_json(v); + EXPECT_EQ(dst.name, "p"); + ASSERT_EQ(dst.datas.size(), 1u); + EXPECT_EQ(dst.datas[0].name, "ok"); + EXPECT_EQ(dst.datas[0].size, 3); +} + +TEST(InfoEntryTest, AsJsonProducesObjectString) +{ + InfoEntry src; + src.name = "serialize.txt"; + src.size = 99; + std::string s; + src.as_json().serialize(std::back_inserter(s)); + EXPECT_NE(s.find("serialize.txt"), std::string::npos); + EXPECT_NE(s.find("\"size\""), std::string::npos); + EXPECT_NE(s.find("\"datas\""), std::string::npos); +} + +class HttpWebStructFixture : public ::testing::Test { +protected: + std::shared_ptr service; + std::shared_ptr context; + std::unique_ptr client; + std::unique_ptr server; + std::filesystem::path tmpDir; + + void SetUp() override + { + std::random_device rd; + tmpDir = std::filesystem::temp_directory_path() / ("hw_struct_" + std::to_string(rd())); + std::filesystem::create_directories(tmpDir); + WebBinder::GetInstance().clear(); + TokenCache::GetInstance().clearTokens(); + service = std::make_shared(); + service->Start(); + context = std::make_shared(asio::ssl::context::tlsv12); + client = std::make_unique(service, context, "127.0.0.1", 13793); + server = std::make_unique(service, context, "127.0.0.1", 13794); + } + + void TearDown() override + { + client.reset(); + server.reset(); + service->Stop(); + WebBinder::GetInstance().clear(); + TokenCache::GetInstance().clearTokens(); + std::error_code ec; + std::filesystem::remove_all(tmpDir, ec); + } +}; + +TEST_F(HttpWebStructFixture, WebBinderBindInsertsAtFront) +{ + ASSERT_EQ(WebBinder::GetInstance().bind("/a", "/home/a"), 0); + ASSERT_EQ(WebBinder::GetInstance().bind("/b", "/home/b"), 0); + EXPECT_EQ(WebBinder::GetInstance().getPath("/b"), "/home/b"); + EXPECT_EQ(WebBinder::GetInstance().containWeb("/a"), true); + EXPECT_EQ(WebBinder::GetInstance().lastWeb("/a"), true); +} + +TEST_F(HttpWebStructFixture, WebBinderGetPathPartialPrefixMatch) +{ + ASSERT_EQ(WebBinder::GetInstance().bind("/files", "/home/docs"), 0); + EXPECT_EQ(WebBinder::GetInstance().getPath("/filesX"), "/home/docsX"); +} + +TEST_F(HttpWebStructFixture, WebBinderUnbindMiddleKeepsOthers) +{ + ASSERT_EQ(WebBinder::GetInstance().bind("/a", "/home/a"), 0); + ASSERT_EQ(WebBinder::GetInstance().bind("/b", "/home/b"), 0); + ASSERT_EQ(WebBinder::GetInstance().bind("/c", "/home/c"), 0); + EXPECT_EQ(WebBinder::GetInstance().unbind("/b"), 0); + EXPECT_TRUE(WebBinder::GetInstance().containWeb("/a")); + EXPECT_TRUE(WebBinder::GetInstance().containWeb("/c")); + EXPECT_FALSE(WebBinder::GetInstance().containWeb("/b")); +} + +TEST_F(HttpWebStructFixture, FileClientGetHeadKeyMixedDelimiters) +{ + std::string headers = + "Key-A: val1\n" + "Key-B: val2\r\n" + "Key-C:val3\n"; + EXPECT_TRUE(client->getHeadKey(headers, "Key-A").find("val1") != std::string::npos); + EXPECT_TRUE(client->getHeadKey(headers, "Key-B").find("val2") != std::string::npos); +} + +TEST_F(HttpWebStructFixture, FileClientGetHeadKeyLastLineWins) +{ + std::string headers = "X: first\nX: second\n"; + std::string v = client->getHeadKey(headers, "X"); + EXPECT_TRUE(v.find("second") != std::string::npos); +} + +TEST_F(HttpWebStructFixture, FileClientCreateNotExistPathEmptyString) +{ + std::string empty; + bool created = client->createNotExistPath(empty, true); + EXPECT_FALSE(created); +} + +TEST_F(HttpWebStructFixture, FileClientCreateNextAvailableNameDirThenFileReuse) +{ + client->setConfig("token", tmpDir.string()); + std::string n = client->createNextAvailableName("share", false); + EXPECT_FALSE(n.empty()); + EXPECT_TRUE(std::filesystem::is_directory(n)); +} + +TEST_F(HttpWebStructFixture, FileServerGenTokenSingleAndVerify) +{ + auto token = server->genToken(R"(["/files/x"])"); + ASSERT_FALSE(token.empty()); + std::string mut = token; + EXPECT_TRUE(server->verifyToken(mut)); +} + +TEST_F(HttpWebStructFixture, FileServerWebBindUnbindSequence) +{ + EXPECT_EQ(server->webBind("/d1", "/home/d1"), 0); + EXPECT_EQ(server->webBind("/d2", "/home/d2"), 0); + EXPECT_EQ(server->webUnbind("/d1"), 0); + EXPECT_EQ(server->webUnbind("/d2"), 0); + EXPECT_EQ(server->webUnbind("/d1"), -1); +} + +TEST_F(HttpWebStructFixture, TokenCacheMultipleGenThenClear) +{ + auto t1 = TokenCache::GetInstance().genToken(R"(["a"])"); + auto t2 = TokenCache::GetInstance().genToken(R"(["b"])"); + auto t3 = TokenCache::GetInstance().genToken(R"(["c"])"); + EXPECT_FALSE(t1.empty()); + EXPECT_FALSE(t2.empty()); + EXPECT_FALSE(t3.empty()); + std::string m1 = t1, m2 = t2; + EXPECT_TRUE(TokenCache::GetInstance().verifyToken(m1)); + EXPECT_TRUE(TokenCache::GetInstance().verifyToken(m2)); + TokenCache::GetInstance().clearTokens(); + std::string m3 = t3; + EXPECT_FALSE(TokenCache::GetInstance().verifyToken(m3)); +} diff --git a/tests/logic/session_test.cpp b/tests/logic/session_test.cpp new file mode 100644 index 000000000..294e0bc6f --- /dev/null +++ b/tests/logic/session_test.cpp @@ -0,0 +1,294 @@ +// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#include + +#include "protoendpoint.h" +#include "protoclient.h" +#include "protoserver.h" +#include "session.h" + +#include "asio/service.h" +#include "asio/ssl_context.h" + +#include +#include +#include +#include +#include + +namespace { + +class TestSessionCallbacks : public SessionCallInterface +{ +public: + int lastState = -100; + std::string lastMsg; + int callCount = 0; + bool stateResult = true; + + void onReceivedMessage(const proto::OriginMessage &request, proto::OriginMessage *response) override + { + if (response) { + response->id = request.id; + response->mask = request.mask; + response->json_msg = request.json_msg; + } + } + + bool onStateChanged(int state, std::string &msg) override + { + lastState = state; + lastMsg = msg; + callCount++; + return stateResult; + } +}; + +} + +class SessionTest : public ::testing::Test { +protected: + std::shared_ptr service; + std::shared_ptr context; + std::shared_ptr callbacks; + + void SetUp() override + { + service = std::make_shared(); + service->Start(); + context = std::make_shared(asio::ssl::context::tlsv12); + callbacks = std::make_shared(); + } + + void TearDown() override + { + callbacks.reset(); + service->Stop(); + } + + void insertSession(ProtoServer &server, const std::string &ip) + { + BaseKit::UUID uid = BaseKit::UUID::Sequential(); + std::unique_lock locker(server._sessionids_lock); + server._session_ids.insert(std::make_pair(ip, uid)); + } +}; + +TEST_F(SessionTest, ProtoServerConstruct) +{ + ProtoServer server(service, context, 19000); + SUCCEED(); +} + +TEST_F(SessionTest, ProtoServerSetCallbacks) +{ + ProtoServer server(service, context, 19001); + server.setCallbacks(callbacks); + EXPECT_NE(server._callbacks, nullptr); + EXPECT_EQ(server._callbacks.get(), callbacks.get()); +} + +TEST_F(SessionTest, ProtoServerHasConnectedEmpty) +{ + ProtoServer server(service, context, 19002); + EXPECT_FALSE(server.hasConnected("192.168.1.1")); + EXPECT_FALSE(server.hasConnected("")); +} + +TEST_F(SessionTest, ProtoServerHasConnectedAfterSessionInsert) +{ + ProtoServer server(service, context, 19003); + insertSession(server, "10.0.0.1"); + EXPECT_TRUE(server.hasConnected("10.0.0.1")); +} + +TEST_F(SessionTest, ProtoServerHandleRealIPMappingInserts) +{ + ProtoServer server(service, context, 19004); + server.handleRealIPMapping("203.0.113.5", "10.0.0.9"); + EXPECT_EQ(server._real_to_remote_ip.size(), 1u); + EXPECT_EQ(server._remote_to_real_ip.size(), 1u); + EXPECT_EQ(server._real_to_remote_ip["203.0.113.5"], "10.0.0.9"); + EXPECT_EQ(server._remote_to_real_ip["10.0.0.9"], "203.0.113.5"); +} + +TEST_F(SessionTest, ProtoServerHasConnectedViaMapping) +{ + ProtoServer server(service, context, 19005); + server.handleRealIPMapping("203.0.113.5", "10.0.0.9"); + insertSession(server, "10.0.0.9"); + EXPECT_TRUE(server.hasConnected("203.0.113.5")); +} + +TEST_F(SessionTest, ProtoServerHasConnectedMappingNoSession) +{ + ProtoServer server(service, context, 19006); + server.handleRealIPMapping("203.0.113.5", "10.0.0.9"); + EXPECT_FALSE(server.hasConnected("203.0.113.5")); +} + +TEST_F(SessionTest, ProtoServerHandleRealIPMappingOverwriteOld) +{ + ProtoServer server(service, context, 19007); + server.handleRealIPMapping("203.0.113.5", "10.0.0.9"); + server.handleRealIPMapping("203.0.113.5", "10.0.0.10"); + EXPECT_EQ(server._real_to_remote_ip["203.0.113.5"], "10.0.0.10"); + EXPECT_EQ(server._remote_to_real_ip.size(), 1u); + EXPECT_EQ(server._remote_to_real_ip["10.0.0.10"], "203.0.113.5"); +} + +TEST_F(SessionTest, ProtoServerHandleRealIPMappingOverwriteRemote) +{ + ProtoServer server(service, context, 19008); + server.handleRealIPMapping("203.0.113.5", "10.0.0.9"); + server.handleRealIPMapping("203.0.113.6", "10.0.0.9"); + EXPECT_EQ(server._remote_to_real_ip["10.0.0.9"], "203.0.113.6"); + EXPECT_EQ(server._real_to_remote_ip.size(), 1u); + EXPECT_EQ(server._real_to_remote_ip["203.0.113.6"], "10.0.0.9"); +} + +TEST_F(SessionTest, ProtoServerHandleRealIPMappingMultiple) +{ + ProtoServer server(service, context, 19009); + server.handleRealIPMapping("1.1.1.1", "10.0.0.1"); + server.handleRealIPMapping("2.2.2.2", "10.0.0.2"); + server.handleRealIPMapping("3.3.3.3", "10.0.0.3"); + EXPECT_EQ(server._real_to_remote_ip.size(), 3u); + EXPECT_EQ(server._remote_to_real_ip.size(), 3u); + insertSession(server, "10.0.0.2"); + EXPECT_TRUE(server.hasConnected("2.2.2.2")); + EXPECT_FALSE(server.hasConnected("1.1.1.1")); + EXPECT_FALSE(server.hasConnected("3.3.3.3")); +} + +TEST_F(SessionTest, ProtoServerHandleRealIPMappingEmptyStrings) +{ + ProtoServer server(service, context, 19010); + server.handleRealIPMapping("", ""); + EXPECT_EQ(server._real_to_remote_ip.size(), 1u); + EXPECT_EQ(server._real_to_remote_ip[""], ""); +} + +TEST_F(SessionTest, ProtoServerPingRemotesInitiallyEmpty) +{ + ProtoServer server(service, context, 19011); + EXPECT_TRUE(server._ping_remotes.empty()); + EXPECT_EQ(server._ping_timer, nullptr); +} + +TEST_F(SessionTest, ProtoClientConstruct) +{ + ProtoClient client(service, context, "127.0.0.1", 19012); + SUCCEED(); +} + +TEST_F(SessionTest, ProtoClientSetCallbacks) +{ + ProtoClient client(service, context, "127.0.0.1", 19013); + client.setCallbacks(callbacks); + EXPECT_NE(client._callbacks, nullptr); + EXPECT_EQ(client._callbacks.get(), callbacks.get()); +} + +TEST_F(SessionTest, ProtoClientHasConnectedDefaultEmpty) +{ + ProtoClient client(service, context, "127.0.0.1", 19014); + EXPECT_FALSE(client.hasConnected("127.0.0.1")); + EXPECT_TRUE(client.hasConnected("")); +} + +TEST_F(SessionTest, ProtoClientHasConnectedMatch) +{ + ProtoClient client(service, context, "127.0.0.1", 19015); + client._connected_host = "192.168.0.50"; + EXPECT_TRUE(client.hasConnected("192.168.0.50")); + EXPECT_FALSE(client.hasConnected("192.168.0.51")); +} + +TEST_F(SessionTest, ProtoClientSetRealIP) +{ + ProtoClient client(service, context, "127.0.0.1", 19016); + EXPECT_TRUE(client._real_ip.empty()); + client.setRealIP("203.0.113.99"); + EXPECT_EQ(client._real_ip, "203.0.113.99"); +} + +TEST_F(SessionTest, ProtoClientSetRealIPEmpty) +{ + ProtoClient client(service, context, "127.0.0.1", 19017); + client.setRealIP(""); + EXPECT_TRUE(client._real_ip.empty()); +} + +TEST_F(SessionTest, ProtoClientConnectReplyedDefault) +{ + ProtoClient client(service, context, "127.0.0.1", 19018); + EXPECT_FALSE(client.connectReplyed()); +} + +TEST_F(SessionTest, ProtoClientConnectReplyedAfterSet) +{ + ProtoClient client(service, context, "127.0.0.1", 19019); + client._connect_replay = true; + EXPECT_TRUE(client.connectReplyed()); +} + +TEST_F(SessionTest, ProtoClientDisconnectAndStop) +{ + ProtoClient client(service, context, "127.0.0.1", 19020); + client.DisconnectAndStop(); + EXPECT_TRUE(client._stop.load()); + EXPECT_FALSE(client._connect_replay.load()); +} + +TEST_F(SessionTest, ProtoClientStopFlagDefault) +{ + ProtoClient client(service, context, "127.0.0.1", 19021); + EXPECT_FALSE(client._stop.load()); +} + +TEST_F(SessionTest, ProtoClientNopongDefault) +{ + ProtoClient client(service, context, "127.0.0.1", 19022); + EXPECT_EQ(client._nopong_count.load(), 0); +} + +TEST_F(SessionTest, ProtoClientPingTimerInitiallyNull) +{ + ProtoClient client(service, context, "127.0.0.1", 19023); + EXPECT_EQ(client._ping_timer, nullptr); +} + +TEST_F(SessionTest, ProtoEndpointActiveTargetDefault) +{ + ProtoClient client(service, context, "127.0.0.1", 19024); + EXPECT_TRUE(client._active_target.empty()); + client._active_target = "1.2.3.4"; + EXPECT_EQ(client._active_target, "1.2.3.4"); +} + +TEST_F(SessionTest, ProtoEndpointSelfRequestDefault) +{ + ProtoServer server(service, context, 19025); + EXPECT_FALSE(server._self_request.load()); + server._self_request.store(true); + EXPECT_TRUE(server._self_request.load()); +} + +TEST_F(SessionTest, ProtoEndpointHasConnectedServerOverride) +{ + ProtoServer server(service, context, 19026); + EXPECT_FALSE(server.hasConnected("9.9.9.9")); + insertSession(server, "9.9.9.9"); + EXPECT_TRUE(server.hasConnected("9.9.9.9")); +} + +TEST_F(SessionTest, ProtoClientRealIPOverride) +{ + ProtoClient client(service, context, "127.0.0.1", 19027); + client.setRealIP("10.1.2.3"); + client.setRealIP("10.4.5.6"); + EXPECT_EQ(client._real_ip, "10.4.5.6"); +}