chromium/ios/chrome/browser/ui/settings/password/password_issues/password_issues_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/password_issues/password_issues_mediator.h"

#import "base/strings/sys_string_conversions.h"
#import "base/test/bind.h"
#import "base/test/scoped_feature_list.h"
#import "components/affiliations/core/browser/fake_affiliation_service.h"
#import "components/google/core/common/google_util.h"
#import "components/keyed_service/core/service_access_type.h"
#import "components/password_manager/core/browser/password_form.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 "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/net/model/crurl.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/passwords/model/password_checkup_utils.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/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/ui/settings/password/password_checkup/password_checkup_constants.h"
#import "ios/chrome/browser/ui/settings/password/password_issues/password_issues_consumer.h"
#import "ios/chrome/grit/ios_strings.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"
#import "ui/base/l10n/l10n_util_mac.h"

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

namespace {

constexpr char kExampleCom[] = "https://example.com";
constexpr char kExampleCom2[] = "https://example2.com";
constexpr char kExampleCom3[] = "https://example3.com";

constexpr NSString* kExampleString = @"example.com";
constexpr NSString* kExample2String = @"example2.com";
constexpr NSString* kExample3String = @"example3.com";

constexpr char kUsername[] = "alice";
constexpr char kUsername2[] = "bob";

constexpr char kPassword[] = "s3cre3t";
constexpr char kPassword2[] = "s3cre3t2";
constexpr char kStrongPassword[] = "pmsFlsnoab4nsl#losb@skpfnsbkjb^klsnbs!cns";
constexpr char kStrongPassword2[] = "sfdf#losb@sdf^klsnbs!cns";
constexpr char kStrongPassword3[] = "sdfsdfwer@313QaDSdsd!cns";

NSString* GetUsername() {
  return base::SysUTF8ToNSString(kUsername);
}

NSString* GetUsername2() {
  return base::SysUTF8ToNSString(kUsername2);
}

// Returns a URL with localized according to the Application Locale.
GURL GetLocalizedURL(const GURL& original) {
  return google_util::AppendGoogleLocaleParam(
      original, GetApplicationContext()->GetApplicationLocale());
}

}  // namespace

// Test class that conforms to PasswordIssuesConsumer in order to test the
// consumer methods are called correctly.
@interface FakePasswordIssuesConsumer : NSObject <PasswordIssuesConsumer>

@property(nonatomic, strong) NSArray<PasswordIssueGroup*>* passwordIssueGroups;

@property(nonatomic, assign) BOOL passwordIssuesListChangedWasCalled;

@property(nonatomic, copy) NSString* title;

@property(nonatomic, copy) NSString* headerText;

@property(nonatomic, copy) CrURL* headerURL;

@property(nonatomic, assign) NSInteger dismissedWarningsCount;

@end

@implementation FakePasswordIssuesConsumer

- (void)setPasswordIssues:(NSArray<PasswordIssueGroup*>*)passwordIssueGroups
    dismissedWarningsCount:(NSInteger)dismissedWarningsCount {
  _passwordIssueGroups = passwordIssueGroups;
  _dismissedWarningsCount = dismissedWarningsCount;
  _passwordIssuesListChangedWasCalled = YES;
}

- (void)setNavigationBarTitle:(NSString*)title {
  _title = title;
}

- (void)setHeader:(NSString*)text URL:(CrURL*)URL {
  _headerText = text;
  _headerURL = URL;
}

@end

// Tests for Password Issues mediator.
class PasswordIssuesMediatorTest : public BlockCleanupTest {
 protected:
  void SetUp() override {
    BlockCleanupTest::SetUp();
    // Create BrowserState.
    TestChromeBrowserState::Builder test_cbs_builder;
    test_cbs_builder.AddTestingFactory(
        IOSChromeProfilePasswordStoreFactory::GetInstance(),
        base::BindRepeating(
            &password_manager::BuildPasswordStore<web::BrowserState,
                                                  TestPasswordStore>));
    test_cbs_builder.AddTestingFactory(
        IOSChromeAffiliationServiceFactory::GetInstance(),
        base::BindRepeating(base::BindLambdaForTesting([](web::BrowserState*) {
          return std::unique_ptr<KeyedService>(
              std::make_unique<affiliations::FakeAffiliationService>());
        })));
    chrome_browser_state_ = std::move(test_cbs_builder).Build();

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

    password_check_ = IOSChromePasswordCheckManagerFactory::GetForBrowserState(
        chrome_browser_state_.get());

    consumer_ = [[FakePasswordIssuesConsumer alloc] init];

    CreateMediator(WarningType::kCompromisedPasswordsWarning);
  }

