chromium/ios/chrome/browser/profile/model/profile_manager_ios_impl_unittest.mm

// Copyright 2024 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/browser/profile/model/profile_manager_ios_impl.h"

#import "base/containers/contains.h"
#import "base/scoped_observation.h"
#import "base/test/test_file_util.h"
#import "base/threading/thread_restrictions.h"
#import "components/variations/scoped_variations_ids_provider.h"
#import "ios/chrome/browser/browser_state/model/constants.h"
#import "ios/chrome/browser/browser_state/model/ios_chrome_io_thread.h"
#import "ios/chrome/browser/optimization_guide/model/ios_chrome_prediction_model_store.h"
#import "ios/chrome/browser/policy/model/browser_policy_connector_ios.h"
#import "ios/chrome/browser/shared/model/application_context/application_context.h"
#import "ios/chrome/browser/shared/model/prefs/pref_names.h"
#import "ios/chrome/browser/shared/model/profile/profile_manager_observer_ios.h"
#import "ios/chrome/test/ios_chrome_scoped_testing_local_state.h"
#import "ios/chrome/test/testing_application_context.h"
#import "ios/web/public/test/web_task_environment.h"
#import "ios/web/public/thread/web_task_traits.h"
#import "ios/web/public/thread/web_thread.h"
#import "services/network/public/cpp/shared_url_loader_factory.h"
#import "testing/gtest/include/gtest/gtest.h"
#import "testing/platform_test.h"

namespace {

// Profile names.
const char kProfileName1[] = "Profile1";
const char kProfileName2[] = "Profile2";

// A scoped ProfileManagerObserverIOS which records which events have been
// received.
class ScopedTestProfileManagerObserverIOS final
    : public ProfileManagerObserverIOS {
 public:
  explicit ScopedTestProfileManagerObserverIOS(ProfileManagerIOS& manager) {
    scoped_observation_.Observe(&manager);
  }

  ~ScopedTestProfileManagerObserverIOS() final = default;

  // Accessor for the booleans used to store which method has been called.
  bool on_profile_created_called() const { return on_profile_created_called_; }

  bool on_profile_loaded_called() const { return on_profile_loaded_called_; }

  // ProfileManagerObserverIOS implementation:
  void OnProfileManagerDestroyed(ProfileManagerIOS* manager) final {
    DCHECK(scoped_observation_.IsObservingSource(manager));
    scoped_observation_.Reset();
  }

  void OnProfileCreated(ProfileManagerIOS* manager, ProfileIOS* profile) final {
    DCHECK(scoped_observation_.IsObservingSource(manager));
    on_profile_created_called_ = true;
  }

  void OnProfileLoaded(ProfileManagerIOS* manager, ProfileIOS* profile) final {
    DCHECK(scoped_observation_.IsObservingSource(manager));
    on_profile_loaded_called_ = true;
  }

 private:
  base::ScopedObservation<ProfileManagerIOS, ProfileManagerObserverIOS>
      scoped_observation_{this};

  bool on_profile_created_called_ = false;
  bool on_profile_loaded_called_ = false;
};

// Returns a callback taking a single parameter and storing it in `output`.
// The `output` must outlive the returned callback as it is captured by copy.
template <typename T>
base::OnceCallback<void(T)> CaptureParam(T* output) {
  return base::BindOnce([](T* output, T value) { *output = value; }, output);
}

}  // namespace

