chromium/chromeos/ash/components/dbus/biod/biod_client_unittest.cc

// Copyright 2017 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/biod/biod_client.h"

#include <map>
#include <memory>
#include <string>
#include <utility>

#include "base/functional/bind.h"
#include "base/memory/raw_ptr.h"
#include "base/run_loop.h"
#include "base/strings/stringprintf.h"
#include "base/test/task_environment.h"
#include "chromeos/ash/components/dbus/biod/messages.pb.h"
#include "chromeos/ash/components/dbus/biod/test_utils.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;

namespace ash {

namespace {

// Shorthand for a commonly-used constant.
const char* kInterface = biod::kBiometricsManagerInterface;

// Value used to intialize dbus::ObjectPath objects in tests to make it easier
// to determine when empty values have been assigned.
const char kInvalidTestPath[] = "/invalid/test/path";

// Value used to intialize string objects in tests to make it easier to
// determine when empty values have been assigned.
const char kInvalidString[] = "invalidString";

// Matcher that verifies that a dbus::Message has member |name|.
MATCHER_P(HasMember, name, "") {
  if (arg->GetMember() != name) {
    *result_listener << "has member " << arg->GetMember();
    return false;
  }
  return true;
}

// Runs |callback| with |response|. Needed due to ResponseCallback expecting a
// bare pointer rather than an std::unique_ptr.
void RunResponseCallback(dbus::ObjectProxy::ResponseCallback callback,
                         std::unique_ptr<dbus::Response> response) {
  std::move(callback).Run(response.get());
}

}  // namespace

class BiodClientTest : public testing::Test {
 public:
  BiodClientTest() = default;

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

  ~BiodClientTest() override = default;

  void SetUp() override {
    dbus::Bus::Options options;
    options.bus_type = dbus::Bus::SYSTEM;
    bus_ = new dbus::MockBus(options);

    dbus::ObjectPath fpc_bio_path = dbus::ObjectPath(base::StringPrintf(
        "%s/%s", biod::kBiodServicePath, biod::kCrosFpBiometricsManagerName));
    proxy_ = new dbus::MockObjectProxy(bus_.get(), biod::kBiodServiceName,
                                       fpc_bio_path);

    // |client_|'s Init() method should request a proxy for communicating with
    // biometrics api.
    EXPECT_CALL(*bus_.get(), GetObjectProxy(biod::kBiodServiceName, _))
        .WillRepeatedly(Return(proxy_.get()));

    // Save |client_|'s signal callback.
    EXPECT_CALL(*proxy_.get(), DoConnectToSignal(kInterface, _, _, _))
        .WillRepeatedly(Invoke(this, &BiodClientTest::ConnectToSignal));

    BiodClient::Initialize(bus_.get());
    client_ = BiodClient::Get();

    // Execute callbacks posted by Init().
    base::RunLoop().RunUntilIdle();
  }

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

  void GetBiometricType(biod::BiometricType type) { biometric_type_ = type; }

 protected:
  // Add an expectation for method with |method_name| to be called. When the
  // method is called the response should match |response|.
  void AddMethodExpectation(const std::string& method_name,
                            std::unique_ptr<dbus::Response> response) {
    ASSERT_FALSE(pending_method_calls_.count(method_name));
    pending_method_calls_[method_name] = std::move(response);
    EXPECT_CALL(*proxy_.get(), DoCallMethod(HasMember(method_name), _, _))
        .WillOnce(Invoke(this, &BiodClientTest::OnCallMethod));
  }

  // Synchronously passes |signal| to |client_|'s handler, simulating the signal
  // from biometrics.
  void EmitSignal(dbus::Signal* signal) {
    const std::string signal_name = signal->GetMember();
    const auto it = signal_callbacks_.find(signal_name);
    ASSERT_TRUE(it != signal_callbacks_.end())
        << "Client didn't register for signal " << signal_name;
    it->second.Run(signal);
  }

