chromium/chromeos/ash/components/dbus/dlcservice/dlcservice_client_unittest.cc

// Copyright 2019 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/dlcservice/dlcservice_client.h"

#include <algorithm>
#include <atomic>
#include <functional>
#include <memory>
#include <string_view>
#include <utility>
#include <vector>

#include "base/functional/bind.h"
#include "base/functional/callback_helpers.h"
#include "base/memory/raw_ptr.h"
#include "base/run_loop.h"
#include "base/test/task_environment.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"

using ::testing::_;
using ::testing::Invoke;
using ::testing::Return;
using ::testing::WithArg;

namespace ash {

namespace {
std::unique_ptr<dbus::Signal> CreateSignal(
    const dlcservice::DlcState& dlc_state) {
  auto signal =
      std::make_unique<dbus::Signal>("com.example.Interface", "SomeSignal");
  dbus::MessageWriter writer(signal.get());
  writer.AppendProtoAsArrayOfBytes(dlc_state);
  return signal;
}

class DlcserviceClientTest : public testing::Test {
 public:
  void SetUp() override {
    dbus::Bus::Options options;
    options.bus_type = dbus::Bus::SYSTEM;

    mock_bus_ = base::MakeRefCounted<::testing::NiceMock<dbus::MockBus>>(
        dbus::Bus::Options());

    mock_proxy_ = base::MakeRefCounted<dbus::MockObjectProxy>(
        mock_bus_.get(), dlcservice::kDlcServiceServiceName,
        dbus::ObjectPath(dlcservice::kDlcServiceServicePath));

    EXPECT_CALL(
        *mock_bus_.get(),
        GetObjectProxy(dlcservice::kDlcServiceServiceName,
                       dbus::ObjectPath(dlcservice::kDlcServiceServicePath)))
        .WillOnce(Return(mock_proxy_.get()));

    EXPECT_CALL(*mock_proxy_.get(),
                DoConnectToSignal(dlcservice::kDlcServiceInterface, _, _, _))
        .WillOnce(Invoke(this, &DlcserviceClientTest::ConnectToSignal));

    EXPECT_CALL(*mock_proxy_.get(), DoWaitForServiceToBeAvailable(_)).Times(1);

    DlcserviceClient::Initialize(mock_bus_.get());
    client_ = DlcserviceClient::Get();

    base::RunLoop().RunUntilIdle();
  }

  void TearDown() override { DlcserviceClient::Shutdown(); }

  void CallMethodWithErrorResponse(
      dbus::MethodCall* method_call,
      int timeout_ms,
      dbus::ObjectProxy::ResponseOrErrorCallback* callback) {
    dbus::Response* response = nullptr;
    dbus::ErrorResponse* err_response = nullptr;
    if (!responses_.empty()) {
      used_responses_.push_back(std::move(responses_.front()));
      responses_.pop_front();
      response = used_responses_.back().get();
    }
    if (!err_responses_.empty()) {
      used_err_responses_.push_back(std::move(err_responses_.front()));
      err_responses_.pop_front();
      err_response = used_err_responses_.back().get();
    }
    CHECK((response != nullptr) != (err_response != nullptr));
    task_environment_.GetMainThreadTaskRunner()->PostTask(
        FROM_HERE,
        base::BindOnce(std::move(*callback), response, err_response));
  }

 protected:
  dlcservice::InstallRequest CreateInstallRequest(
      const std::string& id = {},
      const std::string& omaha_url = {},
      bool reserve = false) {
    dlcservice::InstallRequest install_request;
    install_request.set_id(id);
    install_request.set_omaha_url(omaha_url);
    install_request.set_reserve(reserve);
    return install_request;
  }

  base::test::SingleThreadTaskEnvironment task_environment_{
      base::test::TaskEnvironment::TimeSource::MOCK_TIME};
  raw_ptr<DlcserviceClient, DanglingUntriaged> client_;
  scoped_refptr<dbus::MockBus> mock_bus_;
  scoped_refptr<dbus::MockObjectProxy> mock_proxy_;
  std::deque<std::unique_ptr<dbus::Response>> responses_;
  std::deque<std::unique_ptr<dbus::ErrorResponse>> err_responses_;

 private:
  void ConnectToSignal(
      const std::string& interface_name,
      const std::string& signal_name,
      dbus::ObjectProxy::SignalCallback signal_callback,
      dbus::ObjectProxy::OnConnectedCallback* on_connected_callback) {
    EXPECT_EQ(interface_name, dlcservice::kDlcServiceInterface);
    task_environment_.GetMainThreadTaskRunner()->PostTask(
        FROM_HERE,
        base::BindOnce(std::move(*on_connected_callback), interface_name,
                       signal_name, true /* success */));
  }

