chromium/ios/chrome/browser/ui/settings/password/passwords_mediator_unittest.mm

// Copyright 2020 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/settings/password/passwords_mediator.h"

#import "base/apple/foundation_util.h"
#import "base/location.h"
#import "base/memory/raw_ptr.h"
#import "base/strings/string_util.h"
#import "base/strings/utf_string_conversions.h"
#import "base/test/bind.h"
#import "components/affiliations/core/browser/fake_affiliation_service.h"
#import "components/feature_engagement/public/event_constants.h"
#import "components/feature_engagement/test/mock_tracker.h"
#import "components/keyed_service/core/service_access_type.h"
#import "components/password_manager/core/browser/password_manager_test_utils.h"
#import "components/password_manager/core/browser/password_store/test_password_store.h"
#import "components/password_manager/core/browser/ui/credential_ui_entry.h"
#import "components/password_manager/core/common/password_manager_features.h"
#import "components/signin/public/identity_manager/objc/identity_manager_observer_bridge.h"
#import "ios/chrome/browser/affiliations/model/ios_chrome_affiliation_service_factory.h"
#import "ios/chrome/browser/favicon/model/favicon_loader.h"
#import "ios/chrome/browser/favicon/model/ios_chrome_favicon_loader_factory.h"
#import "ios/chrome/browser/feature_engagement/model/tracker_factory.h"
#import "ios/chrome/browser/passwords/model/ios_chrome_password_check_manager.h"
#import "ios/chrome/browser/passwords/model/ios_chrome_password_check_manager_factory.h"
#import "ios/chrome/browser/passwords/model/ios_chrome_profile_password_store_factory.h"
#import "ios/chrome/browser/passwords/model/password_check_observer_bridge.h"
#import "ios/chrome/browser/shared/model/browser/test/test_browser.h"
#import "ios/chrome/browser/shared/model/profile/profile_ios.h"
#import "ios/chrome/browser/shared/model/profile/test/test_profile_ios.h"
#import "ios/chrome/browser/shared/ui/table_view/legacy_chrome_table_view_controller_test.h"
#import "ios/chrome/browser/sync/model/sync_observer_bridge.h"
#import "ios/chrome/browser/sync/model/sync_service_factory.h"
#import "ios/chrome/browser/ui/settings/password/passwords_consumer.h"
#import "ios/chrome/browser/ui/settings/password/passwords_mediator+Testing.h"
#import "ios/chrome/browser/ui/settings/utils/password_auto_fill_status_observer.h"
#import "ios/web/public/test/web_task_environment.h"
#import "testing/gmock/include/gmock/gmock.h"
#import "testing/gtest/include/gtest/gtest.h"
#import "testing/gtest_mac.h"

namespace {

using password_manager::PasswordForm;
using password_manager::InsecureType;
using password_manager::TestPasswordStore;

// Creates a saved password form.
PasswordForm CreatePasswordForm() {
  PasswordForm form;
  form.username_value = u"[email protected]";
  form.password_value = u"test";
  form.signon_realm = "http://www.example.com/";
  form.in_store = PasswordForm::Store::kProfileStore;
  return form;
}

// Create the Feature Engagement Mock Tracker.
std::unique_ptr<KeyedService> BuildFeatureEngagementMockTracker(
    web::BrowserState* browser_state) {
  return std::make_unique<feature_engagement::test::MockTracker>();
}

}  // namespace

@interface FakePasswordsConsumer : NSObject <PasswordsConsumer> {
  std::vector<password_manager::CredentialUIEntry> _credentials;
  std::vector<password_manager::CredentialUIEntry> _blockedSites;
  std::vector<password_manager::AffiliatedGroup> _affiliatedGroups;
}

// Number of time the method updateOnDeviceEncryptionSessionAndUpdateTableView
// was called. Used to test that primary account change and sync change
// causes the update to occur.
@property(nonatomic, assign) NSInteger numberOfCallToChangeOnDeviceEncryption;

@property(nonatomic, copy) NSString* detailedText;

