chromium/ios/chrome/test/app/sync_test_util.mm

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

#import "ios/chrome/test/app/sync_test_util.h"

#import <set>
#import <string>

#import "base/check.h"
#import "base/files/file_path.h"
#import "base/functional/bind.h"
#import "base/memory/ptr_util.h"
#import "base/path_service.h"
#import "base/strings/sys_string_conversions.h"
#import "base/strings/utf_string_conversions.h"
#import "base/test/ios/wait_util.h"
#import "base/time/time.h"
#import "base/uuid.h"
#import "components/autofill/core/browser/address_data_manager.h"
#import "components/autofill/core/browser/personal_data_manager.h"
#import "components/history/core/browser/history_service.h"
#import "components/keyed_service/core/service_access_type.h"
#import "components/metrics/demographics/demographic_metrics_test_utils.h"
#import "components/sync/base/data_type.h"
#import "components/sync/base/pref_names.h"
#import "components/sync/base/time.h"
#import "components/sync/engine/loopback_server/loopback_server_entity.h"
#import "components/sync/protocol/device_info_specifics.pb.h"
#import "components/sync/protocol/session_specifics.pb.h"
#import "components/sync/protocol/sync_enums.pb.h"
#import "components/sync/service/sync_service.h"
#import "components/sync/service/sync_service_impl.h"
#import "components/sync/test/entity_builder_factory.h"
#import "components/sync/test/fake_server.h"
#import "components/sync/test/fake_server_network_resources.h"
#import "components/sync/test/fake_server_nigori_helper.h"
#import "components/sync/test/fake_server_verifier.h"
#import "components/sync/test/nigori_test_utils.h"
#import "components/sync/test/sessions_hierarchy.h"
#import "components/sync_device_info/device_info.h"
#import "components/sync_device_info/device_info_sync_service.h"
#import "components/sync_device_info/device_info_util.h"
#import "components/sync_device_info/local_device_info_provider.h"
#import "components/sync_sessions/session_store.h"
#import "components/sync_sessions/session_sync_test_helper.h"
#import "ios/chrome/browser/autofill/model/personal_data_manager_factory.h"
#import "ios/chrome/browser/history/model/history_service_factory.h"
#import "ios/chrome/browser/shared/model/paths/paths.h"
#import "ios/chrome/browser/shared/model/profile/profile_ios.h"
#import "ios/chrome/browser/sync/model/device_info_sync_service_factory.h"
#import "ios/chrome/browser/sync/model/sync_service_factory.h"
#import "ios/chrome/browser/synced_sessions/model/distant_session.h"
#import "ios/chrome/browser/synced_sessions/model/distant_tab.h"
#import "ios/chrome/test/app/chrome_test_util.h"
#import "testing/gtest/include/gtest/gtest.h"

namespace {

std::unique_ptr<fake_server::FakeServer> gSyncFakeServer;

NSString* const kSyncTestErrorDomain = @"SyncTestDomain";

// Overrides the network callback of the current SyncServiceImpl with
// `create_http_post_provider_factory_cb`.
void OverrideSyncNetwork(const syncer::CreateHttpPostProviderFactory&
                             create_http_post_provider_factory_cb) {
  ChromeBrowserState* browser_state =
      chrome_test_util::GetOriginalBrowserState();
  DCHECK(browser_state);
  syncer::SyncServiceImpl* service =
      SyncServiceFactory::GetAsSyncServiceImplForBrowserStateForTesting(
          browser_state);
  service->OverrideNetworkForTest(create_http_post_provider_factory_cb);
}

// Returns a bookmark server entity based on `title` and `url`.
std::unique_ptr<syncer::LoopbackServerEntity> CreateBookmarkServerEntity(
    const std::string& title,
    const GURL& url) {
  fake_server::EntityBuilderFactory entity_builder_factory;
  fake_server::BookmarkEntityBuilder bookmark_builder =
      entity_builder_factory.NewBookmarkEntityBuilder(title);
  return bookmark_builder.BuildBookmark(url);
}

}  // namespace

