chromium/chrome/browser/nearby_sharing/local_device_data/nearby_share_local_device_data_manager_impl_unittest.cc

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

#include "chrome/browser/nearby_sharing/local_device_data/nearby_share_local_device_data_manager_impl.h"

#include <locale>
#include <memory>
#include <optional>
#include <string>

#include "base/strings/utf_string_conversions.h"
#include "base/test/bind.h"
#include "chrome/browser/nearby_sharing/client/fake_nearby_share_client.h"
#include "chrome/browser/nearby_sharing/common/fake_nearby_share_profile_info_provider.h"
#include "chrome/browser/nearby_sharing/common/nearby_share_prefs.h"
#include "chrome/browser/nearby_sharing/local_device_data/fake_nearby_share_device_data_updater.h"
#include "chrome/browser/nearby_sharing/local_device_data/nearby_share_device_data_updater.h"
#include "chrome/browser/nearby_sharing/local_device_data/nearby_share_device_data_updater_impl.h"
#include "chrome/grit/generated_resources.h"
#include "chromeos/ash/components/nearby/common/scheduling/fake_nearby_scheduler.h"
#include "chromeos/ash/components/nearby/common/scheduling/fake_nearby_scheduler_factory.h"
#include "chromeos/ash/components/nearby/common/scheduling/nearby_scheduler_factory.h"
#include "components/sync_preferences/testing_pref_service_syncable.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/abseil-cpp/absl/strings/ascii.h"
#include "third_party/nearby/sharing/proto/device_rpc.pb.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/chromeos/devicetype_utils.h"

namespace {

const char kFakeDeviceName[] = "My Cool Chromebook";
const char kFakeEmptyDeviceName[] = "";
const char kFakeFullName[] = "Barack Obama";
const char16_t kFakeGivenName[] = u"Barack";
const char kFakeIconUrl[] = "https://www.google.com";
const char kFakeIconUrl2[] = "https://www.google.com/2";
const char kFakeIconToken[] = "token";
const char kFakeIconToken2[] = "token2";
const char kFakeInvalidDeviceName[] = "\xC0";
const char kFakeTooLongDeviceName[] = "this string is 33 bytes in UTF-8!";
const char16_t kFakeTooLongGivenName[] = u"this is a 33-byte string in utf-8";
const char kFakeTooLongTruncatedDeviceName[] =
    "this is a 33-...'s Chrome device";

nearby::sharing::proto::UpdateDeviceResponse CreateResponse(
    const std::optional<std::string>& full_name,
    const std::optional<std::string>& icon_url,
    const std::optional<std::string>& icon_token) {
  nearby::sharing::proto::UpdateDeviceResponse response;
  if (full_name)
    response.set_person_name(*full_name);

  if (icon_url)
    response.set_image_url(*icon_url);

  if (icon_token)
    response.set_image_token(*icon_token);

  return response;
}

std::vector<nearby::sharing::proto::Contact> GetFakeContacts() {
  nearby::sharing::proto::Contact contact1;
  nearby::sharing::proto::Contact contact2;
  contact1.mutable_identifier()->set_account_name("account1");
  contact2.mutable_identifier()->set_account_name("account2");
  return {std::move(contact1), std::move(contact2)};
}

std::vector<nearby::sharing::proto::PublicCertificate> GetFakeCertificates() {
  nearby::sharing::proto::PublicCertificate cert1;
  nearby::sharing::proto::PublicCertificate cert2;
  cert1.set_secret_id("id1");
  cert2.set_secret_id("id2");
  return {std::move(cert1), std::move(cert2)};
}

}  // namespace

