| // 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 "components/autofill/core/browser/suggestion_selection.h" |
| |
| #include <algorithm> |
| #include <iterator> |
| |
| #include "base/guid.h" |
| #include "base/rand_util.h" |
| #include "base/strings/stringprintf.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "base/test/metrics/histogram_tester.h" |
| #include "base/time/time.h" |
| #include "components/autofill/core/browser/autofill_profile_comparator.h" |
| #include "components/autofill/core/browser/autofill_test_utils.h" |
| #include "components/autofill/core/common/autofill_clock.h" |
| #include "testing/gmock/include/gmock/gmock.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| |
| namespace autofill { |
| namespace suggestion_selection { |
| |
| using testing::Each; |
| using testing::ElementsAre; |
| using testing::Field; |
| using testing::Not; |
| using testing::ResultOf; |
| |
| namespace { |
| const std::string TEST_APP_LOCALE = "en-US"; |
| } |
| |
| class SuggestionSelectionTest : public testing::Test { |
| public: |
| SuggestionSelectionTest() |
| : app_locale_(TEST_APP_LOCALE), comparator_(TEST_APP_LOCALE) {} |
| |
| protected: |
| std::unique_ptr<AutofillProfile> CreateProfileUniquePtr( |
| const char* first_name, |
| const char* last_name = "Morrison") { |
| std::unique_ptr<AutofillProfile> profile_ptr = |
| std::make_unique<AutofillProfile>(base::GenerateGUID(), |
| test::kEmptyOrigin); |
| test::SetProfileInfo(profile_ptr.get(), first_name, "Mitchell", last_name, |
| "johnwayne@me.xyz", "Fox", |
| "123 Zoo St.\nSecond Line\nThird line", "unit 5", |
| "Hollywood", "CA", "91601", "US", "12345678910"); |
| return profile_ptr; |
| } |
| |
| base::string16 GetCanonicalUtf16Content(const char* content) { |
| return comparator_.NormalizeForComparison(base::ASCIIToUTF16(content)); |
| } |
| |
| std::vector<Suggestion> CreateSuggestions( |
| const std::vector<AutofillProfile*>& profiles, |
| const ServerFieldType& field_type) { |
| std::vector<Suggestion> suggestions; |
| std::transform(profiles.begin(), profiles.end(), |
| std::back_inserter(suggestions), |
| [field_type](const AutofillProfile* profile) { |
| return Suggestion(profile->GetRawInfo(field_type)); |
| }); |
| |
| return suggestions; |
| } |
| |
| const std::string app_locale_; |
| const AutofillProfileComparator comparator_; |
| }; |
| |
| TEST_F(SuggestionSelectionTest, |
| GetPrefixMatchedSuggestions_GetMatchingProfile) { |
| const std::unique_ptr<AutofillProfile> profile1 = |
| CreateProfileUniquePtr("Marion"); |
| const std::unique_ptr<AutofillProfile> profile2 = |
| CreateProfileUniquePtr("Bob"); |
| |
| std::vector<AutofillProfile*> matched_profiles; |
| auto suggestions = GetPrefixMatchedSuggestions( |
| AutofillType(NAME_FIRST), GetCanonicalUtf16Content("Mar"), comparator_, |
| {profile1.get(), profile2.get()}, &matched_profiles); |
| |
| ASSERT_EQ(1U, suggestions.size()); |
| ASSERT_EQ(1U, matched_profiles.size()); |
| EXPECT_THAT(suggestions, ElementsAre(Field(&Suggestion::value, |
| base::ASCIIToUTF16("Marion")))); |
| } |
| |
| TEST_F(SuggestionSelectionTest, GetPrefixMatchedSuggestions_NoMatchingProfile) { |
| const std::unique_ptr<AutofillProfile> profile1 = |
| CreateProfileUniquePtr("Bob"); |
| |
| std::vector<AutofillProfile*> matched_profiles; |
| auto suggestions = GetPrefixMatchedSuggestions( |
| AutofillType(NAME_FIRST), GetCanonicalUtf16Content("Mar"), comparator_, |
| {profile1.get()}, &matched_profiles); |
| |
| ASSERT_TRUE(matched_profiles.empty()); |
| ASSERT_TRUE(suggestions.empty()); |
| } |
| |
| TEST_F(SuggestionSelectionTest, |
| GetPrefixMatchedSuggestions_EmptyProfilesInput) { |
| std::vector<AutofillProfile*> matched_profiles; |
| auto suggestions = GetPrefixMatchedSuggestions( |
| AutofillType(NAME_FIRST), GetCanonicalUtf16Content("Mar"), comparator_, |
| {}, &matched_profiles); |
| |
| ASSERT_TRUE(matched_profiles.empty()); |
| ASSERT_TRUE(suggestions.empty()); |
| } |
| |
| TEST_F(SuggestionSelectionTest, GetPrefixMatchedSuggestions_LimitProfiles) { |
| std::vector<std::unique_ptr<AutofillProfile>> profiles_data; |
| for (size_t i = 0; i < kMaxSuggestedProfilesCount; i++) { |
| profiles_data.push_back(CreateProfileUniquePtr("Marion")); |
| } |
| |
| // Add another profile to go above the limit. |
| profiles_data.push_back(CreateProfileUniquePtr("Marie")); |
| |
| // Map all the pointers into an array that has the right type. |
| std::vector<AutofillProfile*> profiles_pointers; |
| std::transform(profiles_data.begin(), profiles_data.end(), |
| std::back_inserter(profiles_pointers), |
| [](const std::unique_ptr<AutofillProfile>& profile) { |
| return profile.get(); |
| }); |
| |
| std::vector<AutofillProfile*> matched_profiles; |
| auto suggestions = GetPrefixMatchedSuggestions( |
| AutofillType(NAME_FIRST), GetCanonicalUtf16Content("Mar"), comparator_, |
| profiles_pointers, &matched_profiles); |
| |
| // Marie should not be found. |
| ASSERT_EQ(kMaxSuggestedProfilesCount, suggestions.size()); |
| ASSERT_EQ(kMaxSuggestedProfilesCount, matched_profiles.size()); |
| |
| EXPECT_THAT(suggestions, Each(Field(&Suggestion::value, |
| Not(base::ASCIIToUTF16("Marie"))))); |
| |
| EXPECT_THAT(matched_profiles, |
| Each(ResultOf( |
| [](const AutofillProfile* profile_ptr) { |
| return profile_ptr->GetRawInfo(NAME_FIRST); |
| }, |
| Not(base::ASCIIToUTF16("Marie"))))); |
| } |
| |
| TEST_F(SuggestionSelectionTest, GetUniqueSuggestions_SingleDedupe) { |
| // Give two suggestions with the same name, and no other field to compare. |
| // Expect only one unique suggestion. |
| const std::unique_ptr<AutofillProfile> profile1 = |
| CreateProfileUniquePtr("Bob"); |
| const std::unique_ptr<AutofillProfile> profile2 = |
| CreateProfileUniquePtr("Bob"); |
| |
| auto profile_pointers = {profile1.get(), profile2.get()}; |
| |
| std::vector<AutofillProfile*> unique_matched_profiles; |
| auto unique_suggestions = |
| GetUniqueSuggestions({}, app_locale_, profile_pointers, |
| CreateSuggestions(profile_pointers, NAME_FIRST), |
| &unique_matched_profiles); |
| |
| ASSERT_EQ(1U, unique_suggestions.size()); |
| ASSERT_EQ(1U, unique_matched_profiles.size()); |
| EXPECT_THAT( |
| unique_suggestions, |
| ElementsAre(Field(&Suggestion::value, base::ASCIIToUTF16("Bob")))); |
| } |
| |
| TEST_F(SuggestionSelectionTest, GetUniqueSuggestions_MultipleDedupe) { |
| // Give two suggestions with the same name and one with a different, and |
| // also last name field to compare. |
| // Expect all profiles listed as unique suggestions. |
| const std::unique_ptr<AutofillProfile> profile1 = |
| CreateProfileUniquePtr("Bob", "Morrison"); |
| const std::unique_ptr<AutofillProfile> profile2 = |
| CreateProfileUniquePtr("Bob", "Parker"); |
| const std::unique_ptr<AutofillProfile> profile3 = |
| CreateProfileUniquePtr("Mary", "Parker"); |
| |
| auto profile_pointers = {profile1.get(), profile2.get(), profile3.get()}; |
| |
| std::vector<AutofillProfile*> unique_matched_profiles; |
| auto unique_suggestions = |
| GetUniqueSuggestions({NAME_LAST}, app_locale_, profile_pointers, |
| CreateSuggestions(profile_pointers, NAME_FIRST), |
| &unique_matched_profiles); |
| |
| ASSERT_EQ(3U, unique_suggestions.size()); |
| ASSERT_EQ(3U, unique_matched_profiles.size()); |
| |
| EXPECT_THAT( |
| unique_suggestions, |
| ElementsAre(Field(&Suggestion::value, base::ASCIIToUTF16("Bob")), |
| Field(&Suggestion::value, base::ASCIIToUTF16("Bob")), |
| Field(&Suggestion::value, base::ASCIIToUTF16("Mary")))); |
| } |
| |
| TEST_F(SuggestionSelectionTest, GetUniqueSuggestions_DedupeLimit) { |
| // Test limit of suggestions. |
| std::vector<std::unique_ptr<AutofillProfile>> profiles_data; |
| for (size_t i = 0; i < kMaxUniqueSuggestionsCount + 1; i++) { |
| profiles_data.push_back(CreateProfileUniquePtr( |
| base::StringPrintf("Bob %zu", i).c_str(), "Doe")); |
| } |
| |
| // Map all the pointers into an array that has the right type. |
| std::vector<AutofillProfile*> profiles_pointers; |
| std::transform(profiles_data.begin(), profiles_data.end(), |
| std::back_inserter(profiles_pointers), |
| [](const std::unique_ptr<AutofillProfile>& profile) { |
| return profile.get(); |
| }); |
| |
| std::vector<AutofillProfile*> unique_matched_profiles; |
| auto unique_suggestions = |
| GetUniqueSuggestions({NAME_LAST}, app_locale_, profiles_pointers, |
| CreateSuggestions(profiles_pointers, NAME_FIRST), |
| &unique_matched_profiles); |
| |
| ASSERT_EQ(kMaxUniqueSuggestionsCount, unique_suggestions.size()); |
| ASSERT_EQ(kMaxUniqueSuggestionsCount, unique_matched_profiles.size()); |
| |
| // All profiles are different. |
| for (size_t i = 0; i < unique_suggestions.size(); i++) { |
| ASSERT_EQ(base::ASCIIToUTF16(base::StringPrintf("Bob %zu", i)), |
| unique_suggestions[i].value); |
| } |
| } |
| |
| TEST_F(SuggestionSelectionTest, GetUniqueSuggestions_EmptyMatchingProfiles) { |
| std::vector<AutofillProfile*> unique_matched_profiles; |
| auto unique_suggestions = GetUniqueSuggestions({NAME_LAST}, app_locale_, {}, |
| {}, &unique_matched_profiles); |
| |
| ASSERT_EQ(0U, unique_matched_profiles.size()); |
| ASSERT_EQ(0U, unique_suggestions.size()); |
| } |
| |
| TEST_F(SuggestionSelectionTest, RemoveProfilesNotUsedSinceTimestamp) { |
| const char kAddressesSuppressedHistogramName[] = |
| "Autofill.AddressesSuppressedForDisuse"; |
| base::Time kCurrentTime; |
| bool result = |
| base::Time::FromUTCString("2017-01-02T00:00:01Z", &kCurrentTime); |
| ASSERT_TRUE(result); |
| constexpr size_t kNumProfiles = 10; |
| constexpr base::TimeDelta k30Days = base::TimeDelta::FromDays(30); |
| constexpr base::TimeDelta k5DaysBuffer = base::TimeDelta::FromDays(5); |
| |
| // Set up the profile vectors with last use dates ranging from |kCurrentTime| |
| // to 270 days ago, in 30 day increments. Note that the profiles are sorted |
| // by decreasing last use date. |
| std::vector<std::unique_ptr<AutofillProfile>> all_profile_data; |
| for (size_t i = 0; i < kNumProfiles; ++i) { |
| all_profile_data.push_back(std::make_unique<AutofillProfile>( |
| base::GenerateGUID(), "https://example.com")); |
| all_profile_data[i]->set_use_date(kCurrentTime - (i * k30Days)); |
| } |
| |
| // Map all the pointers into an array that has the right type. |
| std::vector<AutofillProfile*> all_profile_ptrs; |
| std::transform(all_profile_data.begin(), all_profile_data.end(), |
| std::back_inserter(all_profile_ptrs), |
| [](const std::unique_ptr<AutofillProfile>& profile) { |
| return profile.get(); |
| }); |
| |
| // Verify that disused profiles get removed from the end. Note that the last |
| // four profiles have use dates more than 175 days ago. |
| { |
| const int kNbSuggestions = 6; |
| const base::Time k175DaysAgo = |
| kCurrentTime - (k30Days * kNbSuggestions) + k5DaysBuffer; |
| // Create a working copy of the profile pointers. |
| std::vector<AutofillProfile*> profiles(all_profile_ptrs); |
| |
| // The first 6 have use dates more recent than 175 days ago. |
| std::vector<AutofillProfile*> expected_profiles( |
| profiles.begin(), profiles.begin() + kNbSuggestions); |
| |
| // Filter the profiles while capturing histograms. |
| base::HistogramTester histogram_tester; |
| suggestion_selection::RemoveProfilesNotUsedSinceTimestamp(k175DaysAgo, |
| &profiles); |
| |
| // Validate that we get the expected filtered profiles and histograms. |
| EXPECT_EQ(expected_profiles, profiles); |
| histogram_tester.ExpectTotalCount(kAddressesSuppressedHistogramName, 1); |
| histogram_tester.ExpectBucketCount(kAddressesSuppressedHistogramName, |
| kNumProfiles - kNbSuggestions, 1); |
| } |
| |
| // Reverse the profile order and verify that disused profiles get removed |
| // from the beginning. Note that the first five profiles, post reversal, have |
| // use dates more then 145 days ago. |
| { |
| const int kNbSuggestions = 5; |
| const base::Time k145DaysAgo = |
| kCurrentTime - (k30Days * kNbSuggestions) + k5DaysBuffer; |
| // Create a reversed working copy of the profile pointers. |
| std::vector<AutofillProfile*> profiles(all_profile_ptrs.rbegin(), |
| all_profile_ptrs.rend()); |
| |
| // The last 5 profiles have use dates more recent than 145 days ago. |
| std::vector<AutofillProfile*> expected_profiles( |
| profiles.begin() + kNbSuggestions, profiles.end()); |
| |
| // Filter the profiles while capturing histograms. |
| base::HistogramTester histogram_tester; |
| suggestion_selection::RemoveProfilesNotUsedSinceTimestamp(k145DaysAgo, |
| &profiles); |
| |
| // Validate that we get the expected filtered profiles and histograms. |
| EXPECT_EQ(expected_profiles, profiles); |
| histogram_tester.ExpectTotalCount(kAddressesSuppressedHistogramName, 1); |
| histogram_tester.ExpectBucketCount(kAddressesSuppressedHistogramName, |
| kNumProfiles - kNbSuggestions, 1); |
| } |
| |
| // Randomize the profile order and validate that the filtered list retains |
| // that order. Note that the six profiles have use dates more then 115 days |
| // ago. |
| { |
| const int kNbSuggestions = 4; |
| const base::Time k115DaysAgo = |
| kCurrentTime - (k30Days * kNbSuggestions) + k5DaysBuffer; |
| |
| // Created a shuffled master copy of the profile pointers. |
| std::vector<AutofillProfile*> shuffled_profiles(all_profile_ptrs); |
| base::RandomShuffle(shuffled_profiles.begin(), shuffled_profiles.end()); |
| |
| // Copy the shuffled profile pointer collections to use as the working set. |
| std::vector<AutofillProfile*> profiles_recently_used(shuffled_profiles); |
| |
| // Filter the profiles while capturing histograms. |
| base::HistogramTester histogram_tester; |
| suggestion_selection::RemoveProfilesNotUsedSinceTimestamp( |
| k115DaysAgo, &profiles_recently_used); |
| |
| // Validate that we have the right profiles. Iterate of the shuffled master |
| // copy and the filtered copy at the same time making sure that the elements |
| // in the filtered copy occur in the same order as the shuffled master. |
| // Along the way, validate that the elements in and out of the filtered copy |
| // have appropriate use dates. |
| // TODO(crbug.com/908456): Refactor the loops to use STL and Gmock. |
| EXPECT_EQ(4u, profiles_recently_used.size()); |
| auto it = shuffled_profiles.begin(); |
| for (const AutofillProfile* profile : profiles_recently_used) { |
| for (; it != shuffled_profiles.end() && (*it) != profile; ++it) { |
| EXPECT_LT((*it)->use_date(), k115DaysAgo); |
| } |
| ASSERT_TRUE(it != shuffled_profiles.end()); |
| EXPECT_GT(profile->use_date(), k115DaysAgo); |
| ++it; |
| } |
| for (; it != shuffled_profiles.end(); ++it) { |
| EXPECT_LT((*it)->use_date(), k115DaysAgo); |
| } |
| |
| // Validate the histograms. |
| histogram_tester.ExpectTotalCount(kAddressesSuppressedHistogramName, 1); |
| histogram_tester.ExpectBucketCount(kAddressesSuppressedHistogramName, 6, 1); |
| } |
| |
| // Verify all profiles are removed if they're all disused. |
| { |
| // Create a working copy of the profile pointers. |
| std::vector<AutofillProfile*> profiles(all_profile_ptrs); |
| |
| // Filter the profiles while capturing histograms. |
| base::HistogramTester histogram_tester; |
| suggestion_selection::RemoveProfilesNotUsedSinceTimestamp( |
| kCurrentTime + base::TimeDelta::FromDays(1), &profiles); |
| |
| // Validate that we get the expected filtered profiles and histograms. |
| EXPECT_TRUE(profiles.empty()); |
| histogram_tester.ExpectTotalCount(kAddressesSuppressedHistogramName, 1); |
| histogram_tester.ExpectBucketCount(kAddressesSuppressedHistogramName, |
| kNumProfiles, 1); |
| } |
| |
| // Verify all profiles are retained if they're sufficiently recently used. |
| { |
| // Create a working copy of the profile pointers. |
| std::vector<AutofillProfile*> profiles(all_profile_ptrs); |
| |
| // Filter the profiles while capturing histograms. |
| base::HistogramTester histogram_tester; |
| suggestion_selection::RemoveProfilesNotUsedSinceTimestamp( |
| kCurrentTime - |
| base::TimeDelta::FromDays(2 * kNumProfiles * k30Days.InDays()), |
| &profiles); |
| |
| // Validate that we get the expected filtered profiles and histograms. |
| EXPECT_EQ(all_profile_ptrs, profiles); |
| histogram_tester.ExpectTotalCount(kAddressesSuppressedHistogramName, 1); |
| histogram_tester.ExpectBucketCount(kAddressesSuppressedHistogramName, 0, 1); |
| } |
| } |
| |
| } // namespace suggestion_selection |
| } // namespace autofill |