class ProfileManagerIOSImplTest : public PlatformTest {
 public:
  ProfileManagerIOSImplTest()
      : profile_manager_(GetApplicationContext()->GetLocalState(),
                         base::CreateUniqueTempDirectoryScopedToTest()) {
    TestingApplicationContext* application_context =
        TestingApplicationContext::GetGlobal();

    // IOSChromeIOThread needs to be created before the IO thread is started.
    // Thus DELAY_IO_THREAD_START is set in WebTaskEnvironment's options. The
    // thread is then started after the creation of IOSChromeIOThread.
    chrome_io_ = std::make_unique<IOSChromeIOThread>(
        application_context->GetLocalState(), application_context->GetNetLog());

    // Register the objects with the TestingApplicationContext.
    application_context->SetIOSChromeIOThread(chrome_io_.get());
    application_context->SetProfileManager(&profile_manager_);

    // Initialize the prediction model store (required by some KeyedServices).
    optimization_guide::IOSChromePredictionModelStore::GetInstance()
        ->Initialize(base::CreateUniqueTempDirectoryScopedToTest());

    // Start the IO thread.
    web_task_environment_.StartIOThread();

    // Post a task to initialize the IOSChromeIOThread object on the IO thread.
    web::GetIOThreadTaskRunner({})->PostTask(
        FROM_HERE, base::BindOnce(&IOSChromeIOThread::InitOnIO,
                                  base::Unretained(chrome_io_.get())));

    // Init the BrowserPolicyConnect as this is required to create an instance
    // of ProfileIOSImpl.
    application_context->GetBrowserPolicyConnector()->Init(
        application_context->GetLocalState(),
        application_context->GetSharedURLLoaderFactory());

    // IOSChromeIOThread requires the SystemURLRequestContextGetter() to be
    // created before the object is shutdown, so force its creation here.
    std::ignore = chrome_io_->system_url_request_context_getter();
  }

  ~ProfileManagerIOSImplTest() override {
    TestingApplicationContext* application_context =
        TestingApplicationContext::GetGlobal();

    // Cleanup the prediction model store (since it is a singleton).
    optimization_guide::IOSChromePredictionModelStore::GetInstance()
        ->ResetForTesting();

    application_context->GetBrowserPolicyConnector()->Shutdown();
    application_context->GetIOSChromeIOThread()->NetworkTearDown();
    application_context->SetProfileManager(nullptr);
    application_context->SetIOSChromeIOThread(nullptr);
  }

  ProfileManagerIOSImpl& profile_manager() { return profile_manager_; }

  // Returns the name of the loaded Profiles.
  std::set<std::string> GetLoadedProfileNames() {
    std::set<std::string> profile_names;
    for (ProfileIOS* profile : profile_manager_.GetLoadedProfiles()) {
      CHECK(profile);

      const std::string& profile_name = profile->GetProfileName();

      CHECK(!base::Contains(profile_names, profile_name));
      profile_names.insert(profile_name);
    }
    return profile_names;
  }

 private:
  IOSChromeScopedTestingLocalState scoped_testing_local_state_;
  std::unique_ptr<IOSChromeIOThread> chrome_io_;
  web::WebTaskEnvironment web_task_environment_{
      web::WebTaskEnvironment::IOThreadType::REAL_THREAD_DELAYED};
  ProfileManagerIOSImpl profile_manager_;

  // Some KeyedService requires a VariationsIdsProvider to be installed.
  variations::ScopedVariationsIdsProvider scoped_variations_ids_provider_{
      variations::VariationsIdsProvider::Mode::kUseSignedInState};
};

// Tests that GetLoadedProfiles() returns an empty list before the Profiles are
// loaded, and then a list containing at least one Profile, and the last used
// Profile is loaded.
TEST_F(ProfileManagerIOSImplTest, LoadProfiles) {
  // There should be no Profile loaded yet.
  EXPECT_EQ(GetLoadedProfileNames(), (std::set<std::string>{}));

  // Register an observer and check that it is correctly notified that
  // a Profile is created and then fully loaded.
  ScopedTestProfileManagerObserverIOS observer(profile_manager());
  ASSERT_FALSE(observer.on_profile_created_called());
  ASSERT_FALSE(observer.on_profile_loaded_called());

  // Load the Profiles, this will implicitly add "Default" as a Profile if there
  // is no saved Profiles. Thus it should load exactly one Profile.
  profile_manager().LoadProfiles();

  // Check that the observer has been notified of the creation and load.
  ASSERT_TRUE(observer.on_profile_created_called());
  ASSERT_TRUE(observer.on_profile_loaded_called());

  // Exactly one Profile must be loaded, it must be the last used Profile with
  // name `kIOSChromeInitialBrowserState`.
  ProfileIOS* profile =
      profile_manager().GetLastUsedProfileDeprecatedDoNotUse();

  ASSERT_TRUE(profile);
  EXPECT_EQ(profile->GetProfileName(), kIOSChromeInitialBrowserState);
  EXPECT_EQ(GetLoadedProfileNames(),
            (std::set<std::string>{kIOSChromeInitialBrowserState}));
}

