blob: 1a700dedadd0b973fa3f7190b95756f10c288e67 [file] [log] [blame]
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "ios/chrome/browser/ui/authentication/chrome_signin_view_controller.h"
#include "base/mac/foundation_util.h"
#include "base/memory/ptr_util.h"
#import "base/strings/sys_string_conversions.h"
#import "base/test/ios/wait_util.h"
#include "base/test/scoped_feature_list.h"
#include "base/timer/mock_timer.h"
#include "components/consent_auditor/consent_auditor.h"
#include "components/consent_auditor/fake_consent_auditor.h"
#include "components/signin/core/browser/account_consistency_method.h"
#include "components/signin/core/browser/account_tracker_service.h"
#include "components/unified_consent/feature.h"
#include "components/unified_consent/scoped_unified_consent.h"
#include "components/version_info/version_info.h"
#include "ios/chrome/browser/application_context.h"
#include "ios/chrome/browser/browser_state/test_chrome_browser_state.h"
#include "ios/chrome/browser/signin/account_tracker_service_factory.h"
#import "ios/chrome/browser/signin/authentication_service_factory.h"
#import "ios/chrome/browser/signin/authentication_service_fake.h"
#include "ios/chrome/browser/sync/consent_auditor_factory.h"
#include "ios/chrome/browser/sync/ios_user_event_service_factory.h"
#include "ios/chrome/browser/unified_consent/unified_consent_service_factory.h"
#include "ios/chrome/grit/ios_chromium_strings.h"
#include "ios/chrome/grit/ios_strings.h"
#import "ios/public/provider/chrome/browser/signin/fake_chrome_identity.h"
#import "ios/public/provider/chrome/browser/signin/fake_chrome_identity_service.h"
#include "ios/web/public/test/test_web_thread_bundle.h"
#import "testing/platform_test.h"
#import "third_party/ocmock/OCMock/OCMock.h"
#include "third_party/ocmock/gtest_support.h"
#include "ui/base/l10n/l10n_util_mac.h"
#if !defined(__has_feature) || !__has_feature(objc_arc)
#error "This file requires ARC support."
#endif
@interface FakeChromeSigninViewControllerDelegate
: NSObject<ChromeSigninViewControllerDelegate>
@property(nonatomic) BOOL didSigninCalled;
@end
@implementation FakeChromeSigninViewControllerDelegate
@synthesize didSigninCalled = _didSigninCalled;
- (void)willStartSignIn:(ChromeSigninViewController*)controller {
}
- (void)willStartAddAccount:(ChromeSigninViewController*)controller {
}
- (void)didSkipSignIn:(ChromeSigninViewController*)controller {
}
- (void)didFailSignIn:(ChromeSigninViewController*)controller {
FAIL();
}
- (void)didSignIn:(ChromeSigninViewController*)controller {
ASSERT_FALSE(self.didSigninCalled);
self.didSigninCalled = YES;
}
- (void)didUndoSignIn:(ChromeSigninViewController*)controller
identity:(ChromeIdentity*)identity {
}
- (void)didAcceptSignIn:(ChromeSigninViewController*)controller
showAccountsSettings:(BOOL)showAccountsSettings {
}
@end
namespace {
const bool kUnifiedConsentParam[] = {
false, true,
};
static std::unique_ptr<KeyedService> CreateFakeConsentAuditor(
web::BrowserState* context) {
return std::make_unique<consent_auditor::FakeConsentAuditor>();
}
static std::unique_ptr<KeyedService> CreateFakeUnifiedConsentService(
web::BrowserState* context) {
return nullptr;
}
// These tests verify that Chrome correctly records user's consent to Chrome
// Sync, which is a GDPR requirement. None of those tests should be turned off.
// If one of those tests fails, one of the following methods should be updated
// with the added or removed strings:
// - ExpectedConsentStringIds()
// - WhiteListLocalizedStrings()
class ChromeSigninViewControllerTest
: public PlatformTest,
public ::testing::WithParamInterface<bool> {
public:
ChromeSigninViewControllerTest()
: unified_consent_enabled_(GetParam()),
scoped_unified_consent_(
unified_consent_enabled_
? unified_consent::UnifiedConsentFeatureState::kEnabled
: unified_consent::UnifiedConsentFeatureState::kDisabled) {}
protected:
void SetUp() override {
PlatformTest::SetUp();
identity_ = [FakeChromeIdentity identityWithEmail:@"foo1@gmail.com"
gaiaID:@"foo1ID"
name:@"Fake Foo 1"];
// Setup services.
TestChromeBrowserState::Builder builder;
builder.AddTestingFactory(
AuthenticationServiceFactory::GetInstance(),
base::BindRepeating(
&AuthenticationServiceFake::CreateAuthenticationService));
builder.AddTestingFactory(ConsentAuditorFactory::GetInstance(),
base::BindRepeating(&CreateFakeConsentAuditor));
builder.AddTestingFactory(
UnifiedConsentServiceFactory::GetInstance(),
base::BindRepeating(&CreateFakeUnifiedConsentService));
context_ = builder.Build();
ios::FakeChromeIdentityService* identity_service =
ios::FakeChromeIdentityService::GetInstanceFromChromeProvider();
identity_service->AddIdentity(identity_);
account_tracker_service_ =
ios::AccountTrackerServiceFactory::GetForBrowserState(context_.get());
fake_consent_auditor_ = static_cast<consent_auditor::FakeConsentAuditor*>(
ConsentAuditorFactory::GetForBrowserState(context_.get()));
// Setup view controller.
vc_ = [[ChromeSigninViewController alloc]
initWithBrowserState:context_.get()
accessPoint:signin_metrics::AccessPoint::ACCESS_POINT_SETTINGS
promoAction:signin_metrics::PromoAction::
PROMO_ACTION_WITH_DEFAULT
signInIdentity:identity_
dispatcher:nil];
vc_delegate_ = [[FakeChromeSigninViewControllerDelegate alloc] init];
vc_.delegate = vc_delegate_;
__block base::MockOneShotTimer* mock_timer_ptr = nullptr;
if (!unified_consent_enabled_) {
vc_.timerGenerator = ^std::unique_ptr<base::OneShotTimer>() {
auto mock_timer = std::make_unique<base::MockOneShotTimer>();
mock_timer_ptr = mock_timer.get();
return mock_timer;
};
}
UIScreen* screen = [UIScreen mainScreen];
UIWindow* window = [[UIWindow alloc] initWithFrame:screen.bounds];
[window makeKeyAndVisible];
[window addSubview:[vc_ view]];
if (!unified_consent_enabled_) {
ASSERT_TRUE(mock_timer_ptr);
mock_timer_ptr->Fire();
}
window_ = window;
}
// Adds in |string_set|, all the strings displayed by |view| and its subviews,
// recursively.
static void AddStringsFromView(NSMutableSet<NSString*>* string_set,
UIView* view) {
for (UIView* subview in view.subviews)
AddStringsFromView(string_set, subview);
if ([view isKindOfClass:[UIButton class]]) {
UIButton* button = static_cast<UIButton*>(view);
if (button.currentTitle)
[string_set addObject:button.currentTitle];
} else if ([view isKindOfClass:[UILabel class]]) {
UILabel* label = static_cast<UILabel*>(view);
if (label.text)
[string_set addObject:label.text];
} else {
NSString* view_name = NSStringFromClass([view class]);
// Views that don't display strings.
NSArray* other_views = @[
@"LegacyAccountControlCell",
@"CollectionViewFooterCell",
@"IdentityPickerView",
@"IdentityView",
@"UIButtonLabel",
@"MDCActivityIndicator",
@"MDCButtonBar",
@"MDCFlexibleHeaderView",
@"MDCHeaderStackView",
@"MDCInkView",
@"MDCNavigationBar",
@"UICollectionView",
@"UICollectionViewControllerWrapperView",
@"UIImageView",
@"UIScrollView",
@"UIView",
];
// If this test fails, the unknown class should be added in other_views if
// it doesn't display any strings, otherwise the strings diplay by this
// class should be added in string_set.
EXPECT_TRUE([other_views containsObject:view_name])
<< base::SysNSStringToUTF8(view_name);
}
}
// Returns the set of strings displayed on the screen based on the views
// displayed by the .
NSSet<NSString*>* LocalizedStringOnScreen() const {
NSMutableSet* string_set = [NSMutableSet set];
AddStringsFromView(string_set, vc_.view);
return string_set;
}
// Returns a localized string based on the string id.
NSString* LocalizedStringFromID(int string_id) const {
NSString* string = l10n_util::GetNSString(string_id);
string = [string stringByReplacingOccurrencesOfString:@"BEGIN_LINK"
withString:@""];
string = [string stringByReplacingOccurrencesOfString:@"END_LINK"
withString:@""];
return string;
}
// Returns all the strings on screen that should be part of the user consent
// and part of the white list strings.
NSSet<NSString*>* LocalizedExpectedStringsOnScreen() const {
const std::vector<int>& string_ids = ExpectedConsentStringIds();
NSMutableSet<NSString*>* set = [NSMutableSet set];
for (auto it = string_ids.begin(); it != string_ids.end(); ++it) {
[set addObject:LocalizedStringFromID(*it)];
}
[set unionSet:WhiteListLocalizedStrings()];
return set;
}
// Returns the list of string id that should be given to RecordGaiaConsent()
// then the consent is given. The list is ordered according to the position
// on the screen.
const std::vector<int> ExpectedConsentStringIds() const {
if (unified_consent_enabled_) {
return {
IDS_IOS_ACCOUNT_UNIFIED_CONSENT_TITLE,
IDS_IOS_ACCOUNT_UNIFIED_CONSENT_SYNC_DATA,
IDS_IOS_ACCOUNT_UNIFIED_CONSENT_PERSONALIZED,
IDS_IOS_ACCOUNT_UNIFIED_CONSENT_BETTER_BROWSER,
IDS_IOS_ACCOUNT_UNIFIED_CONSENT_SETTINGS,
};
}
return {
IDS_IOS_ACCOUNT_CONSISTENCY_CONFIRMATION_SYNC_TITLE,
IDS_IOS_ACCOUNT_CONSISTENCY_CONFIRMATION_SYNC_DESCRIPTION,
IDS_IOS_ACCOUNT_CONSISTENCY_CONFIRMATION_SERVICES_TITLE,
IDS_IOS_ACCOUNT_CONSISTENCY_CONFIRMATION_SERVICES_DESCRIPTION,
IDS_IOS_ACCOUNT_CONSISTENCY_CONFIRMATION_OPEN_SETTINGS,
};
}
// Returns the white list of strings that can be displayed on screen but
// should not be part of ExpectedConsentStringIds().
NSSet<NSString*>* WhiteListLocalizedStrings() const {
if (unified_consent_enabled_) {
return [NSSet setWithObjects:@"Fake Foo 1", @"foo1@gmail.com", @"CANCEL",
@"YES, I'M IN", nil];
}
return [NSSet setWithObjects:@"Hi, Fake Foo 1", @"foo1@gmail.com",
@"OK, GOT IT", @"UNDO", nil];
}
int ConfirmationStringId() const {
if (unified_consent_enabled_) {
return IDS_IOS_ACCOUNT_UNIFIED_CONSENT_OK_BUTTON;
}
return IDS_IOS_ACCOUNT_CONSISTENCY_CONFIRMATION_OK_BUTTON;
}
int SettingsConfirmationStringId() const {
if (unified_consent_enabled_) {
return IDS_IOS_ACCOUNT_UNIFIED_CONSENT_SETTINGS;
}
return IDS_IOS_ACCOUNT_CONSISTENCY_CONFIRMATION_OPEN_SETTINGS;
}
// Returns true if the primary button is visible and its tile is equal the
// |string_id| (case insensitive).
bool IsPrimaryButtonVisibleWithTitle(int string_id) {
if (vc_.primaryButton.isHidden)
return false;
NSString* primary_title = vc_.primaryButton.currentTitle;
return [primary_title
caseInsensitiveCompare:l10n_util::GetNSString(string_id)] ==
NSOrderedSame;
}
// Returns |view| if it is kind of UIScrollView or returns the UIScrollView-
// kind in the subviews (recursive search). At most one UIScrollView is
// expected.
UIScrollView* FindConsentScrollView(UIView* view) {
if ([view isKindOfClass:[UIScrollView class]])
return base::mac::ObjCCastStrict<UIScrollView>(view);
UIScrollView* found_scroll_view = nil;
for (UIView* subview in view.subviews) {
UIScrollView* scroll_view_from_subview = FindConsentScrollView(subview);
if (scroll_view_from_subview) {
EXPECT_EQ(nil, found_scroll_view);
found_scroll_view = scroll_view_from_subview;
}
}
return found_scroll_view;
}
// Scrolls to the bottom if needed and returns once the primary button is
// found with the confirmation title (based on ConfirmationStringId()).
// The scroll is done without animation. Otherwise, the scroll view doesn't
// scroll correctly inside base::test::ios::WaitUntilConditionOrTimeout().
void ScrollConsentViewToBottom() {
ConditionBlock condition = ^bool() {
if (IsPrimaryButtonVisibleWithTitle(
IDS_IOS_ACCOUNT_CONSISTENCY_CONFIRMATION_SCROLL_BUTTON)) {
UIScrollView* consent_scroll_view = FindConsentScrollView(vc_.view);
CGPoint bottom_offset =
CGPointMake(0, consent_scroll_view.contentSize.height -
consent_scroll_view.bounds.size.height +
consent_scroll_view.contentInset.bottom);
[consent_scroll_view setContentOffset:bottom_offset animated:NO];
}
return IsPrimaryButtonVisibleWithTitle(ConfirmationStringId());
};
bool condition_met =
base::test::ios::WaitUntilConditionOrTimeout(10, condition);
EXPECT_TRUE(condition_met);
}
// Waits until all expected strings are on the screen.
void WaitAndExpectAllStringsOnScreen() {
__block NSSet<NSString*>* not_found_strings = nil;
__block NSSet<NSString*>* not_expected_strings = nil;
// Make sure the consent view is scrolled to the button to show the
// confirmation button (instead of the "more" button).
ScrollConsentViewToBottom();
ConditionBlock condition = ^bool() {
NSSet<NSString*>* found_strings = LocalizedStringOnScreen();
NSSet<NSString*>* expected_strings = LocalizedExpectedStringsOnScreen();
not_found_strings = [expected_strings
objectsPassingTest:^BOOL(NSString* string, BOOL* stop) {
return ![found_strings containsObject:string];
}];
not_expected_strings = [found_strings
objectsPassingTest:^BOOL(NSString* string, BOOL* stop) {
return ![expected_strings containsObject:string];
}];
return [found_strings isEqual:expected_strings];
};
bool condition_met =
base::test::ios::WaitUntilConditionOrTimeout(10, condition);
NSString* failureExplaination = [NSString
stringWithFormat:@"Strings not found: %@, Strings not expected: %@",
not_found_strings, not_expected_strings];
EXPECT_TRUE(condition_met) << base::SysNSStringToUTF8(failureExplaination);
}
bool unified_consent_enabled_;
unified_consent::ScopedUnifiedConsent scoped_unified_consent_;
web::TestWebThreadBundle thread_bundle_;
std::unique_ptr<TestChromeBrowserState> context_;
FakeChromeIdentity* identity_;
UIWindow* window_;
ChromeSigninViewController* vc_;
consent_auditor::FakeConsentAuditor* fake_consent_auditor_;
AccountTrackerService* account_tracker_service_;
base::MockOneShotTimer* mock_timer_ptr_ = nullptr;
FakeChromeSigninViewControllerDelegate* vc_delegate_;
};
INSTANTIATE_TEST_CASE_P(,
ChromeSigninViewControllerTest,
::testing::ValuesIn(kUnifiedConsentParam));
// Tests that all strings on the screen are either part of the consent string
// list defined in FakeConsentAuditor::ExpectedConsentStringIds()), or are part
// of the white list strings defined in
// FakeConsentAuditor::WhiteListLocalizedStrings().
TEST_P(ChromeSigninViewControllerTest, TestAllStrings) {
WaitAndExpectAllStringsOnScreen();
}
// Tests when the user taps on "OK GOT IT", that RecordGaiaConsent() is called
// with the expected list of string ids, and confirmation string id.
TEST_P(ChromeSigninViewControllerTest, TestConsentWithOKGOTIT) {
WaitAndExpectAllStringsOnScreen();
[vc_.primaryButton sendActionsForControlEvents:UIControlEventTouchUpInside];
ConditionBlock condition = ^bool() {
return this->vc_delegate_.didSigninCalled;
};
EXPECT_TRUE(base::test::ios::WaitUntilConditionOrTimeout(10, condition));
const std::vector<int>& recorded_ids =
fake_consent_auditor_->recorded_id_vectors().at(0);
EXPECT_EQ(ExpectedConsentStringIds(), recorded_ids);
EXPECT_EQ(ConfirmationStringId(),
fake_consent_auditor_->recorded_confirmation_ids().at(0));
EXPECT_EQ(consent_auditor::ConsentStatus::GIVEN,
fake_consent_auditor_->recorded_statuses().at(0));
EXPECT_EQ(consent_auditor::Feature::CHROME_SYNC,
fake_consent_auditor_->recorded_features().at(0));
EXPECT_EQ(account_tracker_service_->PickAccountIdForAccount(
base::SysNSStringToUTF8([identity_ gaiaID]),
base::SysNSStringToUTF8([identity_ userEmail])),
fake_consent_auditor_->account_id());
}
// Tests that RecordGaiaConsent() is not called when the user taps on UNDO.
TEST_P(ChromeSigninViewControllerTest, TestRefusingConsent) {
WaitAndExpectAllStringsOnScreen();
[vc_.secondaryButton sendActionsForControlEvents:UIControlEventTouchUpInside];
EXPECT_EQ(0ul, fake_consent_auditor_->recorded_id_vectors().size());
EXPECT_EQ(0ul, fake_consent_auditor_->recorded_confirmation_ids().size());
}
// Tests that RecordGaiaConsent() is called with the expected list of string
// ids, and settings confirmation string id.
TEST_P(ChromeSigninViewControllerTest, TestConsentWithSettings) {
WaitAndExpectAllStringsOnScreen();
[vc_ signinConfirmationControllerDidTapSettingsLink:vc_.confirmationVC];
const std::vector<int>& recorded_ids =
fake_consent_auditor_->recorded_id_vectors().at(0);
EXPECT_EQ(ExpectedConsentStringIds(), recorded_ids);
EXPECT_EQ(SettingsConfirmationStringId(),
fake_consent_auditor_->recorded_confirmation_ids().at(0));
EXPECT_EQ(consent_auditor::ConsentStatus::GIVEN,
fake_consent_auditor_->recorded_statuses().at(0));
EXPECT_EQ(consent_auditor::Feature::CHROME_SYNC,
fake_consent_auditor_->recorded_features().at(0));
EXPECT_EQ(account_tracker_service_->PickAccountIdForAccount(
base::SysNSStringToUTF8([identity_ gaiaID]),
base::SysNSStringToUTF8([identity_ userEmail])),
fake_consent_auditor_->account_id());
}
} // namespace