  // Creates a mediator for the given warning type.
  void CreateMediator(WarningType warning_type) {
    mediator_ = [[PasswordIssuesMediator alloc]
          initForWarningType:warning_type
        passwordCheckManager:password_check_.get()
               faviconLoader:IOSChromeFaviconLoaderFactory::GetForBrowserState(
                                 chrome_browser_state_.get())
                 syncService:SyncServiceFactory::GetForBrowserState(
                                 chrome_browser_state_.get())];
    mediator_.consumer = consumer_;
  }

  // Adds password form and insecure password to the store.
  void MakeTestPasswordIssue(std::string website = kExampleCom,
                             std::string username = kUsername,
                             std::string password = kPassword,
                             InsecureType insecure_type = InsecureType::kLeaked,
                             bool muted = false) {
    PasswordForm form;
    form.signon_realm = website;
    form.username_value = base::ASCIIToUTF16(username);
    form.password_value = base::ASCIIToUTF16(password);
    form.url = GURL(website + "/login");
    form.action = GURL(website + "/action");
    form.username_element = u"email";
    form.password_issues = {
        {insecure_type,
         password_manager::InsecurityMetadata(
             base::Time::Now(), password_manager::IsMuted(muted),
             password_manager::TriggerBackendNotification(false))}};
    form.in_store = PasswordForm::Store::kProfileStore;
    store()->AddLogin(form);
  }

  void CheckIssue(NSUInteger group = 0,
                  NSUInteger index = 0,
                  NSString* expected_website = kExampleString,
                  NSString* expected_username = GetUsername()) {
    ASSERT_LT(group, consumer().passwordIssueGroups.count);

    PasswordIssueGroup* issue_group = consumer().passwordIssueGroups[group];
    ASSERT_LT(index, issue_group.passwordIssues.count);

    PasswordIssue* issue = issue_group.passwordIssues[index];

    EXPECT_NSEQ(expected_username, issue.username);
    EXPECT_NSEQ(expected_website, issue.website);
  }

  void CheckGroupsCount(NSUInteger expected_count) {
    EXPECT_EQ(expected_count, consumer().passwordIssueGroups.count);
  }

  void CheckGroupSize(NSUInteger group, NSUInteger expected_size) {
    ASSERT_LT(group, consumer().passwordIssueGroups.count);
    EXPECT_EQ(expected_size,
              consumer().passwordIssueGroups[group].passwordIssues.count);
  }

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

  FakePasswordIssuesConsumer* consumer() { return consumer_; }

  PasswordIssuesMediator* mediator() { return mediator_; }

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

 private:
  base::test::ScopedFeatureList feature_list_;
  web::WebTaskEnvironment task_environment_;
  std::unique_ptr<TestChromeBrowserState> chrome_browser_state_;
  scoped_refptr<TestPasswordStore> store_;
  scoped_refptr<IOSChromePasswordCheckManager> password_check_;
  FakePasswordIssuesConsumer* consumer_;
  PasswordIssuesMediator* mediator_;
};

// Tests that changes to password store are reflected to the consumer.
TEST_F(PasswordIssuesMediatorTest, TestPasswordIssuesChanged) {
  CheckGroupsCount(0);
  consumer().passwordIssuesListChangedWasCalled = NO;

  MakeTestPasswordIssue();
  RunUntilIdle();

  EXPECT_TRUE([consumer() passwordIssuesListChangedWasCalled]);

  CheckGroupsCount(1);
  CheckGroupSize(/*group=*/0, /*expected_size=*/1);
  CheckIssue();
}

// Tests that changes to password store are not sent to the consumer if the
// credentials with the current warning type did not change.
TEST_F(PasswordIssuesMediatorTest, TestPasswordIssuesChangedNotCalled) {
  CreateMediator(WarningType::kCompromisedPasswordsWarning);

  CheckGroupsCount(0);
  consumer().passwordIssuesListChangedWasCalled = NO;

  // Add other types of insecure passwords that shouldn't be sent to the
  // consumer.
  MakeTestPasswordIssue(kExampleCom, kUsername, kPassword, InsecureType::kWeak);
  MakeTestPasswordIssue(kExampleCom2, kUsername, kPassword,
                        InsecureType::kReused);
  RunUntilIdle();

  EXPECT_FALSE([consumer() passwordIssuesListChangedWasCalled]);

  // Add compromised password that should be sent to consumer.
  MakeTestPasswordIssue(kExampleCom, kUsername2, kPassword,
                        InsecureType::kLeaked);
  RunUntilIdle();

  EXPECT_TRUE([consumer() passwordIssuesListChangedWasCalled]);

  CheckGroupsCount(1);
  CheckGroupSize(/*group=*/0, /*expected_size=*/1);
  CheckIssue(/*group=*/0, /*index=*/0, /*expected_website=*/kExampleString,
             /*expected_username=*/GetUsername2());
}