@property(nonatomic, assign) BOOL shouldShowPasswordManagerWidgetPromoCalled;

@end

@implementation FakePasswordsConsumer

- (void)setPasswordCheckUIState:(PasswordCheckUIState)state
         insecurePasswordsCount:(NSInteger)insecureCount {
}

- (void)setCredentials:
            (std::vector<password_manager::CredentialUIEntry>)credentials
          blockedSites:
              (std::vector<password_manager::CredentialUIEntry>)blockedSites {
  _credentials = credentials;
  _blockedSites = blockedSites;
}

- (void)setSavingPasswordsToAccount:(BOOL)savingPasswordsToAccount {
}

- (void)setAffiliatedGroups:
            (const std::vector<password_manager::AffiliatedGroup>&)
                affiliatedGroups
               blockedSites:
                   (const std::vector<password_manager::CredentialUIEntry>&)
                       blockedSites {
  _affiliatedGroups = affiliatedGroups;
  _blockedSites = blockedSites;
}

- (std::vector<password_manager::AffiliatedGroup>)affiliatedGroups {
  return _affiliatedGroups;
}

- (void)setShouldShowPasswordManagerWidgetPromo:
    (BOOL)shouldShowPasswordManagerWidgetPromo {
  _shouldShowPasswordManagerWidgetPromoCalled = YES;
}

@end

// Tests for Passwords mediator.
class PasswordsMediatorTest : public BlockCleanupTest {
 protected:
  void SetUp() override {
    BlockCleanupTest::SetUp();
    TestChromeBrowserState::Builder builder;
    builder.AddTestingFactory(
        IOSChromeProfilePasswordStoreFactory::GetInstance(),
        base::BindRepeating(
            &password_manager::BuildPasswordStore<web::BrowserState,
                                                  TestPasswordStore>));
    builder.AddTestingFactory(
        IOSChromeAffiliationServiceFactory::GetInstance(),
        base::BindRepeating(base::BindLambdaForTesting([](web::BrowserState*) {
          return std::unique_ptr<KeyedService>(
              std::make_unique<affiliations::FakeAffiliationService>());
        })));

    builder.AddTestingFactory(
        feature_engagement::TrackerFactory::GetInstance(),
        base::BindRepeating(&BuildFeatureEngagementMockTracker));
    browser_state_ = std::move(builder).Build();

    store_ =
        base::WrapRefCounted(static_cast<password_manager::TestPasswordStore*>(
            IOSChromeProfilePasswordStoreFactory::GetForBrowserState(
                browser_state_.get(), ServiceAccessType::EXPLICIT_ACCESS)
                .get()));
    password_check_ = IOSChromePasswordCheckManagerFactory::GetForBrowserState(
        browser_state_.get());

    consumer_ = [[FakePasswordsConsumer alloc] init];

    mediator_ = [[PasswordsMediator alloc]
        initWithPasswordCheckManager:password_check_
                       faviconLoader:IOSChromeFaviconLoaderFactory::
                                         GetForBrowserState(
                                             browser_state_.get())
                         syncService:SyncServiceFactory::GetForBrowserState(
                                         browser_state_.get())
                         prefService:browser_state_->GetPrefs()];

    mock_tracker_ = static_cast<feature_engagement::test::MockTracker*>(
        feature_engagement::TrackerFactory::GetForBrowserState(browserState()));
    mediator_.tracker = mock_tracker_;

    mediator_.consumer = consumer_;
  }

  PasswordsMediator* mediator() { return mediator_; }

  ChromeBrowserState* browserState() { return browser_state_.get(); }

  TestPasswordStore* store() { return store_.get(); }

  FakePasswordsConsumer* consumer() { return consumer_; }

  feature_engagement::test::MockTracker* mockTracker() { return mock_tracker_; }

  void RunUntilIdle() { task_environment_.RunUntilIdle(); }