// Tests that LoadProfiles() always loads the "last used Profile" when
// `kLastUsedProfile` and `kLastActiveProfiles` are out of sync.
//
// See https://crbug.com/345478758 for crashes related to this.
//
// Specifically, this test case check that even if both properties are set but
// `kLastUsedProfile` is not `kIOSChromeInitialBrowserState` and not in
// `kLastActiveProfiles`, then the last used Profile is still loaded.
TEST_F(ProfileManagerIOSImplTest, LoadProfiles_IncoherentPrefs_1) {
  ASSERT_NE(kProfileName1, kIOSChromeInitialBrowserState);
  ASSERT_NE(kProfileName2, kIOSChromeInitialBrowserState);

  // There should be no Profile loaded yet.
  EXPECT_EQ(GetLoadedProfileNames(), (std::set<std::string>{}));

  PrefService* local_state = GetApplicationContext()->GetLocalState();
  local_state->SetString(prefs::kLastUsedProfile, kProfileName1);
  local_state->SetList(prefs::kLastActiveProfiles,
                       base::Value::List().Append(kProfileName2));

  profile_manager().LoadProfiles();

  // Exactly two Profile must be loaded, named `kProfileName1` and
  // `kProfileName2`, and the last used Profile is the one named `kProfile1`.
  ProfileIOS* profile =
      profile_manager().GetLastUsedProfileDeprecatedDoNotUse();

  ASSERT_TRUE(profile);
  EXPECT_EQ(profile->GetProfileName(), kProfileName1);
  EXPECT_EQ(GetLoadedProfileNames(),
            (std::set<std::string>{kProfileName1, kProfileName2}));
}

// Tests that LoadProfiles() always loads the "last used Profile" when
// `kLastUsedProfile` and `kLastActiveProfiles` are out of sync.
//
// See https://crbug.com/345478758 for crashes related to this.
//
// Specifically, this test case check that if `kLastActiveProfiles` is not set
// and `kLastUsedProfile` is not `kIOSChromeInitialBrowserState`, then the last
// used Profile is still loaded.
TEST_F(ProfileManagerIOSImplTest, LoadProfiles_IncoherentPrefs_2) {
  ASSERT_NE(kProfileName1, kIOSChromeInitialBrowserState);
  ASSERT_NE(kProfileName2, kIOSChromeInitialBrowserState);

  // There should be no Profile loaded yet.
  EXPECT_EQ(GetLoadedProfileNames(), (std::set<std::string>{}));

  PrefService* local_state = GetApplicationContext()->GetLocalState();
  local_state->SetString(prefs::kLastUsedProfile, kProfileName1);
  local_state->SetList(prefs::kLastActiveProfiles, base::Value::List());

  profile_manager().LoadProfiles();

  // Exactly one Profile must be loaded, it must be the last used Profile with
  // name `kProfileName1`.
  ProfileIOS* profile =
      profile_manager().GetLastUsedProfileDeprecatedDoNotUse();

  ASSERT_TRUE(profile);
  EXPECT_EQ(profile->GetProfileName(), kProfileName1);
  EXPECT_EQ(GetLoadedProfileNames(), (std::set<std::string>{kProfileName1}));
}

