chromium/chromeos/ash/components/dbus/featured/featured_client_unittest.cc

// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "chromeos/ash/components/dbus/featured/featured_client.h"

#include <map>
#include <string>

#include "base/barrier_closure.h"
#include "base/check_op.h"
#include "base/files/file_util.h"
#include "base/files/scoped_temp_dir.h"
#include "base/run_loop.h"
#include "base/strings/escape.h"
#include "base/test/bind.h"
#include "base/test/task_environment.h"
#include "base/threading/platform_thread.h"
#include "base/time/time.h"
#include "chromeos/ash/components/dbus/featured/fake_featured_client.h"
#include "chromeos/ash/components/dbus/featured/featured.pb.h"
#include "dbus/message.h"
#include "dbus/mock_bus.h"
#include "dbus/mock_object_proxy.h"
#include "dbus/object_path.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/cros_system_api/constants/featured.h"
#include "third_party/cros_system_api/dbus/service_constants.h"

namespace ash::featured {

namespace {

using ::testing::_;
using ::testing::Invoke;
using ::testing::IsEmpty;
using ::testing::Return;

std::string CreateEscapedFilename(const std::string& trial_name,
                                  const std::string& group_name) {
  std::string escaped_trial_name = base::EscapeAllExceptUnreserved(trial_name);
  std::string escaped_group_name = base::EscapeAllExceptUnreserved(group_name);

  return base::StrCat(
      {escaped_trial_name, feature::kTrialGroupSeparator, escaped_group_name});
}

}  // namespace

class FeaturedClientTest : public testing::Test {
 public:
  FeaturedClientTest()
      : bus_(base::MakeRefCounted<dbus::MockBus>(dbus::Bus::Options{})),
        path_(dbus::ObjectPath(::featured::kFeaturedServicePath)),
        proxy_(base::MakeRefCounted<dbus::MockObjectProxy>(
            bus_.get(),
            ::featured::kFeaturedServiceName,
            path_)) {
    // Makes sure `GetObjectProxy()` is called with the correct service name and
    // path.
    EXPECT_CALL(*bus_.get(),
                GetObjectProxy(::featured::kFeaturedServiceName, path_))
        .WillRepeatedly(Return(proxy_.get()));

    EXPECT_TRUE(dir_.CreateUniqueTempDir());
    active_trials_dir_ = dir_.GetPath().Append("active_trials");
    EXPECT_TRUE(base::CreateDirectory(active_trials_dir_));
  }

  FeaturedClientTest(const FeaturedClientTest&) = delete;
  FeaturedClientTest& operator=(const FeaturedClientTest&) = delete;

  ~FeaturedClientTest() override = default;

 protected:
  // Helper method to test FeaturedClient::ParseTrialFilename. Wrapping this
  // logic simplifies the testing logic by allowing us to use a friend class
  // instead of several FRIEND_TEST_ALL_PREFIXES.
  //
  // Callers must initialize FeaturedClient before calling this method.
  bool ParseTrialFilename(const base::FilePath& path,
                          base::FieldTrial::ActiveGroup& active_group) {
    return FeaturedClient::ParseTrialFilename(path, active_group);
  }

