| // Copyright 2024 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "remoting/host/linux/gdbus_connection_ref.h" |
| |
| #include <array> |
| #include <cstddef> |
| #include <memory> |
| #include <optional> |
| #include <string> |
| #include <tuple> |
| #include <vector> |
| |
| #include "base/functional/callback_helpers.h" |
| #include "base/test/task_environment.h" |
| #include "base/test/test_future.h" |
| #include "base/types/expected.h" |
| #include "dbus/test_service.h" |
| #include "remoting/host/base/loggable.h" |
| #include "remoting/host/linux/dbus_interfaces/org_chromium_TestInterface.h" |
| #include "remoting/host/linux/dbus_interfaces/org_freedesktop_DBus_Properties.h" |
| #include "remoting/host/linux/gvariant_ref.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| |
| namespace remoting { |
| |
| namespace test_interface = org_chromium_TestInterface; |
| |
| class GDBusConnectionRefTest : public testing::Test { |
| public: |
| void SetUp() override { |
| // Start the test service (runs on its own thread). |
| ASSERT_TRUE(test_service_.StartService()); |
| test_service_.WaitUntilServiceIsStarted(); |
| |
| base::test::TestFuture<base::expected<GDBusConnectionRef, Loggable>> |
| connection; |
| GDBusConnectionRef::CreateForSessionBus(connection.GetCallback()); |
| ASSERT_TRUE(connection.Get().has_value()); |
| connection_ = connection.Take().value(); |
| } |
| |
| void TearDown() override { test_service_.ShutdownAndBlock(); } |
| |
| protected: |
| static constexpr gvariant::ObjectPathCStr kObjectPath = |
| "/org/chromium/TestObject"; |
| |
| void PingBus() { |
| base::test::TestFuture<base::expected<gvariant::Ignored, Loggable>> |
| response; |
| connection_.Call("org.freedesktop.DBus", "/", "org.freedesktop.DBus.Peer", |
| "Ping", std::tuple(), response.GetCallback()); |
| EXPECT_TRUE(response.Get().has_value()); |
| } |
| |
| // Need a UI thread to get a GLib main loop. |
| base::test::SingleThreadTaskEnvironment task_environment_{ |
| base::test::SingleThreadTaskEnvironment::MainThreadType::UI}; |
| dbus::TestService test_service_{dbus::TestService::Options()}; |
| // Service name is randomly generated each run. |
| std::string service_name_{test_service_.service_name()}; |
| GDBusConnectionRef connection_; |
| }; |
| |
| TEST_F(GDBusConnectionRefTest, MethodCall) { |
| std::array kMessages = {"one", "two", "three"}; |
| std::array< |
| base::test::TestFuture<base::expected<std::tuple<std::string>, Loggable>>, |
| 3> |
| futures; |
| |
| for (std::size_t i = 0; i < 3; ++i) { |
| connection_.Call<test_interface::AsyncEcho>( |
| service_name_.c_str(), kObjectPath, std::tuple(kMessages[i]), |
| futures[i].GetCallback()); |
| } |
| |
| for (std::size_t i = 0; i < 3; ++i) { |
| EXPECT_TRUE(futures[i].Get().has_value()); |
| EXPECT_EQ(kMessages[i], get<0>(futures[i].Get().value())); |
| } |
| } |
| |
| TEST_F(GDBusConnectionRefTest, MethodCallError) { |
| base::test::TestFuture<base::expected<std::tuple<>, Loggable>> result; |
| connection_.Call<test_interface::BrokenMethod>( |
| service_name_.c_str(), kObjectPath, std::tuple(), result.GetCallback()); |
| EXPECT_FALSE(result.Get().has_value()); |
| } |
| |
| TEST_F(GDBusConnectionRefTest, GetProperty) { |
| base::test::TestFuture<base::expected<std::string, Loggable>> name_value; |
| base::test::TestFuture<base::expected<std::vector<std::string>, Loggable>> |
| methods_value; |
| |
| connection_.GetProperty<test_interface::Name>( |
| service_name_.c_str(), kObjectPath, name_value.GetCallback()); |
| connection_.GetProperty<test_interface::Methods>( |
| service_name_.c_str(), kObjectPath, methods_value.GetCallback()); |
| |
| ASSERT_TRUE(name_value.Get().has_value()); |
| EXPECT_EQ("TestService", name_value.Get().value()); |
| |
| ASSERT_TRUE(methods_value.Get().has_value()); |
| EXPECT_EQ((std::vector<std::string>{"Echo", "SlowEcho", "AsyncEcho", |
| "BrokenMethod"}), |
| methods_value.Get().value()); |
| } |
| |
| TEST_F(GDBusConnectionRefTest, SetProperty) { |
| // TestService doesn't actually remember the set value of name, so instead one |
| // must listen for the PropertiesChanged signal to make sure Set was called |
| // properly. |
| base::test::TestFuture< |
| GVariantRef<org_freedesktop_DBus_Properties::PropertiesChanged::kType>> |
| change_signal; |
| auto signal_subscription = |
| connection_ |
| .SignalSubscribe<org_freedesktop_DBus_Properties::PropertiesChanged>( |
| service_name_.c_str(), kObjectPath, |
| change_signal.GetRepeatingCallback()); |
| |
| base::test::TestFuture<base::expected<void, Loggable>> set_complete; |
| |
| const char* value = "new value"; |
| |
| connection_.SetProperty<test_interface::Name>( |
| service_name_.c_str(), kObjectPath, value, set_complete.GetCallback()); |
| |
| EXPECT_TRUE(set_complete.Get().has_value()); |
| auto [interface_name, changed_properties, invalidated_properties] = |
| change_signal.Take(); |
| EXPECT_EQ(test_interface::Name::kInterfaceName, interface_name.string_view()); |
| EXPECT_EQ(GVariantRef<"v">::From(gvariant::Boxed(value)), |
| changed_properties.LookUp(test_interface::Name::kPropertyName)); |
| } |
| |
| TEST_F(GDBusConnectionRefTest, SignalSubscribe) { |
| base::test::TestFuture<std::tuple<std::string>> signal_future; |
| |
| auto signal_subscription = connection_.SignalSubscribe<test_interface::Test>( |
| service_name_.c_str(), kObjectPath, signal_future.GetRepeatingCallback()); |
| |
| // Subscribing to a signal involves calling a bus method to send the match |
| // rule to the bus. Unfortunately, Gio does not provide a way to inform the |
| // caller of when the bus method has completed, as normally one is |
| // communicating with a different process and the subscription creation only |
| // has to be ordered with respect to other DBus messages (like in the |
| // SetProperty test above). Here, since the test-service signal is triggered |
| // directly, the test needs to wait for the match rule to be set before |
| // triggering the signal. As a workaround, the test pings the bus and waits |
| // for a response. Since the Ping message is sent after the match-rule message |
| // we know the match-rule message has been received by the time we received |
| // the ping response. |
| PingBus(); |
| |
| test_service_.SendTestSignal("message1"); |
| EXPECT_EQ("message1", get<0>(signal_future.Take())); |
| |
| test_service_.SendTestSignal("message2"); |
| EXPECT_EQ("message2", get<0>(signal_future.Take())); |
| } |
| |
| TEST_F(GDBusConnectionRefTest, DropSubscription) { |
| base::test::TestFuture<std::tuple<std::string>> signal_future; |
| |
| auto signal_subscription = connection_.SignalSubscribe<test_interface::Test>( |
| service_name_.c_str(), kObjectPath, signal_future.GetRepeatingCallback()); |
| |
| PingBus(); |
| |
| test_service_.SendTestSignal("message1"); |
| EXPECT_EQ("message1", get<0>(signal_future.Take())); |
| |
| // Create a new subscription at root and drop original subscription. |
| signal_subscription = connection_.SignalSubscribe<test_interface::Test>( |
| service_name_.c_str(), "/", signal_future.GetRepeatingCallback()); |
| |
| PingBus(); |
| |
| // The next signal on TestObject should be ignored, but the following one on |
| // the root object will match the new subscription. |
| test_service_.SendTestSignal("message2"); |
| test_service_.SendTestSignalFromRoot("message3"); |
| EXPECT_EQ("message3", get<0>(signal_future.Take())); |
| } |
| |
| TEST_F(GDBusConnectionRefTest, SubscribeAll) { |
| std::string connection_name = test_service_.GetConnectionName(); |
| |
| base::test::TestFuture<std::string, gvariant::ObjectPath, std::string, |
| std::string, GVariantRef<"r">> |
| signal_future; |
| |
| auto signal_subscription = connection_.SignalSubscribe( |
| service_name_.c_str(), std::nullopt, nullptr, nullptr, |
| signal_future.GetRepeatingCallback()); |
| |
| PingBus(); |
| |
| test_service_.SendTestSignal("message1"); |
| |
| { |
| auto [sender, object_path, interface_name, signal_name, arguments] = |
| signal_future.Take(); |
| EXPECT_EQ(connection_name, sender); |
| EXPECT_EQ(kObjectPath, object_path); |
| EXPECT_EQ(test_interface::Test::kInterfaceName, interface_name); |
| EXPECT_EQ(test_interface::Test::kSignalName, signal_name); |
| EXPECT_EQ(GVariantRef<>::From(std::tuple("message1")), arguments); |
| } |
| |
| test_service_.SendTestSignalFromRoot("message2"); |
| |
| { |
| auto [sender, object_path, interface_name, signal_name, arguments] = |
| signal_future.Take(); |
| EXPECT_EQ(connection_name, sender); |
| EXPECT_EQ(gvariant::ObjectPathCStr("/"), object_path); |
| EXPECT_EQ(test_interface::Test::kInterfaceName, interface_name); |
| EXPECT_EQ(test_interface::Test::kSignalName, signal_name); |
| EXPECT_EQ(GVariantRef<>::From(std::tuple("message2")), arguments); |
| } |
| |
| const char* prop_value = "value3"; |
| connection_.SetProperty<test_interface::Name>( |
| service_name_.c_str(), kObjectPath, prop_value, base::DoNothing()); |
| |
| { |
| auto [sender, object_path, interface_name, signal_name, arguments] = |
| signal_future.Take(); |
| EXPECT_EQ(connection_name, sender); |
| EXPECT_EQ(kObjectPath, object_path); |
| EXPECT_EQ( |
| org_freedesktop_DBus_Properties::PropertiesChanged::kInterfaceName, |
| interface_name); |
| EXPECT_EQ(org_freedesktop_DBus_Properties::PropertiesChanged::kSignalName, |
| signal_name); |
| auto typed_args = |
| GVariantRef<org_freedesktop_DBus_Properties::PropertiesChanged::kType>:: |
| TryFrom(arguments); |
| ASSERT_TRUE(typed_args.has_value()); |
| EXPECT_EQ(GVariantRef<"v">::From(gvariant::Boxed(prop_value)), |
| typed_args->get<1>().LookUp(test_interface::Name::kPropertyName)); |
| } |
| } |
| |
| } // namespace remoting |