// Tests that LoadProfiles() always loads the "last used Profile" when
// `kLastUsedProfile` and `kLastActiveProfiles` are out of sync.
//
// See https://crbug.com/345478758 for crashes related to this.
//
// Specifically, this test case check that if `kLastActiveProfiles` is set but
// does not contains the value `kIOSChromeInitialBrowserState` and
// `kLastUsedProfile` is unset, then the last used Profile is still loaded.
TEST_F(ProfileManagerIOSImplTest, LoadProfiles_IncoherentPrefs_3) {
  ASSERT_NE(kProfileName1, kIOSChromeInitialBrowserState);
  ASSERT_NE(kProfileName2, kIOSChromeInitialBrowserState);

  // There should be no Profile loaded yet.
  EXPECT_EQ(GetLoadedProfileNames(), (std::set<std::string>{}));

  PrefService* local_state = GetApplicationContext()->GetLocalState();
  local_state->SetString(prefs::kLastUsedProfile, std::string());
  local_state->SetList(prefs::kLastActiveProfiles,
                       base::Value::List().Append(kProfileName2));

  profile_manager().LoadProfiles();

  // Exactly two Profile must be loaded, named `kProfileName2` and
  // `kIOSChromeInitialBrowserState`, and the last used Profile is the one named
  // `kIOSChromeInitialBrowserState`.
  ProfileIOS* profile =
      profile_manager().GetLastUsedProfileDeprecatedDoNotUse();

  ASSERT_TRUE(profile);
  EXPECT_EQ(profile->GetProfileName(), kIOSChromeInitialBrowserState);
  EXPECT_EQ(
      GetLoadedProfileNames(),
      (std::set<std::string>{kProfileName2, kIOSChromeInitialBrowserState}));
}

// Tests that LoadProfileAsync(...) correctly loads a known Profile, and that
// the load is not blocking the main thread.
TEST_F(ProfileManagerIOSImplTest, LoadProfileAsync) {
  // Pretends that a Profile named `kProfileName1` exists. Required as
  // LoadProfileAsync(...) won't create new Profiles.
  profile_manager().GetProfileAttributesStorage()->AddProfile(kProfileName1);

  base::RunLoop run_loop;
  ProfileIOS* created_profile = nullptr;
  ProfileIOS* loaded_profile = nullptr;

  // Load the Profile asynchronously while disallowing blocking on the/ current
  // sequence (to ensure that the method is really asynchronous and does not
  // block the sequence).
  {
    base::ScopedDisallowBlocking disallow_blocking;
    const bool success = profile_manager().LoadProfileAsync(
        kProfileName1,
        CaptureParam(&loaded_profile).Then(run_loop.QuitClosure()),
        CaptureParam(&created_profile));

    ASSERT_TRUE(success);
  }

  // The Profile instance should have been created but not yet fully initialized
  // (as the initialisation is asynchronous).
  EXPECT_TRUE(created_profile);
  EXPECT_FALSE(loaded_profile);

  run_loop.Run();

  // The Profile should have been successfully loaded and initialized.
  EXPECT_TRUE(created_profile);
  EXPECT_TRUE(loaded_profile);

  // The two callbacks were invoked with the same object.
  EXPECT_EQ(created_profile, loaded_profile);
}

// Tests that calls LoadProfileAsync(...) on a loaded Profile return the Profile
// immediately and still don't block the main thread.
TEST_F(ProfileManagerIOSImplTest, LoadProfileAsync_Reload) {
  // Pretends that a Profile named `kProfileName1` exists. Required as
  // LoadProfileAsync(...) won't create new Profiles.
  profile_manager().GetProfileAttributesStorage()->AddProfile(kProfileName1);

  // Load the Profile a first time.
  {
    base::RunLoop run_loop;
    ProfileIOS* created_profile = nullptr;
    ProfileIOS* loaded_profile = nullptr;

    // Load the Profile asynchronously while disallowing blocking on the current
    // sequence (to ensure that the method is really asynchronous and does not
    // block the sequence).
    {
      base::ScopedDisallowBlocking disallow_blocking;
      const bool success = profile_manager().LoadProfileAsync(
          kProfileName1,
          CaptureParam(&loaded_profile).Then(run_loop.QuitClosure()),
          CaptureParam(&created_profile));

      ASSERT_TRUE(success);
    }

    // The Profile instance should have been created but not yet fully
    // initialized (as the initialisation is asynchronous).
    EXPECT_TRUE(created_profile);
    EXPECT_FALSE(loaded_profile);

    run_loop.Run();

    // The Profile should have been successfully loaded and initialized.
    EXPECT_TRUE(created_profile);
    EXPECT_TRUE(loaded_profile);

    // The two callbacks were invoked with the same object.
    EXPECT_EQ(created_profile, loaded_profile);
  }

  // Load the Profile a second time. Since it is already loaded, the callback
  // should be called synchronously and successfully.
  {
    base::RunLoop run_loop;
    ProfileIOS* created_profile = nullptr;
    ProfileIOS* loaded_profile = nullptr;

    // Load the Profile asynchronously while disallowing blocking on the current
    // sequence (to ensure that the method is really asynchronous and does not
    // block the sequence).
    {
      base::ScopedDisallowBlocking disallow_blocking;
      const bool success = profile_manager().LoadProfileAsync(
          kProfileName1,
          CaptureParam(&loaded_profile).Then(run_loop.QuitClosure()),
          CaptureParam(&created_profile));

      ASSERT_TRUE(success);
    }

    // Since the Profile has already been loaded, both callback should be
    // invoked synchronously.
    EXPECT_TRUE(created_profile);
    EXPECT_TRUE(loaded_profile);

    // The two callbacks were invoked with the same object.
    EXPECT_EQ(created_profile, loaded_profile);

    run_loop.Run();
  }
}

