| // 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::kEnabledNoBump |
| : 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 = @[ |
| @"AccountControlCell", |
| @"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 |