// Tests that only passwords issues of the current warning type are sent to the
// consumer.
TEST_F(PasswordIssuesMediatorTest, TestPasswordIssuesFilteredByWarningType) {
  // Create all types of insecure passwords.
  // Weak.
  MakeTestPasswordIssue(kExampleCom, kUsername, kPassword, InsecureType::kWeak);
  // Reused.
  MakeTestPasswordIssue(kExampleCom2, kUsername, kStrongPassword,
                        InsecureType::kReused);
  MakeTestPasswordIssue(kExampleCom3, kUsername2, kStrongPassword,
                        InsecureType::kReused);
  // Dismissed Compromised
  MakeTestPasswordIssue(kExampleCom3, kUsername, kStrongPassword2,
                        InsecureType::kLeaked, /*muted=*/true);
  // Compromised.
  MakeTestPasswordIssue(kExampleCom, kUsername2, kStrongPassword3,
                        InsecureType::kPhished);
  RunUntilIdle();

  // Send only compromised passwords to consumer.
  CreateMediator(WarningType::kCompromisedPasswordsWarning);

  CheckIssue(/*group=*/0, /*index=*/0, /*expected_website=*/kExampleString,
             /*expected_username=*/GetUsername2());

  EXPECT_EQ(consumer().dismissedWarningsCount, 1);

  // Send only weak passwords to consumer.
  CreateMediator(WarningType::kWeakPasswordsWarning);

  CheckIssue();

  EXPECT_EQ(0, consumer().dismissedWarningsCount);

  // Send only reused passwords to consumer.
  CreateMediator(WarningType::kReusedPasswordsWarning);

  CheckIssue(/*group=*/0, /*index=*/0, /*expected_website=*/kExample2String);
  CheckIssue(/*group=*/0, /*index=*/1, /*expected_website=*/kExample3String,
             /*expected_username=*/GetUsername2());

  EXPECT_EQ(0, consumer().dismissedWarningsCount);

  // Send only dismissed passwords to consumer.
  CreateMediator(WarningType::kDismissedWarningsWarning);

  CheckIssue(/*group=*/0, /*index=*/0, /*expected_website=*/kExample3String);

  EXPECT_EQ(0, consumer().dismissedWarningsCount);
}

/// Tests the mediator sets the consumer title for compromised passwords.
TEST_F(PasswordIssuesMediatorTest, TestSetConsumerCompromisedTitle) {
  CreateMediator(WarningType::kCompromisedPasswordsWarning);

  EXPECT_NSEQ(@"Compromised Passwords", consumer().title);

  MakeTestPasswordIssue();
  RunUntilIdle();

  EXPECT_NSEQ(@"1 Compromised Password", consumer().title);

  MakeTestPasswordIssue(kExampleCom2);
  RunUntilIdle();

  EXPECT_NSEQ(@"2 Compromised Passwords", consumer().title);
}

/// Tests the mediator sets the consumer title for weak passwords.
TEST_F(PasswordIssuesMediatorTest, TestSetConsumerWeakTitle) {
  CreateMediator(WarningType::kWeakPasswordsWarning);

  MakeTestPasswordIssue(kExampleCom, kUsername, kPassword, InsecureType::kWeak);
  RunUntilIdle();

  EXPECT_NSEQ(@"1 Weak Password", consumer().title);

  MakeTestPasswordIssue(kExampleCom2, kUsername, kPassword,
                        InsecureType::kWeak);
  RunUntilIdle();

  EXPECT_NSEQ(@"2 Weak Passwords", consumer().title);
}

/// Tests the mediator sets the consumer title for dismissed warnings.
TEST_F(PasswordIssuesMediatorTest, TestSetConsumerDismissedTitle) {
  CreateMediator(WarningType::kDismissedWarningsWarning);

  MakeTestPasswordIssue();
  RunUntilIdle();

  EXPECT_NSEQ(@"Dismissed Warnings", consumer().title);
}

/// Tests the mediator sets the consumer title for reused passwords.
TEST_F(PasswordIssuesMediatorTest, TestSetConsumerReusedTitle) {
  CreateMediator(WarningType::kReusedPasswordsWarning);

  MakeTestPasswordIssue(kExampleCom, kUsername, kPassword,
                        InsecureType::kReused);
  MakeTestPasswordIssue(kExampleCom2, kUsername, kPassword,
                        InsecureType::kReused);
  RunUntilIdle();

  EXPECT_NSEQ(@"2 Reused Passwords", consumer().title);
}

/// Tests the mediator sets the consumer header for compromised passwords.
TEST_F(PasswordIssuesMediatorTest, TestSetConsumerCompromisedHeader) {
  CreateMediator(WarningType::kCompromisedPasswordsWarning);

  EXPECT_NSEQ(
      l10n_util::GetNSString(IDS_IOS_COMPROMISED_PASSWORD_ISSUES_DESCRIPTION),
      consumer().headerText);
  EXPECT_EQ(GetLocalizedURL(
                GURL(password_manager::
                         kPasswordManagerHelpCenterChangeUnsafePasswordsURL)),
            consumer().headerURL.gurl);
}