  // Passes a enroll scan done signal to |client_|.
  void EmitEnrollScanDoneSignal(biod::ScanResult scan_result,
                                bool enroll_session_complete,
                                int percent_complete) {
    dbus::Signal signal(kInterface,
                        biod::kBiometricsManagerEnrollScanDoneSignal);
    dbus::MessageWriter writer(&signal);
    biod::EnrollScanDone protobuf;
    protobuf.set_scan_result(scan_result);
    protobuf.set_done(enroll_session_complete);
    protobuf.set_percent_complete(percent_complete);
    writer.AppendProtoAsArrayOfBytes(protobuf);
    EmitSignal(&signal);
  }

  // Appends AuthScanMatches to |writer| message.
  void AppendMatchesArray(dbus::MessageWriter* writer,
                          const AuthScanMatches& matches) {
    dbus::MessageWriter array_writer(nullptr);
    writer->OpenArray("{sx}", &array_writer);
    for (auto& match : matches) {
      dbus::MessageWriter entry_writer(nullptr);
      array_writer.OpenDictEntry(&entry_writer);
      entry_writer.AppendString(match.first);
      entry_writer.AppendArrayOfObjectPaths(match.second);
      array_writer.CloseContainer(&entry_writer);
    }
    writer->CloseContainer(&array_writer);
  }

  // Passes a auth scan done signal to |client_|.
  void EmitAuthScanDoneSignal(const biod::FingerprintMessage& msg,
                              const AuthScanMatches& matches) {
    dbus::Signal signal(kInterface, biod::kBiometricsManagerAuthScanDoneSignal);
    dbus::MessageWriter writer(&signal);
    writer.AppendProtoAsArrayOfBytes(msg);
    AppendMatchesArray(&writer, matches);

    EmitSignal(&signal);
  }

  // Passes a scan failed signal to |client_|.
  void EmitScanFailedSignal() {
    dbus::Signal signal(kInterface,
                        biod::kBiometricsManagerSessionFailedSignal);
    EmitSignal(&signal);
  }

  std::map<std::string, std::unique_ptr<dbus::Response>> pending_method_calls_;

  base::test::SingleThreadTaskEnvironment task_environment_;

  // Mock bus and proxy for simulating calls.
  scoped_refptr<dbus::MockBus> bus_;
  scoped_refptr<dbus::MockObjectProxy> proxy_;

  // Convenience pointer to the global instance.
  raw_ptr<BiodClient, DanglingUntriaged> client_;

  // Maps from biod signal name to the corresponding callback provided by
  // |client_|.
  std::map<std::string, dbus::ObjectProxy::SignalCallback> signal_callbacks_;

  biod::BiometricType biometric_type_;

 private:
  // Handles calls to |proxy_|'s ConnectToSignal() method.
  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, kInterface);
    signal_callbacks_[signal_name] = signal_callback;
    task_environment_.GetMainThreadTaskRunner()->PostTask(
        FROM_HERE,
        base::BindOnce(std::move(*on_connected_callback), interface_name,
                       signal_name, true /* success */));
  }

  // Handles calls to |proxy_|'s CallMethod().
  void OnCallMethod(dbus::MethodCall* method_call,
                    int timeout_ms,
                    dbus::ObjectProxy::ResponseCallback* callback) {
    auto it = pending_method_calls_.find(method_call->GetMember());
    ASSERT_TRUE(it != pending_method_calls_.end());
    auto pending_response = std::move(it->second);
    pending_method_calls_.erase(it);

    task_environment_.GetMainThreadTaskRunner()->PostTask(
        FROM_HERE, base::BindOnce(&RunResponseCallback, std::move(*callback),
                                  std::move(pending_response)));
  }
};