  std::deque<std::unique_ptr<dbus::Response>> used_responses_;
  std::deque<std::unique_ptr<dbus::ErrorResponse>> used_err_responses_;
};

class MockObserver : public DlcserviceClient::Observer {
 public:
  MOCK_METHOD(void,
              OnDlcStateChanged,
              (const dlcservice::DlcState& dlc_state),
              ());
};

TEST_F(DlcserviceClientTest, GetDlcStateSuccessTest) {
  responses_.push_back(dbus::Response::CreateEmpty());
  dbus::Response* response = responses_.front().get();
  dbus::MessageWriter writer(response);
  dlcservice::DlcState dlc_state;
  writer.AppendProtoAsArrayOfBytes(dlc_state);

  EXPECT_CALL(*mock_proxy_.get(), DoCallMethodWithErrorResponse(_, _, _))
      .WillOnce(
          Invoke(this, &DlcserviceClientTest::CallMethodWithErrorResponse));

  DlcserviceClient::GetDlcStateCallback callback =
      base::BindOnce([](std::string_view err, const dlcservice::DlcState&) {
        EXPECT_EQ(dlcservice::kErrorNone, err);
      });
  client_->GetDlcState("some-dlc-id", std::move(callback));
  base::RunLoop().RunUntilIdle();
}

TEST_F(DlcserviceClientTest, GetDlcStateFailureTest) {
  dbus::MethodCall method_call(dlcservice::kDlcServiceInterface,
                               dlcservice::kGetDlcStateMethod);
  method_call.SetSerial(123);
  err_responses_.push_back(dbus::ErrorResponse::FromMethodCall(
      &method_call, DBUS_ERROR_FAILED, "some-unknown-error"));

  EXPECT_CALL(*mock_proxy_.get(), DoCallMethodWithErrorResponse(_, _, _))
      .WillRepeatedly(
          Invoke(this, &DlcserviceClientTest::CallMethodWithErrorResponse));

  client_->GetDlcState(
      "some-dlc-id",
      base::BindOnce([](std::string_view err, const dlcservice::DlcState&) {
        EXPECT_EQ(dlcservice::kErrorInternal, err);
      }));
  base::RunLoop().RunUntilIdle();

  err_responses_.push_back(dbus::ErrorResponse::FromMethodCall(
      &method_call, dlcservice::kErrorInvalidDlc,
      "Some error due to bad DLC."));

  client_->GetDlcState(
      "some-dlc-id",
      base::BindOnce([](std::string_view err, const dlcservice::DlcState&) {
        EXPECT_EQ(dlcservice::kErrorInvalidDlc, err);
      }));
  base::RunLoop().RunUntilIdle();
}

TEST_F(DlcserviceClientTest, GetExistingDlcsSuccessTest) {
  responses_.push_back(dbus::Response::CreateEmpty());
  dbus::Response* response = responses_.front().get();
  dbus::MessageWriter writer(response);
  dlcservice::DlcsWithContent dlcs_with_content;
  writer.AppendProtoAsArrayOfBytes(dlcs_with_content);

  EXPECT_CALL(*mock_proxy_.get(), DoCallMethodWithErrorResponse(_, _, _))
      .WillOnce(
          Invoke(this, &DlcserviceClientTest::CallMethodWithErrorResponse));

  DlcserviceClient::GetExistingDlcsCallback callback = base::BindOnce(
      [](std::string_view err, const dlcservice::DlcsWithContent&) {
        EXPECT_EQ(dlcservice::kErrorNone, err);
      });
  client_->GetExistingDlcs(std::move(callback));
  base::RunLoop().RunUntilIdle();
}

TEST_F(DlcserviceClientTest, GetExistingDlcsFailureTest) {
  dbus::MethodCall method_call(dlcservice::kDlcServiceInterface,
                               dlcservice::kGetExistingDlcsMethod);
  method_call.SetSerial(123);
  err_responses_.push_back(dbus::ErrorResponse::FromMethodCall(
      &method_call, DBUS_ERROR_FAILED, "some-unknown-error"));

  EXPECT_CALL(*mock_proxy_.get(), DoCallMethodWithErrorResponse(_, _, _))
      .WillRepeatedly(
          Invoke(this, &DlcserviceClientTest::CallMethodWithErrorResponse));

  client_->GetExistingDlcs(base::BindOnce(
      [](std::string_view err, const dlcservice::DlcsWithContent&) {
        EXPECT_EQ(dlcservice::kErrorInternal, err);
      }));
  base::RunLoop().RunUntilIdle();

  err_responses_.push_back(dbus::ErrorResponse::FromMethodCall(
      &method_call, dlcservice::kErrorInvalidDlc,
      "Some error due to bad DLC."));

  client_->GetExistingDlcs(base::BindOnce(
      [](std::string_view err, const dlcservice::DlcsWithContent&) {
        EXPECT_EQ(dlcservice::kErrorInvalidDlc, err);
      }));
  base::RunLoop().RunUntilIdle();
}

TEST_F(DlcserviceClientTest, UninstallSuccessTest) {
  responses_.push_back(dbus::Response::CreateEmpty());

  EXPECT_CALL(*mock_proxy_.get(), DoCallMethodWithErrorResponse(_, _, _))
      .WillOnce(
          Invoke(this, &DlcserviceClientTest::CallMethodWithErrorResponse));

  DlcserviceClient::UninstallCallback callback = base::BindOnce(
      [](std::string_view err) { EXPECT_EQ(dlcservice::kErrorNone, err); });
  client_->Uninstall("some-dlc-id", std::move(callback));
  base::RunLoop().RunUntilIdle();
}

TEST_F(DlcserviceClientTest, UninstallFailureTest) {
  dbus::MethodCall method_call(dlcservice::kDlcServiceInterface,
                               dlcservice::kUninstallMethod);
  method_call.SetSerial(123);
  err_responses_.push_back(dbus::ErrorResponse::FromMethodCall(
      &method_call, dlcservice::kErrorInternal, ""));

  EXPECT_CALL(*mock_proxy_.get(), DoCallMethodWithErrorResponse(_, _, _))
      .WillRepeatedly(
          Invoke(this, &DlcserviceClientTest::CallMethodWithErrorResponse));

  DlcserviceClient::UninstallCallback callback = base::BindOnce(
      [](std::string_view err) { EXPECT_EQ(dlcservice::kErrorInternal, err); });
  client_->Uninstall("some-dlc-id", std::move(callback));
  base::RunLoop().RunUntilIdle();
}

TEST_F(DlcserviceClientTest, UninstallBusyStatusTest) {
  dbus::MethodCall method_call(dlcservice::kDlcServiceInterface,
                               dlcservice::kUninstallMethod);
  method_call.SetSerial(123);
  err_responses_.push_back(dbus::ErrorResponse::FromMethodCall(
      &method_call, dlcservice::kErrorBusy, ""));

  EXPECT_CALL(*mock_proxy_.get(), DoCallMethodWithErrorResponse(_, _, _))
      .WillRepeatedly(
          Invoke(this, &DlcserviceClientTest::CallMethodWithErrorResponse));

  DlcserviceClient::UninstallCallback callback = base::BindOnce(
      [](std::string_view err) { EXPECT_EQ(dlcservice::kErrorBusy, err); });
  client_->Uninstall("some-dlc-id", std::move(callback));
  base::RunLoop().RunUntilIdle();
}

TEST_F(DlcserviceClientTest, PurgeSuccessTest) {
  responses_.push_back(dbus::Response::CreateEmpty());

  EXPECT_CALL(*mock_proxy_.get(), DoCallMethodWithErrorResponse(_, _, _))
      .WillOnce(
          Invoke(this, &DlcserviceClientTest::CallMethodWithErrorResponse));

  DlcserviceClient::PurgeCallback callback = base::BindOnce(
      [](std::string_view err) { EXPECT_EQ(dlcservice::kErrorNone, err); });
  client_->Purge("some-dlc-id", std::move(callback));
  base::RunLoop().RunUntilIdle();
}

TEST_F(DlcserviceClientTest, PurgeFailureTest) {
  dbus::MethodCall method_call(dlcservice::kDlcServiceInterface,
                               dlcservice::kPurgeMethod);
  method_call.SetSerial(123);
  err_responses_.push_back(dbus::ErrorResponse::FromMethodCall(
      &method_call, dlcservice::kErrorInternal, ""));

  EXPECT_CALL(*mock_proxy_.get(), DoCallMethodWithErrorResponse(_, _, _))
      .WillRepeatedly(
          Invoke(this, &DlcserviceClientTest::CallMethodWithErrorResponse));

  DlcserviceClient::PurgeCallback callback = base::BindOnce(
      [](std::string_view err) { EXPECT_EQ(dlcservice::kErrorInternal, err); });
  client_->Purge("some-dlc-id", std::move(callback));
  base::RunLoop().RunUntilIdle();
}

TEST_F(DlcserviceClientTest, PurgeBusyStatusTest) {
  dbus::MethodCall method_call(dlcservice::kDlcServiceInterface,
                               dlcservice::kPurgeMethod);
  method_call.SetSerial(123);
  err_responses_.push_back(dbus::ErrorResponse::FromMethodCall(
      &method_call, dlcservice::kErrorBusy, ""));

  EXPECT_CALL(*mock_proxy_.get(), DoCallMethodWithErrorResponse(_, _, _))
      .WillRepeatedly(
          Invoke(this, &DlcserviceClientTest::CallMethodWithErrorResponse));

  DlcserviceClient::PurgeCallback callback = base::BindOnce(
      [](std::string_view err) { EXPECT_EQ(dlcservice::kErrorBusy, err); });
  client_->Purge("some-dlc-id", std::move(callback));
  base::RunLoop().RunUntilIdle();
}

TEST_F(DlcserviceClientTest, InstallSuccessTest) {
  responses_.push_back(dbus::Response::CreateEmpty());

  EXPECT_CALL(*mock_proxy_.get(), DoCallMethodWithErrorResponse(_, _, _))
      .WillOnce(
          Invoke(this, &DlcserviceClientTest::CallMethodWithErrorResponse));

  DlcserviceClient::InstallCallback install_callback =
      base::BindOnce([](const DlcserviceClient::InstallResult& install_result) {
        EXPECT_EQ(dlcservice::kErrorNone, install_result.error);
      });
  client_->Install(CreateInstallRequest("foo-dlc"), std::move(install_callback),
                   base::DoNothing());
  base::RunLoop().RunUntilIdle();
}

TEST_F(DlcserviceClientTest, InstallFailureTest) {
  dbus::MethodCall method_call(dlcservice::kDlcServiceInterface,
                               dlcservice::kInstallMethod);
  method_call.SetSerial(123);
  err_responses_.push_back(dbus::ErrorResponse::FromMethodCall(
      &method_call, dlcservice::kErrorInternal, ""));

  EXPECT_CALL(*mock_proxy_.get(), DoCallMethodWithErrorResponse(_, _, _))
      .WillOnce(
          Invoke(this, &DlcserviceClientTest::CallMethodWithErrorResponse));

  DlcserviceClient::InstallCallback install_callback =
      base::BindOnce([](const DlcserviceClient::InstallResult& install_result) {
        EXPECT_EQ(dlcservice::kErrorInternal, install_result.error);
      });
  client_->Install(CreateInstallRequest("foo-dlc"), std::move(install_callback),
                   base::DoNothing());
  base::RunLoop().RunUntilIdle();
}

TEST_F(DlcserviceClientTest, InstallProgressTest) {
  EXPECT_CALL(*mock_proxy_.get(), DoCallMethodWithErrorResponse(_, _, _))
      .WillOnce(
          Invoke(this, &DlcserviceClientTest::CallMethodWithErrorResponse));
  std::atomic<size_t> counter{0};
  DlcserviceClient::InstallCallback install_callback = base::BindOnce(
      [](const DlcserviceClient::InstallResult& install_result) {});
  DlcserviceClient::ProgressCallback progress_callback = base::BindRepeating(
      [](decltype(counter)* counter, double) { ++*counter; }, &counter);

  responses_.push_back(dbus::Response::CreateEmpty());
  client_->Install(CreateInstallRequest(), std::move(install_callback),
                   std::move(progress_callback));
  base::RunLoop().RunUntilIdle();
  EXPECT_EQ(0u, counter.load());

  dlcservice::DlcState dlc_state;
  dlc_state.set_state(dlcservice::DlcState::INSTALLING);
  auto signal = CreateSignal(dlc_state);

  client_->DlcStateChangedForTest(signal.get());
  base::RunLoop().RunUntilIdle();
  EXPECT_EQ(1u, counter.load());
}

TEST_F(DlcserviceClientTest, InstallProgressSkipUnheldDlcIdsTest) {
  EXPECT_CALL(*mock_proxy_.get(), DoCallMethodWithErrorResponse(_, _, _))
      .WillOnce(Return());
  std::atomic<size_t> counter{0};
  DlcserviceClient::InstallCallback install_callback = base::BindOnce(
      [](const DlcserviceClient::InstallResult& install_result) {});
  DlcserviceClient::ProgressCallback progress_callback = base::BindRepeating(
      [](decltype(counter)* counter, double) { ++*counter; }, &counter);

  client_->Install(CreateInstallRequest("foo"), std::move(install_callback),
                   std::move(progress_callback));
  base::RunLoop().RunUntilIdle();
  EXPECT_EQ(0u, counter.load());

  dlcservice::DlcState dlc_state;
  dlc_state.set_id("bar-is-not-foo");
  dlc_state.set_state(dlcservice::DlcState::INSTALLING);
  auto signal = CreateSignal(dlc_state);

  client_->DlcStateChangedForTest(signal.get());
  base::RunLoop().RunUntilIdle();
  EXPECT_EQ(0u, counter.load());
}

TEST_F(DlcserviceClientTest, InstallBusyStatusTest) {
  dbus::MethodCall method_call(dlcservice::kDlcServiceInterface,
                               dlcservice::kInstallMethod);
  method_call.SetSerial(123);
  err_responses_.push_back(dbus::ErrorResponse::FromMethodCall(
      &method_call, dlcservice::kErrorBusy, ""));
  err_responses_.push_back(dbus::ErrorResponse::FromMethodCall(
      &method_call, dlcservice::kErrorNone, ""));

  EXPECT_CALL(*mock_proxy_.get(), DoCallMethodWithErrorResponse(_, _, _))
      .WillRepeatedly(
          Invoke(this, &DlcserviceClientTest::CallMethodWithErrorResponse));

  DlcserviceClient::InstallCallback install_callback =
      base::BindOnce([](const DlcserviceClient::InstallResult& install_result) {
        EXPECT_EQ(dlcservice::kErrorNone, install_result.error);
      });
  client_->Install(CreateInstallRequest("foo-dlc"), std::move(install_callback),
                   base::DoNothing());
  base::RunLoop().RunUntilIdle();
}

TEST_F(DlcserviceClientTest, PendingTaskTest) {
  const size_t kLoopCount = 3;
  EXPECT_CALL(*mock_proxy_.get(), DoCallMethodWithErrorResponse(_, _, _))
      .Times(kLoopCount)
      .WillRepeatedly(
          Invoke(this, &DlcserviceClientTest::CallMethodWithErrorResponse));
  std::atomic<size_t> counter{0};

  // All |Install()| request after the first should be queued.
  for (size_t i = 0; i < kLoopCount; ++i) {
    DlcserviceClient::InstallCallback install_callback = base::BindOnce(
        [](decltype(counter)* counter,
           const DlcserviceClient::InstallResult& install_result) {
          ++*counter;
        },
        &counter);
    responses_.push_back(dbus::Response::CreateEmpty());
    client_->Install(CreateInstallRequest(), std::move(install_callback),
                     base::DoNothing());
  }
  base::RunLoop().RunUntilIdle();
  EXPECT_EQ(0u, counter.load());

  dlcservice::DlcState dlc_state;
  dlc_state.set_state(dlcservice::DlcState::INSTALLED);
  auto signal = CreateSignal(dlc_state);

  for (size_t i = 1; i < 100; ++i) {
    client_->DlcStateChangedForTest(signal.get());
    task_environment_.FastForwardBy(base::Seconds(3));
    base::RunLoop().RunUntilIdle();
    EXPECT_EQ(i <= kLoopCount ? i : kLoopCount, counter.load());
  }
}

TEST_F(DlcserviceClientTest, StateChangeObserver) {
  dlcservice::DlcState dlc_state;
  dlc_state.set_state(dlcservice::DlcState::INSTALLED);
  auto signal = CreateSignal(dlc_state);

  MockObserver observer;
  // If no observer has been added, nothing should be called.
  EXPECT_CALL(observer, OnDlcStateChanged(_)).Times(0);
  client_->DlcStateChangedForTest(signal.get());

  // Adding one observer should call once.
  EXPECT_CALL(observer, OnDlcStateChanged(_)).Times(1);
  client_->AddObserver(&observer);
  client_->DlcStateChangedForTest(signal.get());

  // Removing the observer causes nothing to be called.
  EXPECT_CALL(observer, OnDlcStateChanged(_)).Times(0);
  client_->RemoveObserver(&observer);
  client_->DlcStateChangedForTest(signal.get());
}

}  // namespace

}  // namespace ash