namespace chrome_test_util {

bool IsFakeSyncServerSetUp() {
  return gSyncFakeServer.get();
}

void SetUpFakeSyncServer() {
  DCHECK(!gSyncFakeServer);
  base::FilePath user_data_dir;
  base::PathService::Get(ios::DIR_USER_DATA, &user_data_dir);
  gSyncFakeServer = std::make_unique<fake_server::FakeServer>(
      user_data_dir.AppendASCII("FakeServer"));
  OverrideSyncNetwork(fake_server::CreateFakeServerHttpPostProviderFactory(
      gSyncFakeServer->AsWeakPtr()));
}

void TearDownFakeSyncServer() {
  DCHECK(gSyncFakeServer);
  gSyncFakeServer.reset();
  OverrideSyncNetwork(syncer::CreateHttpPostProviderFactory());
}

void ClearFakeSyncServerData() {
  // Allow the caller to preventively clear server data.
  if (gSyncFakeServer) {
    gSyncFakeServer->ClearServerData();
  }
}

void FlushFakeSyncServerToDisk() {
  DCHECK(gSyncFakeServer);
  gSyncFakeServer->FlushToDisk();
}

void TriggerSyncCycle(syncer::DataType type) {
  ChromeBrowserState* browser_state =
      chrome_test_util::GetOriginalBrowserState();
  syncer::SyncService* sync_service =
      SyncServiceFactory::GetForBrowserState(browser_state);
  sync_service->TriggerRefresh({type});
}

int GetNumberOfSyncEntities(syncer::DataType type) {
  base::Value::Dict entities = gSyncFakeServer->GetEntitiesAsDictForTesting();

  base::Value::List* entity_list =
      entities.FindList(DataTypeToDebugString(type));
  DCHECK(entity_list);
  return static_cast<int>(entity_list->size());
}

BOOL VerifyNumberOfSyncEntitiesWithName(syncer::DataType type,
                                        std::string name,
                                        size_t count,
                                        NSError** error) {
  DCHECK(gSyncFakeServer);
  fake_server::FakeServerVerifier verifier(gSyncFakeServer.get());
  testing::AssertionResult result =
      verifier.VerifyEntityCountByTypeAndName(count, type, name);
  if (result != testing::AssertionSuccess() && error != nil) {
    NSDictionary* errorInfo = @{
      NSLocalizedDescriptionKey : base::SysUTF8ToNSString(result.message())
    };
    *error = [NSError errorWithDomain:kSyncTestErrorDomain
                                 code:0
                             userInfo:errorInfo];
    return NO;
  }
  return result == testing::AssertionSuccess();
}

void AddBookmarkToFakeSyncServer(std::string url, std::string title) {
  fake_server::EntityBuilderFactory entity_builder_factory;
  fake_server::BookmarkEntityBuilder bookmark_builder =
      entity_builder_factory.NewBookmarkEntityBuilder(title);
  gSyncFakeServer->InjectEntity(bookmark_builder.BuildBookmark(GURL(url)));
}

void AddLegacyBookmarkToFakeSyncServer(std::string url,
                                       std::string title,
                                       std::string originator_client_item_id) {
  DCHECK(
      !base::Uuid::ParseCaseInsensitive(originator_client_item_id).is_valid());
  fake_server::EntityBuilderFactory entity_builder_factory;
  fake_server::BookmarkEntityBuilder bookmark_builder =
      entity_builder_factory.NewBookmarkEntityBuilder(
          title, std::move(originator_client_item_id));
  gSyncFakeServer->InjectEntity(
      bookmark_builder
          .SetGeneration(fake_server::BookmarkEntityBuilder::
                             BookmarkGeneration::kWithoutTitleInSpecifics)
          .BuildBookmark(GURL(url)));
}

void AddSessionToFakeSyncServer(
    const synced_sessions::DistantSession& session) {
  std::vector<sync_pb::SessionSpecifics> specifics_list;
  SessionID window_id = SessionID::NewUnique();
  // Tab specifics.
  std::vector<SessionID> tab_list;
  sync_sessions::SessionSyncTestHelper helper;
  for (const std::unique_ptr<synced_sessions::DistantTab>& distant_tab :
       session.tabs) {
    sync_pb::SessionSpecifics tab = helper.BuildTabSpecifics(
        session.tag, base::UTF16ToUTF8(distant_tab->title),
        distant_tab->virtual_url.spec(), window_id, distant_tab->tab_id);
    tab.mutable_tab()->set_last_active_time_unix_epoch_millis(
        (distant_tab->last_active_time - base::Time::UnixEpoch())
            .InMilliseconds());
    specifics_list.push_back(tab);
    tab_list.push_back(distant_tab->tab_id);
  }
  // Header specifics.
  sync_pb::SessionSpecifics header =
      sync_sessions::SessionSyncTestHelper::BuildHeaderSpecificsWithoutWindows(
          session.tag, session.name, session.form_factor);
  sync_sessions::SessionSyncTestHelper::AddWindowSpecifics(window_id, tab_list,
                                                           &header);
  specifics_list.push_back(header);
  // Add entities to fake server.
  for (const sync_pb::SessionSpecifics& specifics : specifics_list) {
    sync_pb::EntitySpecifics entity;
    *entity.mutable_session() = specifics;
    gSyncFakeServer->InjectEntity(
        syncer::PersistentUniqueClientEntity::CreateFromSpecificsForTesting(
            /*non_unique_name=*/"",
            sync_sessions::SessionStore::GetClientTag(entity.session()), entity,
            /*creation_time=*/syncer::TimeToProtoTime(session.modified_time),
            /*last_modified_time=*/
            syncer::TimeToProtoTime(session.modified_time)));
  }
}

bool IsSyncEngineInitialized() {
  ChromeBrowserState* browser_state =
      chrome_test_util::GetOriginalBrowserState();
  DCHECK(browser_state);
  syncer::SyncService* syncService =
      SyncServiceFactory::GetForBrowserState(browser_state);
  return syncService->IsEngineInitialized();
}

std::string GetSyncCacheGuid() {
  DCHECK(IsSyncEngineInitialized());
  ChromeBrowserState* browser_state =
      chrome_test_util::GetOriginalBrowserState();
  syncer::DeviceInfoSyncService* service =
      DeviceInfoSyncServiceFactory::GetForBrowserState(browser_state);
  const syncer::LocalDeviceInfoProvider* info_provider =
      service->GetLocalDeviceInfoProvider();
  return info_provider->GetLocalDeviceInfo()->guid();
}

bool VerifySyncInvalidationFieldsPopulated() {
  DCHECK(IsFakeSyncServerSetUp());
  const std::string cache_guid = GetSyncCacheGuid();
  std::vector<sync_pb::SyncEntity> entities =
      gSyncFakeServer->GetSyncEntitiesByDataType(syncer::DEVICE_INFO);
  for (const sync_pb::SyncEntity& entity : entities) {
    if (entity.specifics().device_info().cache_guid() == cache_guid) {
      const sync_pb::InvalidationSpecificFields& invalidation_fields =
          entity.specifics().device_info().invalidation_fields();
      // TODO(crbug.com/40754340): check if `instance_id_token` is present once
      // fixed.
      return !invalidation_fields.interested_data_type_ids().empty();
    }
  }
  // The local DeviceInfo hasn't been committed yet.
  return false;
}

void AddUserDemographicsToSyncServer(
    int birth_year,
    metrics::UserDemographicsProto::Gender gender) {
  metrics::test::AddUserBirthYearAndGenderToSyncServer(
      gSyncFakeServer->AsWeakPtr(), birth_year, gender);
}

void AddAutofillProfileToFakeSyncServer(std::string guid,
                                        std::string full_name) {
  DCHECK(IsFakeSyncServerSetUp());

  sync_pb::EntitySpecifics entity_specifics;
  sync_pb::AutofillProfileSpecifics* autofill_profile =
      entity_specifics.mutable_autofill_profile();
  autofill_profile->add_name_full(full_name);
  autofill_profile->set_guid(guid);

  std::unique_ptr<syncer::LoopbackServerEntity> entity =
      syncer::PersistentUniqueClientEntity::CreateFromSpecificsForTesting(
          /*non_unique_name=*/guid, /*client_tag=*/guid, entity_specifics,
          12345, 12345);
  gSyncFakeServer->InjectEntity(std::move(entity));
}

void DeleteAutofillProfileFromFakeSyncServer(std::string guid) {
  DCHECK(IsFakeSyncServerSetUp());

  std::vector<sync_pb::SyncEntity> autofill_profiles =
      gSyncFakeServer->GetSyncEntitiesByDataType(syncer::AUTOFILL_PROFILE);
  std::string entity_id;
  std::string client_tag_hash;
  for (const sync_pb::SyncEntity& autofill_profile : autofill_profiles) {
    if (autofill_profile.specifics().autofill_profile().guid() == guid) {
      entity_id = autofill_profile.id_string();
      client_tag_hash = autofill_profile.client_tag_hash();
      break;
    }
  }
  // Delete the entity if it exists.
  if (!entity_id.empty()) {
    std::unique_ptr<syncer::LoopbackServerEntity> entity;
    entity = syncer::PersistentTombstoneEntity::CreateNew(entity_id,
                                                          client_tag_hash);
    gSyncFakeServer->InjectEntity(std::move(entity));
  }
}

bool IsAutofillProfilePresent(std::string guid, std::string full_name) {
  ChromeBrowserState* browser_state =
      chrome_test_util::GetOriginalBrowserState();
  autofill::PersonalDataManager* personal_data_manager =
      autofill::PersonalDataManagerFactory::GetForBrowserState(browser_state);
  const autofill::AutofillProfile* autofill_profile =
      personal_data_manager->address_data_manager().GetProfileByGUID(guid);

  if (autofill_profile) {
    std::string actual_full_name =
        base::UTF16ToUTF8(autofill_profile->GetRawInfo(autofill::NAME_FULL));
    return actual_full_name == full_name;
  }
  return false;
}

void ClearAutofillProfile(std::string guid) {
  ChromeBrowserState* browser_state =
      chrome_test_util::GetOriginalBrowserState();
  autofill::PersonalDataManager* personal_data_manager =
      autofill::PersonalDataManagerFactory::GetForBrowserState(browser_state);
  personal_data_manager->RemoveByGUID(guid);
}

BOOL VerifySessionsOnSyncServer(const std::multiset<std::string>& expected_urls,
                                NSError** error) {
  DCHECK(gSyncFakeServer);
  fake_server::SessionsHierarchy expected_sessions;
  expected_sessions.AddWindow(expected_urls);
  fake_server::FakeServerVerifier verifier(gSyncFakeServer.get());
  testing::AssertionResult result = verifier.VerifySessions(expected_sessions);
  if (result != testing::AssertionSuccess() && error != nil) {
    NSDictionary* errorInfo = @{
      NSLocalizedDescriptionKey : base::SysUTF8ToNSString(result.message())
    };
    *error = [NSError errorWithDomain:kSyncTestErrorDomain
                                 code:0
                             userInfo:errorInfo];
    return NO;
  }
  return result == testing::AssertionSuccess();
}

BOOL VerifyHistoryOnSyncServer(const std::multiset<GURL>& expected_urls,
                               NSError** error) {
  DCHECK(gSyncFakeServer);
  fake_server::FakeServerVerifier verifier(gSyncFakeServer.get());
  testing::AssertionResult result = verifier.VerifyHistory(expected_urls);
  if (result != testing::AssertionSuccess() && error != nil) {
    NSDictionary* errorInfo = @{
      NSLocalizedDescriptionKey : base::SysUTF8ToNSString(result.message())
    };
    *error = [NSError errorWithDomain:kSyncTestErrorDomain
                                 code:0
                             userInfo:errorInfo];
    return NO;
  }
  return result == testing::AssertionSuccess();
}

void AddTypedURLToClient(const GURL& url, base::Time visitTimestamp) {
  ChromeBrowserState* browser_state =
      chrome_test_util::GetOriginalBrowserState();
  history::HistoryService* historyService =
      ios::HistoryServiceFactory::GetForBrowserState(
          browser_state, ServiceAccessType::EXPLICIT_ACCESS);

  historyService->AddPage(url, visitTimestamp, 0, 1, GURL(),
                          history::RedirectList(), ui::PAGE_TRANSITION_TYPED,
                          history::SOURCE_BROWSED, false);
}

void AddHistoryVisitToFakeSyncServer(const GURL& url) {
  sync_pb::EntitySpecifics entitySpecifics;
  sync_pb::HistorySpecifics* history = entitySpecifics.mutable_history();
  history->set_visit_time_windows_epoch_micros(
      base::Time::Now().ToDeltaSinceWindowsEpoch().InMicroseconds());
  history->set_originator_cache_guid("originator_cache_guid");
  history->mutable_page_transition()->set_core_transition(
      sync_pb::SyncEnums_PageTransition_LINK);
  auto* redirect_entry = history->add_redirect_entries();
  redirect_entry->set_url(url.spec());
  std::unique_ptr<syncer::LoopbackServerEntity> entity =
      syncer::PersistentUniqueClientEntity::CreateFromSpecificsForTesting(
          /*non_unique_name=*/std::string(), /*client_tag=*/
          base::NumberToString(history->visit_time_windows_epoch_micros()),
          entitySpecifics, /*creation_time=*/12345,
          /*last_modified_time=*/12345);
  gSyncFakeServer->InjectEntity(std::move(entity));
}

void AddDeviceInfoToFakeSyncServer(const std::string& device_name,
                                   base::Time last_updated_timestamp) {
  sync_pb::EntitySpecifics specifics;
  sync_pb::DeviceInfoSpecifics& device_info = *specifics.mutable_device_info();
  device_info.set_cache_guid("cache_guid_" + device_name);
  device_info.set_client_name(device_name);
  device_info.set_device_type(sync_pb::SyncEnums_DeviceType_TYPE_PHONE);
  device_info.set_sync_user_agent("UserAgent");
  device_info.set_chrome_version("1.0");
  device_info.set_signin_scoped_device_id("Id");
  int64_t mtime = syncer::TimeToProtoTime(last_updated_timestamp);
  device_info.set_last_updated_timestamp(mtime);
  device_info.mutable_feature_fields()->set_send_tab_to_self_receiving_enabled(
      true);
  device_info.mutable_feature_fields()->set_send_tab_to_self_receiving_type(
      sync_pb::
          SyncEnums_SendTabReceivingType_SEND_TAB_RECEIVING_TYPE_CHROME_OR_UNSPECIFIED);

  gSyncFakeServer->InjectEntity(
      syncer::PersistentUniqueClientEntity::CreateFromSpecificsForTesting(
          "non_unique_name",
          syncer::DeviceInfoUtil::SpecificsToTag(device_info), specifics,
          /*creation_time=*/mtime, mtime));
}

BOOL IsUrlPresentOnClient(const GURL& url,
                          BOOL expect_present,
                          NSError** error) {
  // Call the history service.
  ChromeBrowserState* browser_state =
      chrome_test_util::GetOriginalBrowserState();
  history::HistoryService* history_service =
      ios::HistoryServiceFactory::GetForBrowserState(
          browser_state, ServiceAccessType::EXPLICIT_ACCESS);

  const GURL block_safe_url(url);
  std::set<GURL> origins;
  origins.insert(block_safe_url);

  __block bool history_service_callback_called = false;
  __block int count = 0;
  history_service->GetCountsAndLastVisitForOriginsForTesting(
      origins, base::BindOnce(^(history::OriginCountAndLastVisitMap result) {
        auto iter = result.find(block_safe_url);
        if (iter != result.end())
          count = iter->second.first;
        history_service_callback_called = true;
      }));

  NSDate* deadline = [NSDate dateWithTimeIntervalSinceNow:4.0];
  while (!history_service_callback_called &&
         [[NSDate date] compare:deadline] != NSOrderedDescending) {
    base::test::ios::SpinRunLoopWithMaxDelay(base::Seconds(0.1));
  }

  NSString* error_message = nil;
  if (!history_service_callback_called) {
    error_message = @"History::GetCountsAndLastVisitForOrigins callback never "
                     "called, app will probably crash later.";
  } else if (count == 0 && expect_present) {
    error_message = @"URL isn't found in HistoryService.";
  } else if (count > 0 && !expect_present) {
    error_message = @"URL isn't supposed to be in HistoryService.";
  }

  if (error_message != nil && error != nil) {
    NSDictionary* error_info = @{NSLocalizedDescriptionKey : error_message};
    *error = [NSError errorWithDomain:kSyncTestErrorDomain
                                 code:0
                             userInfo:error_info];
    return NO;
  }
  return error_message == nil;
}

void DeleteTypedUrlFromClient(const GURL& url) {
  ChromeBrowserState* browser_state =
      chrome_test_util::GetOriginalBrowserState();
  history::HistoryService* history_service =
      ios::HistoryServiceFactory::GetForBrowserState(
          browser_state, ServiceAccessType::EXPLICIT_ACCESS);

  history_service->DeleteURLs({url});
}

namespace {
// Add a sync passphrase and returns its key_params.
syncer::KeyParamsForTesting AddSyncPassphraseInternal(
    const std::string& sync_passphrase) {
  syncer::KeyParamsForTesting key_params =
      syncer::Pbkdf2PassphraseKeyParamsForTesting(sync_passphrase);
  fake_server::SetNigoriInFakeServer(
      syncer::BuildCustomPassphraseNigoriSpecifics(key_params),
      gSyncFakeServer.get());
  return key_params;
}
}  // namespace

void AddSyncPassphrase(const std::string& sync_passphrase) {
  AddSyncPassphraseInternal(sync_passphrase);
}

void AddBookmarkWithSyncPassphrase(const std::string& sync_passphrase) {
  syncer::KeyParamsForTesting key_params =
      AddSyncPassphraseInternal(sync_passphrase);
  std::unique_ptr<syncer::LoopbackServerEntity> server_entity =
      CreateBookmarkServerEntity("PBKDF2-encrypted bookmark",
                                 GURL("http://example.com/doesnt-matter"));
  server_entity->SetSpecifics(GetEncryptedBookmarkEntitySpecifics(
      server_entity->GetSpecifics().bookmark(), key_params));
  gSyncFakeServer->InjectEntity(std::move(server_entity));
}

}  // namespace chrome_test_util