 private:
  web::WebTaskEnvironment task_environment_;
  std::unique_ptr<TestChromeBrowserState> browser_state_;
  scoped_refptr<TestPasswordStore> store_;
  scoped_refptr<IOSChromePasswordCheckManager> password_check_;
  FakePasswordsConsumer* consumer_;
  PasswordsMediator* mediator_;
  raw_ptr<feature_engagement::test::MockTracker> mock_tracker_;
};

// Consumer should be notified when passwords are changed.
TEST_F(PasswordsMediatorTest, NotifiesConsumerOnPasswordChange) {
  PasswordForm form = CreatePasswordForm();
  store()->AddLogin(form);
  RunUntilIdle();
  password_manager::CredentialUIEntry credential(form);
  std::vector<password_manager::AffiliatedGroup> affiliatedGroups =
      [consumer() affiliatedGroups];
  EXPECT_EQ(1u, affiliatedGroups.size());
  EXPECT_THAT(affiliatedGroups[0].GetCredentials(),
              testing::ElementsAre(credential));
  // Remove form from the store.
  store()->RemoveLogin(FROM_HERE, form);
  RunUntilIdle();
  affiliatedGroups = [consumer() affiliatedGroups];
  EXPECT_THAT(affiliatedGroups, testing::IsEmpty());
}

// Tests that `ShouldTriggerHelpUI` is called on the FET and that the consumer
// is being notified of whether the Password Manager widget promo should be
// shown when the mediator's consumer is set.
TEST_F(PasswordsMediatorTest, NotifiesConsumerToShowPromoOrNot) {
  // Make sure that `shouldShowPasswordManagerWidgetPromoCalled` isn't already
  // true.
  EXPECT_FALSE(consumer().shouldShowPasswordManagerWidgetPromoCalled);

  EXPECT_CALL(
      *mockTracker(),
      ShouldTriggerHelpUI(testing::Ref(
          feature_engagement::kIPHiOSPromoPasswordManagerWidgetFeature)))
      .Times(testing::Exactly(1));

  [mediator() askFETToShowPasswordManagerWidgetPromo];

  EXPECT_TRUE(consumer().shouldShowPasswordManagerWidgetPromoCalled);
}

// Tests that `Dismissed` is called on the FET on disconnect when the Password
// Manager widget promo was shown and was not dismissed by the user.
TEST_F(PasswordsMediatorTest, NotifiesFETToDismissPromoOnDisconnect) {
  // Show the promo first.
  EXPECT_CALL(*mockTracker(), ShouldTriggerHelpUI(testing::_))
      .WillRepeatedly(testing::Return(true));
  [mediator() askFETToShowPasswordManagerWidgetPromo];

  ASSERT_TRUE(mediator().shouldNotifyFETToDismissPasswordManagerWidgetPromo);

  EXPECT_CALL(
      *mockTracker(),
      Dismissed(testing::Ref(
          feature_engagement::kIPHiOSPromoPasswordManagerWidgetFeature)))
      .Times(testing::Exactly(1));

  [mediator() disconnect];
}

// Tests that `NotifyEvent` and `Dismissed` is called on the FET when the user
// taps the close button of the Password Manager widget promo.
TEST_F(PasswordsMediatorTest, NotifiesFETToDismissPromoOnPromoClosed) {
  // Show the promo first.
  EXPECT_CALL(*mockTracker(), ShouldTriggerHelpUI(testing::_))
      .WillRepeatedly(testing::Return(true));
  [mediator() askFETToShowPasswordManagerWidgetPromo];

  ASSERT_TRUE(mediator().shouldNotifyFETToDismissPasswordManagerWidgetPromo);

  EXPECT_CALL(
      *mockTracker(),
      NotifyEvent(
          feature_engagement::events::kPasswordManagerWidgetPromoClosed));
  EXPECT_CALL(
      *mockTracker(),
      Dismissed(testing::Ref(
          feature_engagement::kIPHiOSPromoPasswordManagerWidgetFeature)))
      .Times(testing::Exactly(1));

  [mediator() notifyFETOfPasswordManagerWidgetPromoDismissal];

  EXPECT_FALSE(mediator().shouldNotifyFETToDismissPasswordManagerWidgetPromo);
}