class NearbyShareLocalDeviceDataManagerImplTest
    : public ::testing::Test,
      public NearbyShareLocalDeviceDataManager::Observer {
 protected:
  struct ObserverNotification {
    ObserverNotification(bool did_device_name_change,
                         bool did_full_name_change,
                         bool did_icon_change)
        : did_device_name_change(did_device_name_change),
          did_full_name_change(did_full_name_change),
          did_icon_change(did_icon_change) {}
    ~ObserverNotification() = default;
    bool operator==(const ObserverNotification& other) const {
      return did_device_name_change == other.did_device_name_change &&
             did_full_name_change == other.did_full_name_change &&
             did_icon_change == other.did_icon_change;
    }

    bool did_device_name_change;
    bool did_full_name_change;
    bool did_icon_change;
  };

  NearbyShareLocalDeviceDataManagerImplTest() = default;
  ~NearbyShareLocalDeviceDataManagerImplTest() override = default;

  void SetUp() override {
    RegisterNearbySharingPrefs(pref_service_.registry());
    ash::nearby::NearbySchedulerFactory::SetFactoryForTesting(
        &scheduler_factory_);
    NearbyShareDeviceDataUpdaterImpl::Factory::SetFactoryForTesting(
        &updater_factory_);
    profile_info_provider()->set_given_name(kFakeGivenName);
  }

  void TearDown() override {
    ash::nearby::NearbySchedulerFactory::SetFactoryForTesting(nullptr);
    NearbyShareDeviceDataUpdaterImpl::Factory::SetFactoryForTesting(nullptr);
  }

  // NearbyShareLocalDeviceDataManager::Observer:
  void OnLocalDeviceDataChanged(bool did_device_name_change,
                                bool did_full_name_change,
                                bool did_icon_change) override {
    notifications_.emplace_back(did_device_name_change, did_full_name_change,
                                did_icon_change);
  }

  void CreateManager() {
    manager_ = NearbyShareLocalDeviceDataManagerImpl::Factory::Create(
        &pref_service_, &http_client_factory_, &profile_info_provider_);
    manager_->AddObserver(this);
    ++num_manager_creations_;
    VerifyInitialization();
    manager_->Start();
  }

  void DestroyManager() {
    manager_->RemoveObserver(this);
    manager_.reset();
  }

  void DownloadDeviceData(
      const std::optional<nearby::sharing::proto::UpdateDeviceResponse>&
          response) {
    manager_->DownloadDeviceData();

    // The scheduler requests a download of device data from the server.
    EXPECT_TRUE(updater()->pending_requests().empty());
    device_data_scheduler()->InvokeRequestCallback();
    EXPECT_FALSE(updater()->pending_requests().empty());

    EXPECT_FALSE(updater()->pending_requests().front().contacts);
    EXPECT_FALSE(updater()->pending_requests().front().certificates);

    size_t num_handled_results =
        device_data_scheduler()->handled_results().size();
    updater()->RunNextRequest(response);
    EXPECT_EQ(num_handled_results + 1,
              device_data_scheduler()->handled_results().size());
    EXPECT_EQ(response.has_value(),
              device_data_scheduler()->handled_results().back());
  }

  void UploadContacts(
      const std::optional<nearby::sharing::proto::UpdateDeviceResponse>&
          response) {
    std::optional<bool> returned_success;
    manager_->UploadContacts(
        GetFakeContacts(),
        base::BindOnce([](std::optional<bool>* returned_success,
                          bool success) { *returned_success = success; },
                       &returned_success));

    EXPECT_FALSE(updater()->pending_requests().front().certificates);
    std::vector<nearby::sharing::proto::Contact> expected_fake_contacts =
        GetFakeContacts();
    for (size_t i = 0; i < expected_fake_contacts.size(); ++i) {
      EXPECT_EQ(expected_fake_contacts[i].SerializeAsString(),
                updater()
                    ->pending_requests()
                    .front()
                    .contacts->at(i)
                    .SerializeAsString());
    }

    EXPECT_FALSE(returned_success);
    updater()->RunNextRequest(response);
    EXPECT_EQ(response.has_value(), returned_success);
  }

  void UploadCertificates(
      const std::optional<nearby::sharing::proto::UpdateDeviceResponse>&
          response) {
    std::optional<bool> returned_success;
    manager_->UploadCertificates(
        GetFakeCertificates(),
        base::BindOnce([](std::optional<bool>* returned_success,
                          bool success) { *returned_success = success; },
                       &returned_success));

    EXPECT_FALSE(updater()->pending_requests().front().contacts);
    std::vector<nearby::sharing::proto::PublicCertificate>
        expected_fake_certificates = GetFakeCertificates();
    for (size_t i = 0; i < expected_fake_certificates.size(); ++i) {
      EXPECT_EQ(expected_fake_certificates[i].SerializeAsString(),
                updater()
                    ->pending_requests()
                    .front()
                    .certificates->at(i)
                    .SerializeAsString());
    }

    EXPECT_FALSE(returned_success);
    updater()->RunNextRequest(response);
    EXPECT_EQ(response.has_value(), returned_success);
  }

  NearbyShareLocalDeviceDataManager* manager() { return manager_.get(); }
  FakeNearbyShareProfileInfoProvider* profile_info_provider() {
    return &profile_info_provider_;
  }
  const std::vector<ObserverNotification>& notifications() {
    return notifications_;
  }
  FakeNearbyShareDeviceDataUpdater* updater() {
    return updater_factory_.instances().back();
  }
  ash::nearby::FakeNearbyScheduler* device_data_scheduler() {
    return scheduler_factory_.pref_name_to_periodic_instance()
        .at(prefs::kNearbySharingSchedulerDownloadDeviceDataPrefName)
        .fake_scheduler;
  }

 private:
  void VerifyInitialization() {
    // Verify updater inputs.
    EXPECT_LT(base::Seconds(1), updater_factory_.latest_timeout());
    EXPECT_EQ(&http_client_factory_, updater_factory_.latest_client_factory());
    ASSERT_EQ(num_manager_creations_, updater_factory_.instances().size());
    EXPECT_EQ(manager_->GetId(),
              updater_factory_.instances().back()->device_id());

    // Verify device data scheduler input parameters.
    ash::nearby::FakeNearbySchedulerFactory::PeriodicInstance
        device_data_scheduler_instance =
            scheduler_factory_.pref_name_to_periodic_instance().at(
                prefs::kNearbySharingSchedulerDownloadDeviceDataPrefName);
    EXPECT_TRUE(device_data_scheduler_instance.fake_scheduler);
    EXPECT_EQ(base::Hours(12), device_data_scheduler_instance.request_period);
    EXPECT_TRUE(device_data_scheduler_instance.retry_failures);
    EXPECT_TRUE(device_data_scheduler_instance.require_connectivity);
    EXPECT_EQ(&pref_service_, device_data_scheduler_instance.pref_service);
  }

  size_t num_manager_creations_ = 0;
  std::vector<ObserverNotification> notifications_;
  sync_preferences::TestingPrefServiceSyncable pref_service_;
  FakeNearbyShareClientFactory http_client_factory_;
  FakeNearbyShareProfileInfoProvider profile_info_provider_;
  ash::nearby::FakeNearbySchedulerFactory scheduler_factory_;
  FakeNearbyShareDeviceDataUpdaterFactory updater_factory_;
  std::unique_ptr<NearbyShareLocalDeviceDataManager> manager_;
};