TEST_F(BiodClientTest, TestStartEnrollSession) {
  const std::string kFakeId("fakeId");
  const std::string kFakeLabel("fakeLabel");
  const dbus::ObjectPath kFakeObjectPath(std::string("/fake/object/path"));
  const dbus::ObjectPath kFakeObjectPath2(std::string("/fake/object/path2"));

  // Verify that by sending a empty reponse or a improperly formatted one, the
  // response is an empty object path.
  AddMethodExpectation(biod::kBiometricsManagerStartEnrollSessionMethod,
                       nullptr);
  dbus::ObjectPath returned_path(kInvalidTestPath);
  client_->StartEnrollSession(
      kFakeId, kFakeLabel,
      base::BindOnce(&test_utils::CopyObjectPath, &returned_path));
  base::RunLoop().RunUntilIdle();
  EXPECT_EQ(dbus::ObjectPath(), returned_path);

  std::unique_ptr<dbus::Response> bad_response(dbus::Response::CreateEmpty());
  dbus::MessageWriter bad_writer(bad_response.get());
  bad_writer.AppendString("");
  AddMethodExpectation(biod::kBiometricsManagerStartEnrollSessionMethod,
                       std::move(bad_response));
  returned_path = dbus::ObjectPath(kInvalidTestPath);
  client_->StartEnrollSession(
      kFakeId, kFakeLabel,
      base::BindOnce(&test_utils::CopyObjectPath, &returned_path));
  base::RunLoop().RunUntilIdle();
  EXPECT_EQ(dbus::ObjectPath(), returned_path);

  std::unique_ptr<dbus::Response> response(dbus::Response::CreateEmpty());
  dbus::MessageWriter writer(response.get());
  writer.AppendObjectPath(kFakeObjectPath);

  // Create a fake response with a fake object path. The start enroll
  // call should return this object path.
  AddMethodExpectation(biod::kBiometricsManagerStartEnrollSessionMethod,
                       std::move(response));
  returned_path = dbus::ObjectPath(kInvalidTestPath);
  client_->StartEnrollSession(
      kFakeId, kFakeLabel,
      base::BindOnce(&test_utils::CopyObjectPath, &returned_path));
  base::RunLoop().RunUntilIdle();
  EXPECT_EQ(kFakeObjectPath, returned_path);
}

TEST_F(BiodClientTest, TestGetRecordsForUserWithRecords) {
  const std::string kFakeId("fakeId");
  const dbus::ObjectPath kFakeObjectPath(std::string("/fake/object/path"));
  const dbus::ObjectPath kFakeObjectPath2(std::string("/fake/object/path2"));
  const std::vector<dbus::ObjectPath> kFakeObjectPaths = {kFakeObjectPath,
                                                          kFakeObjectPath2};

  std::unique_ptr<dbus::Response> response(dbus::Response::CreateEmpty());
  dbus::MessageWriter writer(response.get());
  writer.AppendArrayOfObjectPaths(kFakeObjectPaths);

  // Create a fake response with an array of fake object paths. The get
  // records for user call should return this array of object paths.
  AddMethodExpectation(biod::kBiometricsManagerGetRecordsForUserMethod,
                       std::move(response));
  std::vector<dbus::ObjectPath> returned_object_paths = {
      dbus::ObjectPath(kInvalidTestPath)};
  bool returned_success = false;

  client_->GetRecordsForUser(
      kFakeId,
      base::BindOnce(
          [](std::vector<dbus::ObjectPath>& returned_paths,
             bool& returned_success, const std::vector<dbus::ObjectPath>& paths,
             bool success) {
            test_utils::CopyObjectPathArray(&returned_paths, paths);
            returned_success = success;
          },
          std::ref(returned_object_paths), std::ref(returned_success)));
  base::RunLoop().RunUntilIdle();
  EXPECT_EQ(returned_success, true);
  EXPECT_EQ(kFakeObjectPaths, returned_object_paths);
}

TEST_F(BiodClientTest, TestGetRecordsForUserWithoutRecords) {
  const std::string kFakeId("fakeId");
  std::unique_ptr<dbus::Response> response(dbus::Response::CreateEmpty());
  dbus::MessageWriter writer(response.get());

  // Create a fake response with an empty array. The response also
  // should be an empty array.
  AddMethodExpectation(biod::kBiometricsManagerGetRecordsForUserMethod,
                       std::move(response));
  std::vector<dbus::ObjectPath> returned_object_paths = {
      dbus::ObjectPath(kInvalidTestPath)};
  bool returned_success = false;

  client_->GetRecordsForUser(
      kFakeId,
      base::BindOnce(
          [](std::vector<dbus::ObjectPath>& returned_paths,
             bool& returned_success, const std::vector<dbus::ObjectPath>& paths,
             bool success) {
            test_utils::CopyObjectPathArray(&returned_paths, paths);
            returned_success = success;
          },
          std::ref(returned_object_paths), std::ref(returned_success)));

  base::RunLoop().RunUntilIdle();
  EXPECT_EQ(returned_success, true);
  EXPECT_EQ(std::vector<dbus::ObjectPath>(), returned_object_paths);
}