// Tests that LoadProfileAsync(...) fails to load an unknown Profile.
TEST_F(ProfileManagerIOSImplTest, LoadProfileAsync_Missing) {
  // Ensures that no Profile named `kProfileName1` exists. This will cause
  // LoadProfileAsync(...) to fail since it does not create new Profiles.
  ASSERT_FALSE(
      profile_manager().GetProfileAttributesStorage()->HasProfileWithName(
          kProfileName1));

  base::RunLoop run_loop;
  ProfileIOS* created_profile = nullptr;
  ProfileIOS* loaded_profile = nullptr;

  // Load the Profile asynchronously while disallowing blocking on the current
  // sequence (to ensure that the method is really asynchronous and does not
  // block the sequence).
  {
    base::ScopedDisallowBlocking disallow_blocking;
    const bool success = profile_manager().LoadProfileAsync(
        kProfileName1,
        CaptureParam(&loaded_profile).Then(run_loop.QuitClosure()),
        CaptureParam(&created_profile));

    ASSERT_FALSE(success);
  }

  run_loop.Run();

  // The Profile was not loaded nor created.
  EXPECT_FALSE(created_profile);
  EXPECT_FALSE(loaded_profile);
}

// Tests that CreateProfileAsync(...) creates and load successfully a new
// Profile.
TEST_F(ProfileManagerIOSImplTest, CreateProfileAsync) {
  // Ensures that no Profile named `kProfileName1` exists. This will cause
  // CreateProfileAsync(...) to create a new Profile.
  ASSERT_FALSE(
      profile_manager().GetProfileAttributesStorage()->HasProfileWithName(
          kProfileName1));

  base::RunLoop run_loop;
  ProfileIOS* created_profile = nullptr;
  ProfileIOS* loaded_profile = nullptr;

  // Load the Profile asynchronously while disallowing blocking on the current
  // sequence (to ensure that the method is really asynchronous and does not
  // block the sequence).
  {
    base::ScopedDisallowBlocking disallow_blocking;
    const bool success = profile_manager().CreateProfileAsync(
        kProfileName1,
        CaptureParam(&loaded_profile).Then(run_loop.QuitClosure()),
        CaptureParam(&created_profile));

    ASSERT_TRUE(success);
  }

  // The Profile instance should have been created but not yet fully initialized
  // (as the initialisation is asynchronous).
  EXPECT_TRUE(created_profile);
  EXPECT_FALSE(loaded_profile);

  run_loop.Run();

  // The Profile should have been successfully loaded and initialized.
  EXPECT_TRUE(created_profile);
  EXPECT_TRUE(loaded_profile);

  // The two callbacks were invoked with the same object.
  EXPECT_EQ(created_profile, loaded_profile);
}