  // Mock bus and proxy for simulating calls.
  scoped_refptr<dbus::MockBus> bus_;
  dbus::ObjectPath path_;
  scoped_refptr<dbus::MockObjectProxy> proxy_;
  base::ScopedTempDir dir_;
  base::FilePath active_trials_dir_;
  base::test::TaskEnvironment task_environment_;
};

TEST_F(FeaturedClientTest, InitializeSuccess) {
  FeaturedClient::Initialize(bus_.get());

  ASSERT_NE(FeaturedClient::Get(), nullptr);

  FeaturedClient::Shutdown();

  EXPECT_EQ(FeaturedClient::Get(), nullptr);
}

TEST_F(FeaturedClientTest, NotInitializedGet) {
  ASSERT_EQ(FeaturedClient::Get(), nullptr);
}

TEST_F(FeaturedClientTest, InitializeFakeSuccess) {
  FeaturedClient::InitializeFake();

  ASSERT_NE(FakeFeaturedClient::Get(), nullptr);

  FakeFeaturedClient::Shutdown();

  EXPECT_EQ(FakeFeaturedClient::Get(), nullptr);
}

TEST_F(FeaturedClientTest, NotInitializedFakeGet) {
  ASSERT_EQ(FakeFeaturedClient::Get(), nullptr);
}

TEST_F(FeaturedClientTest, HandleSeedFetched_Success) {
  EXPECT_CALL(*proxy_,
              DoCallMethod(_, dbus::ObjectProxy::TIMEOUT_USE_DEFAULT, _))
      .WillOnce(Invoke([](dbus::MethodCall* call, int timeout_ms,
                          dbus::MockObjectProxy::ResponseCallback* callback) {
        std::unique_ptr<dbus::Response> response =
            dbus::Response::CreateEmpty();
        std::move(*callback).Run(response.get());
      }));

  FeaturedClient::Initialize(bus_.get());
  FeaturedClient* client = FeaturedClient::Get();

  ASSERT_NE(client, nullptr);

  ::featured::SeedDetails safe_seed;

  bool ran_callback = false;
  client->HandleSeedFetched(
      safe_seed, base::BindLambdaForTesting([&ran_callback](bool success) {
        EXPECT_TRUE(success);
        ran_callback = true;
      }));
  // Ensures the callback was executed.
  EXPECT_TRUE(ran_callback);

  FeaturedClient::Shutdown();

  EXPECT_EQ(FeaturedClient::Get(), nullptr);
}

// Check that `HandleSeedFetched` runs the callback with a false success value
// if the server (platform) returns an error responses.
TEST_F(FeaturedClientTest, HandleSeedFetched_Failure_ErrorResponse) {
  EXPECT_CALL(*proxy_,
              DoCallMethod(_, dbus::ObjectProxy::TIMEOUT_USE_DEFAULT, _))
      .WillOnce(Invoke([](dbus::MethodCall* call, int timeout_ms,
                          dbus::MockObjectProxy::ResponseCallback* callback) {
        // Not setting the serial causes a crash.
        call->SetSerial(123);
        std::unique_ptr<dbus::Response> response =
            dbus::ErrorResponse::FromMethodCall(call, DBUS_ERROR_FAILED,
                                                "test");
        std::move(*callback).Run(response.get());
      }));

  FeaturedClient::Initialize(bus_.get());
  FeaturedClient* client = FeaturedClient::Get();

  ASSERT_NE(client, nullptr);

  ::featured::SeedDetails safe_seed;

  bool ran_callback = false;
  client->HandleSeedFetched(
      safe_seed, base::BindLambdaForTesting([&ran_callback](bool success) {
        EXPECT_FALSE(success);
        ran_callback = true;
      }));
  // Ensures the callback was executed.
  EXPECT_TRUE(ran_callback);

  FeaturedClient::Shutdown();

  EXPECT_EQ(FeaturedClient::Get(), nullptr);
}

// Check that `HandleSeedFetched` runs the callback with a false success value
// if the method call is unsuccessful (response is a nullptr).
TEST_F(FeaturedClientTest, HandleSeedFetched_Failure_NullResponse) {
  EXPECT_CALL(*proxy_,
              DoCallMethod(_, dbus::ObjectProxy::TIMEOUT_USE_DEFAULT, _))
      .WillOnce(Invoke([](dbus::MethodCall* call, int timeout_ms,
                          dbus::MockObjectProxy::ResponseCallback* callback) {
        std::move(*callback).Run(nullptr);
      }));

  FeaturedClient::Initialize(bus_.get());
  FeaturedClient* client = FeaturedClient::Get();

  ASSERT_NE(client, nullptr);

  ::featured::SeedDetails safe_seed;

  bool ran_callback = false;
  client->HandleSeedFetched(
      safe_seed, base::BindLambdaForTesting([&ran_callback](bool success) {
        EXPECT_FALSE(success);
        ran_callback = true;
      }));
  // Ensures the callback was executed.
  EXPECT_TRUE(ran_callback);

  FeaturedClient::Shutdown();

  EXPECT_EQ(FeaturedClient::Get(), nullptr);
}

// Check that Fake runs `HandleSeedFetched` callback with a false success value
// by default (no expected responses added).
TEST_F(FeaturedClientTest, FakeHandleSeedFetched_InvokeFalseByDefault) {
  FeaturedClient::InitializeFake();
  FakeFeaturedClient* client = FakeFeaturedClient::Get();

  ASSERT_NE(client, nullptr);

  ::featured::SeedDetails safe_seed;

  bool ran_callback = false;
  client->HandleSeedFetched(
      safe_seed, base::BindLambdaForTesting([&ran_callback](bool success) {
        EXPECT_FALSE(success);
        ran_callback = true;
      }));
  // Ensures the callback was executed.
  EXPECT_TRUE(ran_callback);
  EXPECT_EQ(client->handle_seed_fetched_attempts(), 1);

  FeaturedClient::Shutdown();

  EXPECT_EQ(FakeFeaturedClient::Get(), nullptr);
}

// Check that Fake runs `HandleSeedFetched` callback with value added by
// `AddResponse`.
TEST_F(FeaturedClientTest, FakeHandleSeedFetched_InvokeSuccessWhenSet) {
  FeaturedClient::InitializeFake();
  FakeFeaturedClient* client = FakeFeaturedClient::Get();

  ASSERT_NE(client, nullptr);

  ::featured::SeedDetails safe_seed;
  client->AddResponse(true);

  bool ran_callback = false;
  client->HandleSeedFetched(
      safe_seed, base::BindLambdaForTesting([&ran_callback](bool success) {
        EXPECT_TRUE(success);
        ran_callback = true;
      }));
  // Ensures the callback was executed.
  EXPECT_TRUE(ran_callback);
  EXPECT_EQ(client->handle_seed_fetched_attempts(), 1);

  FeaturedClient::Shutdown();

  EXPECT_EQ(FakeFeaturedClient::Get(), nullptr);
}

TEST_F(FeaturedClientTest, ReadTrialsActivatedBeforeChromeStartup_FilesExist) {
  // Create active trial files before FeaturedClient is initialized.
  EXPECT_TRUE(base::WriteFile(
      active_trials_dir_.Append("test_trial_1,test_group_1"), ""));
  EXPECT_TRUE(base::WriteFile(
      active_trials_dir_.Append("test_trial_2,test_group_2"), ""));

  std::map<std::string, std::string> expected;
  expected.insert({"test_trial_1", "test_group_1"});
  expected.insert({"test_trial_2", "test_group_2"});

  base::RunLoop run_loop;
  base::RepeatingClosure barrier_callback =
      base::BarrierClosure(2, run_loop.QuitClosure());
  std::map<std::string, std::string> actual;
  FeaturedClient::InitializeForTesting(
      bus_.get(), active_trials_dir_,
      base::BindLambdaForTesting(
          [&actual, &barrier_callback](const std::string& trial_name,
                                       const std::string& group_name) {
            actual.insert({trial_name, group_name});
            barrier_callback.Run();
          }));
  run_loop.Run();
  EXPECT_EQ(actual, expected);

  FeaturedClient::Shutdown();

  EXPECT_EQ(FeaturedClient::Get(), nullptr);
}

TEST_F(FeaturedClientTest,
       ReadTrialsActivatedBeforeChromeStartup_NoFilesExist) {
  std::map<std::string, std::string> actual_trials;
  FeaturedClient::InitializeForTesting(
      bus_.get(), active_trials_dir_,
      base::BindLambdaForTesting(
          [&actual_trials](const std::string& trial_name,
                           const std::string& group_name) {
            actual_trials.insert({trial_name, group_name});
          }));
  EXPECT_THAT(actual_trials, IsEmpty());

  FeaturedClient::Shutdown();

  EXPECT_EQ(FeaturedClient::Get(), nullptr);
}

TEST_F(FeaturedClientTest, ParseTrialFileName_ImproperFilename_MissingGroup) {
  FeaturedClient::InitializeForTesting(bus_.get(), active_trials_dir_,
                                       base::DoNothing());
  FeaturedClient* client = FeaturedClient::Get();

  ASSERT_NE(client, nullptr);

  base::FieldTrial::ActiveGroup active_group;
  base::FilePath trial_file = active_trials_dir_.Append("test_trial");

  EXPECT_TRUE(base::WriteFile(trial_file, ""));
  EXPECT_FALSE(ParseTrialFilename(trial_file, active_group));

  FeaturedClient::Shutdown();

  EXPECT_EQ(FeaturedClient::Get(), nullptr);
}

TEST_F(FeaturedClientTest,
       ParseTrialFileName_ImproperFilename_MissingSeparator) {
  FeaturedClient::InitializeForTesting(bus_.get(), active_trials_dir_,
                                       base::DoNothing());
  FeaturedClient* client = FeaturedClient::Get();

  ASSERT_NE(client, nullptr);

  base::FieldTrial::ActiveGroup active_group;
  base::FilePath trial_file = active_trials_dir_.Append("test_trialtest_group");

  EXPECT_TRUE(base::WriteFile(trial_file, ""));
  EXPECT_FALSE(ParseTrialFilename(trial_file, active_group));

  FeaturedClient::Shutdown();

  EXPECT_EQ(FeaturedClient::Get(), nullptr);
}

struct ParseProperFilenameTestParams {
  std::string expected_trial_name;
  std::string expected_group_name;
};

// Parameterized tests to check that properly formatted filenames with special
// characters (eg. whitespace, /, *, etc) are parsed correctly.
class FeaturedClientTrialFileTest
    : public FeaturedClientTest,
      public ::testing::WithParamInterface<ParseProperFilenameTestParams> {};

TEST_P(FeaturedClientTrialFileTest, ParseProperFilename) {
  FeaturedClient::InitializeForTesting(bus_.get(), active_trials_dir_,
                                       base::DoNothing());
  FeaturedClient* client = FeaturedClient::Get();
  ASSERT_NE(client, nullptr);

  const ParseProperFilenameTestParams test_case = GetParam();
  base::FilePath trial_file = active_trials_dir_.Append(CreateEscapedFilename(
      test_case.expected_trial_name, test_case.expected_group_name));
  EXPECT_TRUE(base::WriteFile(trial_file, ""));

  base::FieldTrial::ActiveGroup active_group;
  EXPECT_TRUE(ParseTrialFilename(trial_file, active_group));
  EXPECT_EQ(active_group.trial_name, test_case.expected_trial_name);
  EXPECT_EQ(active_group.group_name, test_case.expected_group_name);

  FeaturedClient::Shutdown();

  EXPECT_EQ(FeaturedClient::Get(), nullptr);
}

INSTANTIATE_TEST_SUITE_P(
    FeaturedClientTrialFileTestSuite,
    FeaturedClientTrialFileTest,
    testing::ValuesIn<ParseProperFilenameTestParams>({
        {"test_trial", "test_group"}, {",", "test_group"}, {"/", "test_group"},
        {"&", "test_group"},          {"!", "test_group"}, {"@", "test_group"},
        {"#", "test_group"},          {"$", "test_group"}, {"%", "test_group"},
        {"^", "test_group"},          {".", "test_group"}, {"~", "test_group"},
        {"-", "test_group"},          {"`", "test_group"}, {"(", "test_group"},
        {")", "test_group"},          {"`", "test_group"}, {"?", "test_group"},
        {"+", "test_group"},          {"=", "test_group"}, {" ", "test_group"},
    }));

#if DCHECK_IS_ON()
using FeaturedClientDeathTest = FeaturedClientTest;
TEST_F(FeaturedClientDeathTest, InitializeFailure_NullBus) {
  EXPECT_DEATH(FeaturedClient::Initialize(nullptr), "");
}

TEST_F(FeaturedClientDeathTest, DoubleInitialize) {
  FeaturedClient::Initialize(bus_.get());

  EXPECT_DEATH(FeaturedClient::Initialize(bus_.get()), "");

  FeaturedClient::Shutdown();

  EXPECT_EQ(FeaturedClient::Get(), nullptr);
}

TEST_F(FeaturedClientDeathTest, DoubleInitializeFake) {
  FeaturedClient::InitializeFake();

  EXPECT_DEATH(FeaturedClient::InitializeFake(), "");

  FakeFeaturedClient::Shutdown();

  EXPECT_EQ(FeaturedClient::Get(), nullptr);
}
#endif  // DCHECK_IS_ON()
}  // namespace ash::featured