TEST_F(BiodClientTest, TestDestroyAllRecords) {
  std::unique_ptr<dbus::Response> response(dbus::Response::CreateEmpty());
  dbus::MessageWriter writer(response.get());

  // Create an empty response to simulate success.
  AddMethodExpectation(biod::kBiometricsManagerDestroyAllRecordsMethod,
                       std::move(response));
  bool result = false;
  client_->DestroyAllRecords(
      base::BindOnce(&test_utils::CopyDBusMethodCallResult, &result));
  base::RunLoop().RunUntilIdle();
  EXPECT_TRUE(result);

  // Return a null response to simulate failure.
  AddMethodExpectation(biod::kBiometricsManagerDestroyAllRecordsMethod,
                       nullptr);
  result = false;
  client_->DestroyAllRecords(
      base::BindOnce(&test_utils::CopyDBusMethodCallResult, &result));
  base::RunLoop().RunUntilIdle();
  EXPECT_FALSE(result);
}

TEST_F(BiodClientTest, TestRemoveRecord) {
  const dbus::ObjectPath kFakeObjectPath(std::string("/fake/object/path"));
  std::unique_ptr<dbus::Response> response(dbus::Response::CreateEmpty());
  dbus::MessageWriter writer(response.get());

  // Create an empty response to simulate success.
  AddMethodExpectation(biod::kRecordRemoveMethod, std::move(response));
  bool result = false;
  client_->RemoveRecord(
      kFakeObjectPath,
      base::BindOnce(&test_utils::CopyDBusMethodCallResult, &result));
  base::RunLoop().RunUntilIdle();
  EXPECT_TRUE(result);

  // Return a null response to simulate failure.
  AddMethodExpectation(biod::kRecordRemoveMethod, nullptr);
  result = false;
  client_->RemoveRecord(
      kFakeObjectPath,
      base::BindOnce(&test_utils::CopyDBusMethodCallResult, &result));
  base::RunLoop().RunUntilIdle();
  EXPECT_FALSE(result);
}

TEST_F(BiodClientTest, TestStartAuthentication) {
  const dbus::ObjectPath kFakeObjectPath(std::string("/fake/object/path"));

  // Verify that by sending a empty reponse or a improperly formatted one, the
  // response is an empty object path.
  AddMethodExpectation(biod::kBiometricsManagerStartAuthSessionMethod, nullptr);
  dbus::ObjectPath returned_path(kInvalidTestPath);
  client_->StartAuthSession(
      base::BindOnce(&test_utils::CopyObjectPath, &returned_path));
  base::RunLoop().RunUntilIdle();
  EXPECT_EQ(dbus::ObjectPath(), returned_path);

  std::unique_ptr<dbus::Response> bad_response(dbus::Response::CreateEmpty());
  dbus::MessageWriter bad_writer(bad_response.get());
  bad_writer.AppendString("");
  AddMethodExpectation(biod::kBiometricsManagerStartAuthSessionMethod,
                       std::move(bad_response));
  returned_path = dbus::ObjectPath(kInvalidTestPath);
  client_->StartAuthSession(
      base::BindOnce(&test_utils::CopyObjectPath, &returned_path));
  base::RunLoop().RunUntilIdle();
  EXPECT_EQ(dbus::ObjectPath(), returned_path);

  // Create a fake response with a fake object path. The start authentication
  // call should return this object path.
  std::unique_ptr<dbus::Response> response(dbus::Response::CreateEmpty());
  dbus::MessageWriter writer(response.get());
  writer.AppendObjectPath(kFakeObjectPath);

  AddMethodExpectation(biod::kBiometricsManagerStartAuthSessionMethod,
                       std::move(response));
  returned_path = dbus::ObjectPath(kInvalidTestPath);
  client_->StartAuthSession(
      base::BindOnce(&test_utils::CopyObjectPath, &returned_path));
  base::RunLoop().RunUntilIdle();
  EXPECT_EQ(kFakeObjectPath, returned_path);
}