// Tests that calling CreateProfileAsync(...) a second time returns the Profile
// that has already been laoded.
TEST_F(ProfileManagerIOSImplTest, CreateProfileAsync_Reload) {
  // Ensures that no Profile named `kProfileName1` exists. This will cause
  // CreateProfileAsync(...) to create a new Profile.
  ASSERT_FALSE(
      profile_manager().GetProfileAttributesStorage()->HasProfileWithName(
          kProfileName1));

  // Load the Profile a first time.
  {
    base::RunLoop run_loop;
    ProfileIOS* created_profile = nullptr;
    ProfileIOS* loaded_profile = nullptr;

    // Load the Profile asynchronously while disallowing blocking on the current
    // sequence (to ensure that the method is really asynchronous and does not
    // block the sequence).
    {
      base::ScopedDisallowBlocking disallow_blocking;
      const bool success = profile_manager().CreateProfileAsync(
          kProfileName1,
          CaptureParam(&loaded_profile).Then(run_loop.QuitClosure()),
          CaptureParam(&created_profile));

      ASSERT_TRUE(success);
    }

    // The Profile instance should have been created but not yet fully
    // initialized (as the initialisation is asynchronous).
    EXPECT_TRUE(created_profile);
    EXPECT_FALSE(loaded_profile);

    run_loop.Run();

    // The Profile should have been successfully loaded and initialized.
    EXPECT_TRUE(created_profile);
    EXPECT_TRUE(loaded_profile);

    // The two callbacks were invoked with the same object.
    EXPECT_EQ(created_profile, loaded_profile);
  }

  // Load the Profile a second time. Since it is already loaded, the callback
  // should be called synchronously and successfully.
  {
    base::RunLoop run_loop;
    ProfileIOS* created_profile = nullptr;
    ProfileIOS* loaded_profile = nullptr;

    // Load the Profile asynchronously while disallowing blocking on the current
    // sequence (to ensure that the method is really asynchronous and does not
    // block the sequence).
    {
      base::ScopedDisallowBlocking disallow_blocking;
      const bool success = profile_manager().CreateProfileAsync(
          kProfileName1,
          CaptureParam(&loaded_profile).Then(run_loop.QuitClosure()),
          CaptureParam(&created_profile));

      ASSERT_TRUE(success);
    }

    // Since the Profile has already been loaded, both callback should be
    // invoked synchronously.
    EXPECT_TRUE(created_profile);
    EXPECT_TRUE(loaded_profile);

    // The two callbacks were invoked with the same object.
    EXPECT_EQ(created_profile, loaded_profile);

    run_loop.Run();
  }
}

// Tests that LoadProfile(...) correctly loads a known Profile in a synchronous
// fashion (i.e. blocks the main thread).
TEST_F(ProfileManagerIOSImplTest, LoadProfile) {
  // Pretends that a Profile named `kProfileName1` exists. Required as
  // LoadProfile(...) won't create new Profiles.
  profile_manager().GetProfileAttributesStorage()->AddProfile(kProfileName1);

  // Load the Profile synchronously.
  ProfileIOS* profile = profile_manager().LoadProfile(kProfileName1);

  // The Profile should have been successfully loaded and initialized.
  EXPECT_TRUE(profile);

  // Calling LoadProfile(...) a second time should return the same
  // object.
  EXPECT_EQ(profile, profile_manager().LoadProfile(kProfileName1));
}

// Tests that LoadProfile(...) fails to load an unknown Profile.
TEST_F(ProfileManagerIOSImplTest, LoadProfile_Missing) {
  // Ensures that no Profile named `kProfileName1` exists. This will cause
  // LoadProfile(...) to fail since it does not create new Profiles.
  ASSERT_FALSE(
      profile_manager().GetProfileAttributesStorage()->HasProfileWithName(
          kProfileName1));

  // Load the Profile synchronously.
  ProfileIOS* profile = profile_manager().LoadProfile(kProfileName1);

  // The Profile was not loaded nor created.
  EXPECT_FALSE(profile);
}

// Tests that CreateProfile(...) creates and load successfully a new Profile in
// a synchronous fashion (i.e. blocks the main thread).
TEST_F(ProfileManagerIOSImplTest, CreateProfile) {
  // Ensures that no Profile named `kProfileName1` exists. This will cause
  // CreateProfileAsync(...) to create a new Profile.
  ASSERT_FALSE(
      profile_manager().GetProfileAttributesStorage()->HasProfileWithName(
          kProfileName1));

  // Create the Profile synchronously.
  ProfileIOS* profile = profile_manager().CreateProfile(kProfileName1);

  // The Profile should have been successfully loaded and initialized.
  EXPECT_TRUE(profile);

  // Calling CreateProfile(...) a second time should return the same
  // object.
  EXPECT_EQ(profile, profile_manager().CreateProfile(kProfileName1));
}