TEST_F(NearbyShareLocalDeviceDataManagerImplTest, DeviceId) {
  CreateManager();

  // A 10-character alphanumeric ID is automatically generated if one doesn't
  // already exist.
  std::string id = manager()->GetId();
  EXPECT_EQ(10u, id.size());
  for (const char c : id)
    EXPECT_TRUE(absl::ascii_isalnum(static_cast<unsigned char>(c)));

  // The ID is persisted.
  DestroyManager();
  CreateManager();
  EXPECT_EQ(id, manager()->GetId());
}

TEST_F(NearbyShareLocalDeviceDataManagerImplTest, DefaultDeviceName) {
  CreateManager();

  // If given name is null, only return the device type.
  profile_info_provider()->set_given_name(std::nullopt);
  EXPECT_EQ(base::UTF16ToUTF8(ui::GetChromeOSDeviceName()),
            manager()->GetDeviceName());

  // Set given name and expect full default device name of the form
  // "<given name>'s <device type>."
  profile_info_provider()->set_given_name(kFakeGivenName);
  EXPECT_EQ(
      l10n_util::GetStringFUTF8(IDS_NEARBY_DEFAULT_DEVICE_NAME, kFakeGivenName,
                                ui::GetChromeOSDeviceName()),
      manager()->GetDeviceName());

  // Make sure that when we use a given name that is very long we truncate
  // correctly.
  profile_info_provider()->set_given_name(kFakeTooLongGivenName);
  EXPECT_EQ(kFakeTooLongTruncatedDeviceName, manager()->GetDeviceName());
}

TEST_F(NearbyShareLocalDeviceDataManagerImplTest, ValidateDeviceName) {
  CreateManager();
  EXPECT_EQ(manager()->ValidateDeviceName(kFakeDeviceName),
            nearby_share::mojom::DeviceNameValidationResult::kValid);
  EXPECT_EQ(manager()->ValidateDeviceName(kFakeEmptyDeviceName),
            nearby_share::mojom::DeviceNameValidationResult::kErrorEmpty);
  EXPECT_EQ(manager()->ValidateDeviceName(kFakeTooLongDeviceName),
            nearby_share::mojom::DeviceNameValidationResult::kErrorTooLong);
  EXPECT_EQ(
      manager()->ValidateDeviceName(kFakeInvalidDeviceName),
      nearby_share::mojom::DeviceNameValidationResult::kErrorNotValidUtf8);
}