/// Tests the mediator sets the consumer header for weak passwords.
TEST_F(PasswordIssuesMediatorTest, TestSetConsumerWeakHeader) {
  CreateMediator(WarningType::kWeakPasswordsWarning);

  EXPECT_NSEQ(l10n_util::GetNSString(IDS_IOS_WEAK_PASSWORD_ISSUES_DESCRIPTION),
              consumer().headerText);
  EXPECT_EQ(GetLocalizedURL(
                GURL(password_manager::
                         kPasswordManagerHelpCenterCreateStrongPasswordsURL)),
            consumer().headerURL.gurl);
}

/// Tests the mediator sets the consumer header for reused passwords.
TEST_F(PasswordIssuesMediatorTest, TestSetConsumerReusedHeader) {
  CreateMediator(WarningType::kReusedPasswordsWarning);

  EXPECT_NSEQ(
      l10n_util::GetNSString(IDS_IOS_REUSED_PASSWORD_ISSUES_DESCRIPTION),
      consumer().headerText);

  EXPECT_FALSE(consumer().headerURL);
}

/// Tests the mediator doesn't set a header for dismissed warnings.
TEST_F(PasswordIssuesMediatorTest, TestSetConsumerDismissedHeader) {
  consumer().headerText = nil;
  consumer().headerURL = nil;

  CreateMediator(WarningType::kDismissedWarningsWarning);

  EXPECT_FALSE(consumer().headerText);
  EXPECT_FALSE(consumer().headerURL);
}

// Tests that passwords are sorted properly.
TEST_F(PasswordIssuesMediatorTest, TestPasswordSorting) {
  CheckGroupsCount(0);

  MakeTestPasswordIssue(kExampleCom3);
  MakeTestPasswordIssue(kExampleCom2, kUsername2);
  RunUntilIdle();

  CheckGroupsCount(1);
  CheckGroupSize(/*group=*/0, /*expected_size=*/2);

  CheckIssue(/*group=*/0, /*index=*/0, /*expected_website=*/kExample2String,
             /*expected_username=*/GetUsername2());
  CheckIssue(/*group=*/0, /*index=*/1, /*expected_website=*/kExample3String);

  MakeTestPasswordIssue(kExampleCom, kUsername2);
  MakeTestPasswordIssue(kExampleCom);
  RunUntilIdle();

  CheckGroupsCount(1);
  CheckGroupSize(/*group=*/0, /*expected_size=*/4);

  CheckIssue();
  CheckIssue(/*group=*/0, /*index=*/1, /*expected_website=*/kExampleString,
             /*expected_username=*/GetUsername2());
  CheckIssue(/*group=*/0, /*index=*/2, /*expected_website=*/kExample2String,
             /*expected_username=*/GetUsername2());
  CheckIssue(/*group=*/0, /*index=*/3, /*expected_website=*/kExample3String);
}

// Tests that reused password issues are grouped by password.
TEST_F(PasswordIssuesMediatorTest, TestReusedPasswordsGrouping) {
  CreateMediator(WarningType::kReusedPasswordsWarning);
  CheckGroupsCount(0);

  // Create group of reused passwords.
  MakeTestPasswordIssue(kExampleCom3, kUsername, kPassword,
                        InsecureType::kReused);
  MakeTestPasswordIssue(kExampleCom2, kUsername2, kPassword,
                        InsecureType::kReused);
  MakeTestPasswordIssue(kExampleCom2, kUsername, kPassword,
                        InsecureType::kReused);

  // Create another group of reused passwords.
  MakeTestPasswordIssue(kExampleCom, kUsername2, kPassword2,
                        InsecureType::kReused);
  MakeTestPasswordIssue(kExampleCom3, kUsername2, kPassword2,
                        InsecureType::kReused);

  RunUntilIdle();

  CheckGroupsCount(2);

  // Validate first group.
  CheckGroupSize(/*group=*/0, /*expected_size=*/2);
  CheckIssue(/*group=*/0, /*index=*/0, /*expected_website=*/kExampleString,
             /*expected_username=*/GetUsername2());
  CheckIssue(/*group=*/0, /*index=*/1, /*expected_website=*/kExample3String,
             /*expected_username=*/GetUsername2());

  // Validate second group.
  CheckGroupSize(/*group=*/1, /*expected_size=*/3);
  CheckIssue(/*group=*/1, /*index=*/0, /*expected_website=*/kExample2String);
  CheckIssue(/*group=*/1, /*index=*/1, /*expected_website=*/kExample2String,
             /*expected_username=*/GetUsername2());
  CheckIssue(/*group=*/1, /*index=*/2, /*expected_website=*/kExample3String);
}