TEST_F(BiodClientTest, TestRequestBiometricType) {
  const auto kFakeBiometricType = biod::BIOMETRIC_TYPE_FINGERPRINT;

  std::unique_ptr<dbus::Response> response(dbus::Response::CreateEmpty());
  dbus::MessageWriter writer(response.get());
  writer.AppendVariantOfUint32(static_cast<uint32_t>(kFakeBiometricType));

  // Create a fake response with biometric type. The get label call should
  // return this exact biometric type.
  biometric_type_ = biod::BIOMETRIC_TYPE_MAX;
  AddMethodExpectation(dbus::kDBusPropertiesGet, std::move(response));
  client_->RequestType(base::BindOnce(&BiodClientTest::GetBiometricType,
                                      base::Unretained(this)));
  base::RunLoop().RunUntilIdle();
  EXPECT_EQ(kFakeBiometricType, biometric_type_);

  // Verify that by sending a null reponse, the result is an unknown biometric
  // type.
  biometric_type_ = biod::BIOMETRIC_TYPE_MAX;
  AddMethodExpectation(dbus::kDBusPropertiesGet, nullptr);
  client_->RequestType(base::BindOnce(&BiodClientTest::GetBiometricType,
                                      base::Unretained(this)));
  base::RunLoop().RunUntilIdle();
  EXPECT_EQ(biod::BIOMETRIC_TYPE_UNKNOWN, biometric_type_);
}

TEST_F(BiodClientTest, TestRequestRecordLabel) {
  const std::string kFakeLabel("fakeLabel");
  const dbus::ObjectPath kFakeRecordPath(std::string("/fake/record/path"));

  std::unique_ptr<dbus::Response> response(dbus::Response::CreateEmpty());
  dbus::MessageWriter writer(response.get());
  writer.AppendVariantOfString(kFakeLabel);

  // Create a fake response with string. The get label call should return this
  // exact string.
  std::string returned_label = kInvalidString;
  AddMethodExpectation(dbus::kDBusPropertiesGet, std::move(response));
  client_->RequestRecordLabel(
      kFakeRecordPath,
      base::BindOnce(&test_utils::CopyString, &returned_label));
  base::RunLoop().RunUntilIdle();
  EXPECT_EQ(kFakeLabel, returned_label);

  // Verify that by sending a null reponse, the result is an empty string.
  returned_label = kInvalidString;
  AddMethodExpectation(dbus::kDBusPropertiesGet, nullptr);
  client_->RequestRecordLabel(
      kFakeRecordPath,
      base::BindOnce(&test_utils::CopyString, &returned_label));
  base::RunLoop().RunUntilIdle();
  EXPECT_EQ("", returned_label);
}

// Verify when signals are mocked, an observer will catch the signals as
// expected.
TEST_F(BiodClientTest, TestNotifyObserversFingerprintMessage) {
  test_utils::TestBiodObserver observer;
  client_->AddObserver(&observer);
  EXPECT_TRUE(client_->HasObserver(&observer));

  const biod::ScanResult scan_signal = biod::ScanResult::SCAN_RESULT_SUCCESS;
  const bool enroll_session_complete = false;
  const int percent_complete = 0;
  const AuthScanMatches test_attempt;
  biod::FingerprintMessage msg;
  msg.set_scan_result(scan_signal);
  EXPECT_EQ(0, observer.NumEnrollScansReceived());
  EXPECT_EQ(0, observer.NumAuthScansReceived());
  EXPECT_EQ(0, observer.num_failures_received());

  EmitEnrollScanDoneSignal(scan_signal, enroll_session_complete,
                           percent_complete);
  EXPECT_EQ(1, observer.NumEnrollScansReceived());

  EmitAuthScanDoneSignal(msg, test_attempt);
  EXPECT_EQ(1, observer.NumAuthScansReceived());

  EmitScanFailedSignal();
  EXPECT_EQ(1, observer.num_failures_received());

  client_->RemoveObserver(&observer);

  EmitEnrollScanDoneSignal(scan_signal, enroll_session_complete,
                           percent_complete);
  EmitAuthScanDoneSignal(msg, test_attempt);
  EXPECT_EQ(1, observer.NumEnrollScansReceived());
  EXPECT_EQ(1, observer.NumAuthScansReceived());
  EXPECT_EQ(1, observer.num_failures_received());
}
}  // namespace ash