TEST_F(NearbyShareLocalDeviceDataManagerImplTest, SetDeviceName) {
  CreateManager();

  profile_info_provider()->set_given_name(kFakeGivenName);
  std::string expected_default_device_name =
      l10n_util::GetStringFUTF8(IDS_NEARBY_DEFAULT_DEVICE_NAME, kFakeGivenName,
                                ui::GetChromeOSDeviceName());
  EXPECT_EQ(expected_default_device_name, manager()->GetDeviceName());
  EXPECT_TRUE(notifications().empty());

  auto error = manager()->SetDeviceName(kFakeEmptyDeviceName);
  EXPECT_EQ(error,
            nearby_share::mojom::DeviceNameValidationResult::kErrorEmpty);
  EXPECT_EQ(expected_default_device_name, manager()->GetDeviceName());
  EXPECT_TRUE(notifications().empty());

  error = manager()->SetDeviceName(kFakeTooLongDeviceName);
  EXPECT_EQ(error,
            nearby_share::mojom::DeviceNameValidationResult::kErrorTooLong);
  EXPECT_EQ(expected_default_device_name, manager()->GetDeviceName());
  EXPECT_TRUE(notifications().empty());

  error = manager()->SetDeviceName(kFakeInvalidDeviceName);
  EXPECT_EQ(
      error,
      nearby_share::mojom::DeviceNameValidationResult::kErrorNotValidUtf8);
  EXPECT_EQ(expected_default_device_name, manager()->GetDeviceName());
  EXPECT_TRUE(notifications().empty());

  error = manager()->SetDeviceName(kFakeDeviceName);
  EXPECT_EQ(error, nearby_share::mojom::DeviceNameValidationResult::kValid);
  EXPECT_EQ(kFakeDeviceName, manager()->GetDeviceName());
  EXPECT_EQ(1u, notifications().size());
  EXPECT_EQ(ObserverNotification(/*did_device_name_change=*/true,
                                 /*did_full_name_change=*/false,
                                 /*did_icon_change=*/false),
            notifications().back());

  // The data is persisted.
  DestroyManager();
  CreateManager();
  EXPECT_EQ(kFakeDeviceName, manager()->GetDeviceName());
}

TEST_F(NearbyShareLocalDeviceDataManagerImplTest, DownloadDeviceData_Success) {
  CreateManager();
  EXPECT_FALSE(manager()->GetFullName());
  EXPECT_FALSE(manager()->GetIconUrl());
  EXPECT_TRUE(notifications().empty());

  DownloadDeviceData(
      CreateResponse(kFakeFullName, kFakeIconUrl, kFakeIconToken));
  EXPECT_EQ(kFakeFullName, manager()->GetFullName());
  EXPECT_EQ(kFakeIconUrl, manager()->GetIconUrl());
  EXPECT_EQ(1u, notifications().size());
  EXPECT_EQ(ObserverNotification(/*did_device_name_change=*/false,
                                 /*did_full_name_change=*/true,
                                 /*did_icon_change=*/true),
            notifications()[0]);

  // The data is persisted.
  DestroyManager();
  CreateManager();
  EXPECT_EQ(kFakeFullName, manager()->GetFullName());
  EXPECT_EQ(kFakeIconUrl, manager()->GetIconUrl());
}

TEST_F(NearbyShareLocalDeviceDataManagerImplTest,
       DownloadDeviceData_EmptyData) {
  CreateManager();
  EXPECT_FALSE(manager()->GetFullName());
  EXPECT_FALSE(manager()->GetIconUrl());
  EXPECT_TRUE(notifications().empty());

  // The server returns empty strings for the full name and icon URL/token.
  // GetFullName() and GetIconUrl() should return non-nullopt values even though
  // they are trivial values.
  DownloadDeviceData(CreateResponse("", "", ""));
  EXPECT_EQ("", manager()->GetFullName());
  EXPECT_EQ("", manager()->GetIconUrl());
  EXPECT_EQ(1u, notifications().size());
  EXPECT_EQ(ObserverNotification(/*did_device_name_change=*/false,
                                 /*did_full_name_change=*/true,
                                 /*did_icon_change=*/true),
            notifications()[0]);

  // Return empty strings again. Ensure that the trivial full name and icon
  // URL/token values are not considered changed and no notification is sent.
  DownloadDeviceData(CreateResponse("", "", ""));
  EXPECT_EQ("", manager()->GetFullName());
  EXPECT_EQ("", manager()->GetIconUrl());
  EXPECT_EQ(1u, notifications().size());

  // The data is persisted.
  DestroyManager();
  CreateManager();
  EXPECT_EQ("", manager()->GetFullName());
  EXPECT_EQ("", manager()->GetIconUrl());
}

