// Copyright 2015 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/ui/authentication/authentication_flow.h"
#import <memory>
#import "base/files/scoped_temp_dir.h"
#import "base/functional/bind.h"
#import "base/memory/ptr_util.h"
#import "base/test/ios/wait_util.h"
#import "base/test/metrics/histogram_tester.h"
#import "base/test/scoped_feature_list.h"
#import "base/values.h"
#import "components/policy/core/common/mock_configuration_policy_provider.h"
#import "components/policy/core/common/policy_loader_ios_constants.h"
#import "components/policy/core/common/policy_map.h"
#import "components/policy/core/common/policy_types.h"
#import "components/pref_registry/pref_registry_syncable.h"
#import "components/signin/public/base/signin_metrics.h"
#import "components/signin/public/identity_manager/tribool.h"
#import "components/sync_preferences/pref_service_mock_factory.h"
#import "components/sync_preferences/pref_service_syncable.h"
#import "ios/chrome/browser/policy/model/cloud/user_policy_constants.h"
#import "ios/chrome/browser/policy/model/enterprise_policy_test_helper.h"
#import "ios/chrome/browser/shared/model/application_context/application_context.h"
#import "ios/chrome/browser/shared/model/browser/test/test_browser.h"
#import "ios/chrome/browser/shared/model/prefs/browser_prefs.h"
#import "ios/chrome/browser/shared/model/profile/test/test_profile_ios.h"
#import "ios/chrome/browser/signin/model/authentication_service.h"
#import "ios/chrome/browser/signin/model/authentication_service_factory.h"
#import "ios/chrome/browser/signin/model/chrome_account_manager_service.h"
#import "ios/chrome/browser/signin/model/chrome_account_manager_service_factory.h"
#import "ios/chrome/browser/signin/model/fake_authentication_service_delegate.h"
#import "ios/chrome/browser/signin/model/fake_system_identity.h"
#import "ios/chrome/browser/signin/model/fake_system_identity_manager.h"
#import "ios/chrome/browser/ui/authentication/authentication_flow_performer.h"
#import "ios/chrome/test/ios_chrome_scoped_testing_local_state.h"
#import "ios/web/public/test/web_task_environment.h"
#import "testing/gtest_mac.h"
#import "testing/platform_test.h"
#import "third_party/ocmock/gtest_support.h"
#import "third_party/ocmock/ocmock_extensions.h"
#import "ui/base/l10n/l10n_util.h"
namespace {
NSString* const kFakeDMToken = @"fake_dm_token";
NSString* const kFakeClientID = @"fake_client_id";
NSString* const kFakeUserAffiliationID = @"fake_user_affiliation_id";
// Duplicated from ios/chrome/browser/ui/authentication/authentication_flow.mm,
// which is fine since the enum values should never be renumbered.
enum class SigninAccountType {
kRegular = 0,
kManaged = 1,
};
class AuthenticationFlowTest : public PlatformTest {
protected:
void SetUp() override {
PlatformTest::SetUp();
TestChromeBrowserState::Builder builder;
builder.AddTestingFactory(
AuthenticationServiceFactory::GetInstance(),
AuthenticationServiceFactory::GetDefaultFactory());
builder.SetPrefService(CreatePrefService());
browser_state_ = std::move(builder).Build();
AuthenticationServiceFactory::CreateAndInitializeForBrowserState(
browser_state_.get(),
std::make_unique<FakeAuthenticationServiceDelegate>());
browser_ = std::make_unique<TestBrowser>(browser_state_.get());
identity1_ = [FakeSystemIdentity fakeIdentity1];
fake_system_identity_manager()->AddIdentity(identity1_);
identity2_ = [FakeSystemIdentity fakeIdentity2];
fake_system_identity_manager()->AddIdentity(identity2_);
managed_identity_ = [FakeSystemIdentity fakeManagedIdentity];
fake_system_identity_manager()->AddIdentity(managed_identity_);
sign_in_completion_ = ^(BOOL success) {
run_loop_.Quit();
signin_result_ =
success ? signin::Tribool::kTrue : signin::Tribool::kFalse;
};
}
std::unique_ptr<sync_preferences::PrefServiceSyncable> CreatePrefService() {
sync_preferences::PrefServiceMockFactory factory;
scoped_refptr<user_prefs::PrefRegistrySyncable> registry(
new user_prefs::PrefRegistrySyncable);
std::unique_ptr<sync_preferences::PrefServiceSyncable> prefs =
factory.CreateSyncable(registry.get());
RegisterBrowserStatePrefs(registry.get());
return prefs;
}
AuthenticationFlowPerformer* GetAuthenticationFlowPerformer() {
return static_cast<AuthenticationFlowPerformer*>(performer_);
}
// Creates a new AuthenticationFlow with default values for fields that are
// not directly useful.
void CreateAuthenticationFlow(PostSignInActionSet postSignInActions,
id<SystemIdentity> identity,
signin_metrics::AccessPoint accessPoint) {
view_controller_ = [OCMockObject niceMockForClass:[UIViewController class]];
authentication_flow_ =
[[AuthenticationFlow alloc] initWithBrowser:browser_.get()
identity:identity
accessPoint:accessPoint
postSignInActions:postSignInActions
presentingViewController:view_controller_];
performer_ =
[OCMockObject mockForClass:[AuthenticationFlowPerformer class]];
[authentication_flow_
setPerformerForTesting:GetAuthenticationFlowPerformer()];
}
// Checks if the AuthenticationFlow operation has completed, and whether it
// was successful.
void CheckSignInCompletion(bool expected_signed_in) {
run_loop_.Run();
const signin::Tribool expected_signin_result =
expected_signed_in ? signin::Tribool::kTrue : signin::Tribool::kFalse;
EXPECT_EQ(expected_signin_result, signin_result_);
[performer_ verify];
}
void SetSigninSuccessExpectations(id<SystemIdentity> identity,
signin_metrics::AccessPoint accessPoint,
NSString* hosted_domain) {
[[performer_ expect] signInIdentity:identity
atAccessPoint:accessPoint
withHostedDomain:hosted_domain
toBrowserState:browser_state_.get()];
}
FakeSystemIdentityManager* fake_system_identity_manager() {
return FakeSystemIdentityManager::FromSystemIdentityManager(
GetApplicationContext()->GetSystemIdentityManager());
}
web::WebTaskEnvironment task_environment_;
IOSChromeScopedTestingLocalState scoped_testing_local_state_;
AuthenticationFlow* authentication_flow_ = nullptr;
std::unique_ptr<TestChromeBrowserState> browser_state_;
std::unique_ptr<Browser> browser_;
id<SystemIdentity> identity1_ = nil;
id<SystemIdentity> identity2_ = nil;
id<SystemIdentity> managed_identity_ = nil;
OCMockObject* performer_ = nil;
signin_ui::CompletionCallback sign_in_completion_;
UIViewController* view_controller_;
// Used to verify histogram logging.
base::HistogramTester histogram_tester_;
// Used to wait for sign-in workflow to complete.
base::RunLoop run_loop_;
signin::Tribool signin_result_ = signin::Tribool::kUnknown;
};
// Tests a Sign In of a normal account on the same profile.
TEST_F(AuthenticationFlowTest, TestSignInSimple) {
base::test::ScopedFeatureList scoped_feature_list;
// Enable user policy to make sure that the authentication flow doesn't try
// a registration when the account isn't managed.
scoped_feature_list.InitAndEnableFeature(
policy::kUserPolicyForSigninOrSyncConsentLevel);
CreateAuthenticationFlow(
PostSignInActionSet({PostSignInAction::kNone}), identity1_,
signin_metrics::AccessPoint::ACCESS_POINT_START_PAGE);
[[[performer_ expect] andDo:^(NSInvocation*) {
[authentication_flow_ didFetchManagedStatus:nil];
}] fetchManagedStatus:browser_state_.get() forIdentity:identity1_];
SetSigninSuccessExpectations(
identity1_, signin_metrics::AccessPoint::ACCESS_POINT_START_PAGE, nil);
[authentication_flow_ startSignInWithCompletion:sign_in_completion_];
// completion block should not be called synchronously.
EXPECT_EQ(signin::Tribool::kUnknown, signin_result_);
CheckSignInCompletion(/*expected_signed_in=*/true);
histogram_tester_.ExpectUniqueSample("Signin.AccountType.SigninConsent",
SigninAccountType::kRegular, 1);
}
// Tests the fetch managed status failure case.
TEST_F(AuthenticationFlowTest, TestFailFetchManagedStatus) {
CreateAuthenticationFlow(
PostSignInActionSet({PostSignInAction::kNone}), identity1_,
signin_metrics::AccessPoint::ACCESS_POINT_START_PAGE);
NSError* error = [NSError errorWithDomain:@"foo" code:0 userInfo:nil];
[[[performer_ expect] andDo:^(NSInvocation*) {
[authentication_flow_ didFailFetchManagedStatus:error];
}] fetchManagedStatus:browser_state_.get()
forIdentity:identity1_];
[[[performer_ expect] andDo:^(NSInvocation* invocation) {
__unsafe_unretained ProceduralBlock completionBlock;
[invocation getArgument:&completionBlock atIndex:3];
completionBlock();
}] showAuthenticationError:[OCMArg any]
withCompletion:[OCMArg any]
viewController:view_controller_
browser:browser_.get()];
[authentication_flow_ startSignInWithCompletion:sign_in_completion_];
CheckSignInCompletion(/*expected_signed_in=*/false);
histogram_tester_.ExpectTotalCount("Signin.AccountType.SigninConsent", 0);
}
// Tests that when signed in only with a managed account and the
// needed features are enabled, the managed account confirmation dialog is
// shown.
TEST_F(AuthenticationFlowTest,
TestShowManagedConfirmationForSigninConsentLevelIfAllFeaturesEnabled) {
// Enable user policy and sign-in promos.
base::test::ScopedFeatureList scoped_feature_list(
policy::kUserPolicyForSigninAndNoSyncConsentLevel);
CreateAuthenticationFlow(
PostSignInActionSet({PostSignInAction::kNone}), managed_identity_,
signin_metrics::AccessPoint::ACCESS_POINT_SUPERVISED_USER);
[[[performer_ expect] andDo:^(NSInvocation*) {
[authentication_flow_ didFetchManagedStatus:@"foo.com"];
}] fetchManagedStatus:browser_state_.get() forIdentity:managed_identity_];
[[[performer_ expect] andDo:^(NSInvocation*) {
[authentication_flow_ didAcceptManagedConfirmation];
}] showManagedConfirmationForHostedDomain:@"foo.com"
viewController:view_controller_
browser:browser_.get()];
[[[performer_ expect] andDo:^(NSInvocation*) {
[authentication_flow_
didRegisterForUserPolicyWithDMToken:kFakeDMToken
clientID:kFakeClientID
userAffiliationIDs:@[ kFakeUserAffiliationID ]];
}] registerUserPolicy:browser_state_.get() forIdentity:managed_identity_];
[[[performer_ expect] andDo:^(NSInvocation*) {
[authentication_flow_ didFetchUserPolicyWithSuccess:YES];
}] fetchUserPolicy:browser_state_.get()
withDmToken:kFakeDMToken
clientID:kFakeClientID
userAffiliationIDs:@[ kFakeUserAffiliationID ]
identity:managed_identity_];
SetSigninSuccessExpectations(
managed_identity_,
signin_metrics::AccessPoint::ACCESS_POINT_SUPERVISED_USER, @"foo.com");
[authentication_flow_ startSignInWithCompletion:sign_in_completion_];
CheckSignInCompletion(/*expected_signed_in=*/true);
histogram_tester_.ExpectUniqueSample("Signin.AccountType.SigninConsent",
SigninAccountType::kManaged, 1);
}
// Tests that the management confirmation dialog is not shown and the user
// policies still fetched when the browser is already managed at the machine
// level. This only applies to the sign-in consent level.
TEST_F(AuthenticationFlowTest,
TestSkipManagedConfirmationWhenAlreadyManagedAtMachineLevel) {
// Enable user policy and sign-in consent only.
base::test::ScopedFeatureList scoped_feature_list(
policy::kUserPolicyForSigninAndNoSyncConsentLevel);
// Set a machine level policy.
base::ScopedTempDir state_directory;
ASSERT_TRUE(state_directory.CreateUniqueTempDir());
EnterprisePolicyTestHelper enterprise_policy_helper(
state_directory.GetPath());
policy::PolicyMap map;
map.Set("test-policy", policy::POLICY_LEVEL_MANDATORY,
policy::POLICY_SCOPE_USER, policy::POLICY_SOURCE_PLATFORM,
base::Value("hello"), nullptr);
enterprise_policy_helper.GetPolicyProvider()->UpdateChromePolicy(map);
CreateAuthenticationFlow(
PostSignInActionSet({PostSignInAction::kNone}), managed_identity_,
signin_metrics::AccessPoint::ACCESS_POINT_SUPERVISED_USER);
[[[performer_ expect] andDo:^(NSInvocation*) {
[authentication_flow_ didFetchManagedStatus:@"foo.com"];
}] fetchManagedStatus:browser_state_.get() forIdentity:managed_identity_];
// Make sure that the is no attempt to show the dialg.
[[performer_ reject] showManagedConfirmationForHostedDomain:@"foo.com"
viewController:view_controller_
browser:browser_.get()];
[[[performer_ expect] andDo:^(NSInvocation*) {
[authentication_flow_ didRegisterForUserPolicyWithDMToken:kFakeDMToken
clientID:kFakeClientID
userAffiliationIDs:@[ kFakeUserAffiliationID ]];
}] registerUserPolicy:browser_state_.get() forIdentity:managed_identity_];
[[[performer_ expect] andDo:^(NSInvocation*) {
[authentication_flow_ didFetchUserPolicyWithSuccess:YES];
}] fetchUserPolicy:browser_state_.get()
withDmToken:kFakeDMToken
clientID:kFakeClientID
userAffiliationIDs:@[ kFakeUserAffiliationID ]
identity:managed_identity_];
SetSigninSuccessExpectations(
managed_identity_,
signin_metrics::AccessPoint::ACCESS_POINT_SUPERVISED_USER, @"foo.com");
[authentication_flow_ startSignInWithCompletion:sign_in_completion_];
CheckSignInCompletion(/*expected_signed_in=*/true);
histogram_tester_.ExpectUniqueSample("Signin.AccountType.SigninConsent",
SigninAccountType::kManaged, 1);
}
} // namespace