// Copyright 2023 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/ash/scalable_iph/scalable_iph_browser_test_base.h"
#include <memory>
#include "ash/constants/ash_features.h"
#include "base/containers/fixed_flat_set.h"
#include "base/functional/bind.h"
#include "base/memory/raw_ptr.h"
#include "base/strings/strcat.h"
#include "chrome/browser/ash/scalable_iph/customizable_test_env_browser_test_base.h"
#include "chrome/browser/ash/scalable_iph/mock_scalable_iph_delegate.h"
#include "chrome/browser/ash/scalable_iph/scalable_iph_delegate_impl.h"
#include "chrome/browser/feature_engagement/tracker_factory.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/profiles/profile_manager.h"
#include "chrome/browser/scalable_iph/scalable_iph_factory.h"
#include "chrome/browser/scalable_iph/scalable_iph_factory_impl.h"
#include "chrome/browser/signin/identity_manager_factory.h"
#include "chrome/browser/ui/ash/multi_user/multi_user_window_manager_helper.h"
#include "chrome/browser/ui/browser.h"
#include "chromeos/ash/components/browser_context_helper/browser_context_helper.h"
#include "chromeos/ash/components/scalable_iph/scalable_iph.h"
#include "chromeos/ash/components/scalable_iph/scalable_iph_constants.h"
#include "chromeos/ash/components/scalable_iph/scalable_iph_delegate.h"
#include "chromeos/ash/services/network_config/in_process_instance.h"
#include "chromeos/ash/services/network_config/public/cpp/cros_network_config_test_helper.h"
#include "chromeos/services/network_config/public/mojom/cros_network_config.mojom.h"
#include "components/keyed_service/content/browser_context_dependency_manager.h"
#include "components/keyed_service/core/keyed_service.h"
#include "components/signin/public/identity_manager/account_capabilities_test_mutator.h"
#include "components/signin/public/identity_manager/account_info.h"
#include "components/signin/public/identity_manager/identity_manager.h"
#include "components/signin/public/identity_manager/identity_test_utils.h"
#include "components/user_manager/user.h"
#include "components/user_manager/user_manager.h"
#include "content/public/browser/browser_context.h"
#include "testing/gmock/include/gmock/gmock.h"
namespace ash {
namespace {
using ::chromeos::network_config::mojom::ConnectionStateType;
using ::chromeos::network_config::mojom::NetworkType;
using Observer = ::scalable_iph::ScalableIphDelegate::Observer;
std::set<std::string> mock_delegate_created_;
constexpr char kTestWiFiId[] = "test-wifi-id";
constexpr auto kEligileUserSessionTypesForMantaService = base::MakeFixedFlatSet<
CustomizableTestEnvBrowserTestBase::UserSessionType>(
{CustomizableTestEnvBrowserTestBase::UserSessionType::kRegular,
CustomizableTestEnvBrowserTestBase::UserSessionType::kRegularNonOwner,
CustomizableTestEnvBrowserTestBase::UserSessionType::kManaged,
CustomizableTestEnvBrowserTestBase::UserSessionType::kRegularWithOobe});
BASE_FEATURE(kScalableIphTest,
"ScalableIphTest",
base::FEATURE_DISABLED_BY_DEFAULT);
} // namespace
ScalableIphBrowserTestBase::ScalableIphBrowserTestBase() {
scalable_iph::ScalableIph::ForceEnableIphFeatureForTesting();
}
ScalableIphBrowserTestBase::~ScalableIphBrowserTestBase() = default;
void ScalableIphBrowserTestBase::SetUp() {
InitializeScopedFeatureList();
network_config::OverrideInProcessInstanceForTesting(
&fake_cros_network_config_);
// Keyed service is a service which is tied to an object. For our use cases,
// the object is `BrowserContext` (e.g. `Profile`). See
// //components/keyed_service/README.md for details on keyed service.
//
// We set a testing factory to inject a mock. A testing factory must be set
// early enough as a service is not created before that, e.g. a `Tracker` must
// not be created before we set `CreateMockTracker`. If a keyed service is
// created before we set our testing factory, `SetTestingFactory` will
// destruct already created keyed services at a time we set our testing
// factory. It destructs a keyed service at an unusual timing. It can trigger
// a dangling pointer issue, etc.
//
// `SetUpOnMainThread` below is too late to set a testing factory. Note that
// `InProcessBrowserTest::SetUp` is called at the very early stage, e.g.
// before command lines are set, etc.
subscription_ =
BrowserContextDependencyManager::GetInstance()
->RegisterCreateServicesCallbackForTesting(base::BindRepeating(
&ScalableIphBrowserTestBase::SetTestingFactories,
enable_mock_tracker_));
CustomizableTestEnvBrowserTestBase::SetUp();
}
// `SetUpOnMainThread` is called just before a test body. Do the mock set up in
// this function as `browser()` is not available in `SetUp` above.
void ScalableIphBrowserTestBase::SetUpOnMainThread() {
if (kEligileUserSessionTypesForMantaService.contains(
test_environment().user_session_type()) &&
!force_disable_manta_service_) {
ScalableIphFactory::GetInstance()
->SetOnBuildingServiceInstanceForTestingCallback(base::BindRepeating(
&ScalableIphBrowserTestBase::SetCanUseMantaService));
}
// `CustomizableTestEnvBrowserTestBase::SetUpOnMainThread` must be called
// before our `SetUpOnMainThread` as login happens in the method, i.e. profile
// is not available before it.
CustomizableTestEnvBrowserTestBase::SetUpOnMainThread();
// If user session type is `kRegularWithOobe`, Chrome enters post login OOBE
// screens after a login. It means that there won't be `ScalableIph` as
// `ScalableIph` starts after post login OOBE screens. We have to wait the
// initialization of `ScalableIph` before setting up mocks.
if (test_environment().user_session_type() ==
CustomizableTestEnvBrowserTestBase::UserSessionType::kRegularWithOobe) {
return;
}
if (enable_multi_user_) {
// Add a secondary user.
LoginManagerMixin* login_manager_mixin = GetLoginManagerMixin();
CHECK(login_manager_mixin);
login_manager_mixin->AppendRegularUsers(1);
CHECK_EQ(login_manager_mixin->users().size(), 2ul);
// By default, `MultiUserWindowManager` is created with multi profile off.
// Re-create for multi profile tests. This has to be done after
// `SetUpOnMainThread` of a base class as the original multi-profile-off
// `MultiUserWindowManager` is created there.
MultiUserWindowManagerHelper::CreateInstanceForTest(
GetPrimaryUserContext().GetAccountId());
}
// If we don't intend to enforce ScalableIph setup (i.e. the user profile
// doesn't qualify for ScalableIph), do not set up mocks as ScalableIph
// should not be available for the profile.
if (!setup_scalable_iph_) {
return;
}
CHECK(enable_scalable_iph_)
<< "ScalableIph feature flag must be intended to be enabled to set up "
"fakes and mocks of ScalableIph";
SetUpMocks();
}
void ScalableIphBrowserTestBase::TearDownOnMainThread() {
// We are going to release references to mock objects below. Verify the
// expectations in advance to have a predictable behavior.
testing::Mock::VerifyAndClearExpectations(mock_tracker_);
mock_tracker_ = nullptr;
testing::Mock::VerifyAndClearExpectations(mock_delegate_);
mock_delegate_ = nullptr;
InProcessBrowserTest::TearDownOnMainThread();
}
void ScalableIphBrowserTestBase::SetUpMocks() {
CHECK(!mock_delegate_) << "Mocks have already been set up.";
// Do not access profile via `browser()` as a browser might not be created if
// session type is WithOobe.
Profile* profile = ProfileManager::GetActiveUserProfile();
CHECK(profile);
CHECK(IsMockDelegateCreatedFor(profile))
<< "ScalableIph service has a timer inside. The service must be created "
"at a login time. We check the behavior by confirming creation of a "
"delegate.";
if (enable_mock_tracker_) {
mock_tracker_ = static_cast<feature_engagement::test::MockTracker*>(
feature_engagement::TrackerFactory::GetForBrowserContext(profile));
CHECK(mock_tracker_)
<< "mock_tracker_ must be non-nullptr. GetForBrowserContext should "
"create one via CreateMockTracker if it does not exist.";
ON_CALL(*mock_tracker_, AddOnInitializedCallback)
.WillByDefault(
[](feature_engagement::Tracker::OnInitializedCallback callback) {
std::move(callback).Run(true);
});
ON_CALL(*mock_tracker_, IsInitialized).WillByDefault(testing::Return(true));
}
// The static cast is necessary to access the delegate functions declared in
// the `ScalableIphFactoryImpl` class.
ScalableIphFactoryImpl* scalable_iph_factory =
static_cast<ScalableIphFactoryImpl*>(ScalableIphFactory::GetInstance());
CHECK(scalable_iph_factory);
CHECK(scalable_iph_factory->has_delegate_factory_for_testing())
<< "This test uses MockScalableIphDelegate. A factory for testing must "
"be set.";
scalable_iph::ScalableIph* scalable_iph =
ScalableIphFactory::GetForBrowserContext(profile);
CHECK(scalable_iph);
// `ScalableIph` for the profile is initialzied in
// `CustomizableTestEnvBrowserTestBase::SetUpOnMainThread` above. We cannot
// simply use `TestMockTimeTaskRunner::ScopedContext` as `RunLoop` is used
// there and it's not supported by `ScopedContext`. We override a task runner
// after a timer has created and started.
task_runner_ = base::MakeRefCounted<base::TestMockTimeTaskRunner>();
scalable_iph->OverrideTaskRunnerForTesting(task_runner());
mock_delegate_ = static_cast<test::MockScalableIphDelegate*>(
scalable_iph->delegate_for_testing());
CHECK(mock_delegate_);
}
void ScalableIphBrowserTestBase::InitializeScopedFeatureList() {
base::FieldTrialParams params;
AppendVersionNumber(params);
AppendUiParams(params);
base::test::FeatureRefAndParams test_config(kScalableIphTest, params);
std::vector<base::test::FeatureRefAndParams> enabled_features({test_config});
std::vector<base::test::FeatureRef> disabled_features;
AppendTestSpecificFeatures(enabled_features, disabled_features);
if (enable_scalable_iph_) {
enabled_features.push_back(
base::test::FeatureRefAndParams(ash::features::kScalableIph, {}));
} else {
disabled_features.push_back(
base::test::FeatureRef(ash::features::kScalableIph));
}
if (enable_scalable_iph_debug_) {
enabled_features.push_back(
base::test::FeatureRefAndParams(ash::features::kScalableIphDebug, {}));
} else {
disabled_features.push_back(
base::test::FeatureRef(ash::features::kScalableIphDebug));
}
scoped_feature_list_.InitWithFeaturesAndParameters(enabled_features,
disabled_features);
}
void ScalableIphBrowserTestBase::AppendUiParams(
base::FieldTrialParams& params) {
AppendFakeUiParamsNotification(params, /*has_body_text=*/true,
kScalableIphTest);
}
void ScalableIphBrowserTestBase::AppendVersionNumber(
base::FieldTrialParams& params,
const base::Feature& feature,
const std::string& version_number) {
params[FullyQualified(feature,
scalable_iph::kCustomParamsVersionNumberParamName)] =
version_number;
}
void ScalableIphBrowserTestBase::AppendVersionNumber(
base::FieldTrialParams& params,
const base::Feature& feature) {
AppendVersionNumber(
params, feature,
base::NumberToString(scalable_iph::kCurrentVersionNumber));
}
void ScalableIphBrowserTestBase::AppendVersionNumber(
base::FieldTrialParams& params) {
AppendVersionNumber(params, kScalableIphTest);
}
void ScalableIphBrowserTestBase::AppendFakeUiParamsNotification(
base::FieldTrialParams& params,
bool has_body_text,
const base::Feature& feature) {
params[FullyQualified(feature, scalable_iph::kCustomUiTypeParamName)] =
scalable_iph::kCustomUiTypeValueNotification;
params[FullyQualified(feature,
scalable_iph::kCustomNotificationIdParamName)] =
kTestNotificationId;
params[FullyQualified(feature,
scalable_iph::kCustomNotificationTitleParamName)] =
kTestNotificationTitle;
if (has_body_text) {
params[FullyQualified(feature,
scalable_iph::kCustomNotificationBodyTextParamName)] =
kTestNotificationBodyText;
}
params[FullyQualified(feature,
scalable_iph::kCustomNotificationButtonTextParamName)] =
kTestNotificationButtonText;
params[FullyQualified(feature,
scalable_iph::kCustomButtonActionTypeParamName)] =
kTestButtonActionTypeOpenChrome;
params[FullyQualified(feature,
scalable_iph::kCustomButtonActionEventParamName)] =
kTestActionEventName;
}
void ScalableIphBrowserTestBase::AppendFakeUiParamsBubble(
base::FieldTrialParams& params) {
params[FullyQualified(kScalableIphTest,
scalable_iph::kCustomUiTypeParamName)] =
scalable_iph::kCustomUiTypeValueBubble;
params[FullyQualified(kScalableIphTest,
scalable_iph::kCustomBubbleIdParamName)] =
kTestBubbleId;
params[FullyQualified(kScalableIphTest,
scalable_iph::kCustomBubbleTitleParamName)] =
kTestBubbleTitle;
params[FullyQualified(kScalableIphTest,
scalable_iph::kCustomBubbleTextParamName)] =
kTestBubbleText;
params[FullyQualified(kScalableIphTest,
scalable_iph::kCustomBubbleButtonTextParamName)] =
kTestBubbleButtonText;
params[FullyQualified(kScalableIphTest,
scalable_iph::kCustomButtonActionTypeParamName)] =
kTestButtonActionTypeOpenGoogleDocs;
params[FullyQualified(kScalableIphTest,
scalable_iph::kCustomButtonActionEventParamName)] =
kTestActionEventName;
params[FullyQualified(kScalableIphTest,
scalable_iph::kCustomBubbleIconParamName)] =
kTestBubbleIconString;
}
// static
std::string ScalableIphBrowserTestBase::FullyQualified(
const base::Feature& feature,
const std::string& param_name) {
return base::StrCat({feature.name, "_", param_name});
}
bool ScalableIphBrowserTestBase::IsMockDelegateCreatedFor(Profile* profile) {
return mock_delegate_created_.contains(profile->GetProfileUserName());
}
void ScalableIphBrowserTestBase::EnableTestIphFeatures(
const std::vector<raw_ptr<const base::Feature, VectorExperimental>>
test_iph_features) {
CHECK(mock_delegate_)
<< "To enable a test iph feature, mocks have to be set up.";
const base::flat_set<const base::Feature*> test_iph_features_set(
test_iph_features.begin(), test_iph_features.end());
ON_CALL(*mock_tracker(), ShouldTriggerHelpUI)
.WillByDefault([test_iph_features_set](const base::Feature& feature) {
return test_iph_features_set.contains(&feature);
});
// Do not access profile via `browser()` as this method can be called before a
// browser is created.
Profile* profile = ProfileManager::GetActiveUserProfile();
CHECK(profile);
// `OverrideFeatureListForTesting` prohibits calling it twice and it has a
// check. We don't need to do another check for `EnableTestIphFeature` being
// called twice.
scalable_iph::ScalableIph* scalable_iph =
ScalableIphFactory::GetForBrowserContext(profile);
scalable_iph->OverrideFeatureListForTesting(test_iph_features);
}
void ScalableIphBrowserTestBase::EnableTestIphFeature() {
EnableTestIphFeatures({&kScalableIphTest});
}
const base::Feature& ScalableIphBrowserTestBase::TestIphFeature() const {
return kScalableIphTest;
}
void ScalableIphBrowserTestBase::TriggerConditionsCheckWithAFakeEvent(
scalable_iph::ScalableIph::Event event) {
// Do not access profile via `browser()` as this method can be called before a
// browser is created.
Profile* profile = ProfileManager::GetActiveUserProfile();
CHECK(profile);
scalable_iph::ScalableIph* scalable_iph =
ScalableIphFactory::GetForBrowserContext(profile);
scalable_iph->RecordEvent(event);
}
ash::UserContext ScalableIphBrowserTestBase::GetPrimaryUserContext() {
return ash::LoginManagerMixin::CreateDefaultUserContext(
GetLoginManagerMixin()->users()[0]);
}
ash::UserContext ScalableIphBrowserTestBase::GetSecondaryUserContext() {
CHECK(enable_multi_user_);
return ash::LoginManagerMixin::CreateDefaultUserContext(
GetLoginManagerMixin()->users()[1]);
}
void ScalableIphBrowserTestBase::ShutdownScalableIph() {
scalable_iph::ScalableIph* scalable_iph =
ScalableIphFactory::GetForBrowserContext(browser()->profile());
CHECK(scalable_iph) << "ScalableIph does not exist for a current profile";
// `ScalableIph::Shutdown` destructs a delegate. Release the pointer to the
// mock delegate to avoid having a dangling pointer. We can retain a pointer
// to the mock tracker as a tracker is not destructed by the
// `ScalableIph::Shutdown`.
mock_delegate_ = nullptr;
scalable_iph->Shutdown();
}
void ScalableIphBrowserTestBase::AddOnlineNetwork() {
fake_cros_network_config_.AddNetworkAndDevice(
network_config::CrosNetworkConfigTestHelper::
CreateStandaloneNetworkProperties(kTestWiFiId, NetworkType::kWiFi,
ConnectionStateType::kOnline,
/*signal_strength=*/0));
}
// static
void ScalableIphBrowserTestBase::SetTestingFactories(
bool enable_mock_tracker,
content::BrowserContext* browser_context) {
if (enable_mock_tracker) {
feature_engagement::TrackerFactory::GetInstance()->SetTestingFactory(
browser_context,
base::BindRepeating(&ScalableIphBrowserTestBase::CreateMockTracker));
}
// The static cast is necessary to access the delegate functions declared in
// the `ScalableIphFactoryImpl` class.
ScalableIphFactoryImpl* scalable_iph_factory =
static_cast<ScalableIphFactoryImpl*>(ScalableIphFactory::GetInstance());
CHECK(scalable_iph_factory);
// This method can be called more than once for a single browser context.
if (scalable_iph_factory->has_delegate_factory_for_testing()) {
return;
}
// This is NOT a testing factory of a keyed service factory .But the delegate
// factory is called from the factory of `ScalableIphFactory`. Set this at the
// same time.
scalable_iph_factory->SetDelegateFactoryForTesting(
base::BindRepeating(&ScalableIphBrowserTestBase::CreateMockDelegate));
}
// static
std::unique_ptr<KeyedService> ScalableIphBrowserTestBase::CreateMockTracker(
content::BrowserContext* browser_context) {
return std::make_unique<feature_engagement::test::MockTracker>();
}
// static
std::unique_ptr<scalable_iph::ScalableIphDelegate>
ScalableIphBrowserTestBase::CreateMockDelegate(Profile* profile,
scalable_iph::Logger* logger) {
std::pair<std::set<std::string>::iterator, bool> result =
mock_delegate_created_.insert(profile->GetProfileUserName());
CHECK(result.second) << "Delegate is created twice for a profile";
std::unique_ptr<test::MockScalableIphDelegate> delegate =
std::make_unique<test::MockScalableIphDelegate>();
delegate->SetDelegate(
std::make_unique<ScalableIphDelegateImpl>(profile, logger));
// Fake behaviors of observers must be set at an early stage as those methods
// are called from constructors, i.e. Set up phases of test fixtures.
delegate->FakeObservers();
return delegate;
}
// static
void ScalableIphBrowserTestBase::SetCanUseMantaService(Profile* profile) {
signin::IdentityManager* identity_manager =
IdentityManagerFactory::GetForProfile(profile);
CHECK(identity_manager);
const user_manager::User* user =
ash::BrowserContextHelper::Get()->GetUserByBrowserContext(profile);
CHECK(user);
AccountInfo account_info = identity_manager->FindExtendedAccountInfoByGaiaId(
user->GetAccountId().GetGaiaId());
AccountCapabilitiesTestMutator test_mutator(&account_info.capabilities);
test_mutator.set_can_use_manta_service(true);
signin::UpdateAccountInfoForAccount(identity_manager, account_info);
}
} // namespace ash