TEST_F(NearbyShareLocalDeviceDataManagerImplTest,
       DownloadDeviceData_IconToken) {
  CreateManager();
  EXPECT_FALSE(manager()->GetFullName());
  EXPECT_FALSE(manager()->GetIconUrl());
  EXPECT_TRUE(notifications().empty());

  DownloadDeviceData(
      CreateResponse(kFakeFullName, kFakeIconUrl, kFakeIconToken));
  EXPECT_EQ(kFakeFullName, manager()->GetFullName());
  EXPECT_EQ(kFakeIconUrl, manager()->GetIconUrl());
  EXPECT_EQ(1u, notifications().size());
  EXPECT_EQ(ObserverNotification(/*did_device_name_change=*/false,
                                 /*did_full_name_change=*/true,
                                 /*did_icon_change=*/true),
            notifications()[0]);

  // Destroy and recreate to ensure name, URL, and token are all persisted.
  DestroyManager();
  CreateManager();

  // The icon URL changes but the token does not; no notification sent.
  DownloadDeviceData(
      CreateResponse(kFakeFullName, kFakeIconUrl2, kFakeIconToken));
  EXPECT_EQ(kFakeFullName, manager()->GetFullName());
  EXPECT_EQ(kFakeIconUrl2, manager()->GetIconUrl());
  EXPECT_EQ(1u, notifications().size());

  // The icon token changes but the URL does not; no notification sent.
  DestroyManager();
  CreateManager();
  DownloadDeviceData(
      CreateResponse(kFakeFullName, kFakeIconUrl2, kFakeIconToken2));
  EXPECT_EQ(kFakeFullName, manager()->GetFullName());
  EXPECT_EQ(kFakeIconUrl2, manager()->GetIconUrl());
  EXPECT_EQ(1u, notifications().size());

  // The icon URL and token change; notification sent.
  DestroyManager();
  CreateManager();
  DownloadDeviceData(
      CreateResponse(kFakeFullName, kFakeIconUrl, kFakeIconToken));
  EXPECT_EQ(kFakeFullName, manager()->GetFullName());
  EXPECT_EQ(kFakeIconUrl, manager()->GetIconUrl());
  EXPECT_EQ(2u, notifications().size());
  EXPECT_EQ(ObserverNotification(/*did_device_name_change=*/false,
                                 /*did_full_name_change=*/false,
                                 /*did_icon_change=*/true),
            notifications()[1]);

  // The data is persisted.
  DestroyManager();
  CreateManager();
  EXPECT_EQ(kFakeFullName, manager()->GetFullName());
  EXPECT_EQ(kFakeIconUrl, manager()->GetIconUrl());
}

TEST_F(NearbyShareLocalDeviceDataManagerImplTest, DownloadDeviceData_Failure) {
  CreateManager();
  DownloadDeviceData(/*response=*/std::nullopt);

  // No full name or icon URL set because response was null.
  EXPECT_EQ(std::nullopt, manager()->GetFullName());
  EXPECT_EQ(std::nullopt, manager()->GetIconUrl());
  EXPECT_TRUE(notifications().empty());
}

TEST_F(NearbyShareLocalDeviceDataManagerImplTest, UploadContacts_Success) {
  CreateManager();
  UploadContacts(CreateResponse(kFakeFullName, kFakeIconUrl, kFakeIconToken));
}

TEST_F(NearbyShareLocalDeviceDataManagerImplTest, UploadContacts_Failure) {
  CreateManager();
  UploadContacts(/*response=*/std::nullopt);
}

TEST_F(NearbyShareLocalDeviceDataManagerImplTest, UploadCertificates_Success) {
  CreateManager();
  UploadCertificates(
      CreateResponse(kFakeFullName, kFakeIconUrl, kFakeIconToken));
}

TEST_F(NearbyShareLocalDeviceDataManagerImplTest, UploadCertificates_Failure) {
  CreateManager();
  UploadCertificates(/*response=*/std::nullopt);
}