| // Copyright 2023 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "components/plus_addresses/plus_address_service_impl.h" |
| |
| #include <optional> |
| #include <string> |
| #include <string_view> |
| #include <vector> |
| |
| #include "base/functional/callback_forward.h" |
| #include "base/functional/callback_helpers.h" |
| #include "base/memory/scoped_refptr.h" |
| #include "base/metrics/field_trial_params.h" |
| #include "base/metrics/histogram_base.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "base/test/gmock_callback_support.h" |
| #include "base/test/gmock_expected_support.h" |
| #include "base/test/metrics/histogram_tester.h" |
| #include "base/test/metrics/user_action_tester.h" |
| #include "base/test/mock_callback.h" |
| #include "base/test/scoped_feature_list.h" |
| #include "base/test/task_environment.h" |
| #include "base/test/test_future.h" |
| #include "base/time/time.h" |
| #include "base/types/cxx23_to_underlying.h" |
| #include "components/affiliations/core/browser/affiliation_utils.h" |
| #include "components/affiliations/core/browser/mock_affiliation_service.h" |
| #include "components/autofill/core/browser/autofill_field.h" |
| #include "components/autofill/core/browser/field_types.h" |
| #include "components/autofill/core/browser/filling/filling_product.h" |
| #include "components/autofill/core/browser/integrators/autofill_plus_address_delegate.h" |
| #include "components/autofill/core/browser/integrators/password_form_classification.h" |
| #include "components/autofill/core/browser/suggestions/suggestion.h" |
| #include "components/autofill/core/browser/suggestions/suggestion_hiding_reason.h" |
| #include "components/autofill/core/browser/suggestions/suggestion_test_helpers.h" |
| #include "components/autofill/core/browser/test_utils/autofill_test_utils.h" |
| #include "components/autofill/core/common/aliases.h" |
| #include "components/autofill/core/common/autofill_test_utils.h" |
| #include "components/autofill/core/common/form_data.h" |
| #include "components/autofill/core/common/form_field_data.h" |
| #include "components/autofill/core/common/html_field_types.h" |
| #include "components/os_crypt/async/browser/test_utils.h" |
| #include "components/plus_addresses/blocked_facets.pb.h" |
| #include "components/plus_addresses/features.h" |
| #include "components/plus_addresses/grit/plus_addresses_strings.h" |
| #include "components/plus_addresses/plus_address_blocklist_data.h" |
| #include "components/plus_addresses/plus_address_hats_utils.h" |
| #include "components/plus_addresses/plus_address_http_client_impl.h" |
| #include "components/plus_addresses/plus_address_preallocator.h" |
| #include "components/plus_addresses/plus_address_prefs.h" |
| #include "components/plus_addresses/plus_address_suggestion_generator.h" |
| #include "components/plus_addresses/plus_address_test_environment.h" |
| #include "components/plus_addresses/plus_address_test_utils.h" |
| #include "components/plus_addresses/plus_address_types.h" |
| #include "components/plus_addresses/settings/fake_plus_address_setting_service.h" |
| #include "components/plus_addresses/webdata/plus_address_sync_util.h" |
| #include "components/plus_addresses/webdata/plus_address_table.h" |
| #include "components/plus_addresses/webdata/plus_address_webdata_service.h" |
| #include "components/prefs/pref_service.h" |
| #include "components/signin/public/base/consent_level.h" |
| #include "components/signin/public/identity_manager/access_token_info.h" |
| #include "components/signin/public/identity_manager/account_info.h" |
| #include "components/signin/public/identity_manager/identity_manager.h" |
| #include "components/signin/public/identity_manager/identity_test_environment.h" |
| #include "components/webdata/common/web_database.h" |
| #include "components/webdata/common/web_database_backend.h" |
| #include "components/webdata/common/web_database_service.h" |
| #include "net/http/http_status_code.h" |
| #include "services/data_decoder/public/cpp/test_support/in_process_data_decoder.h" |
| #include "services/network/public/cpp/shared_url_loader_factory.h" |
| #include "services/network/public/cpp/weak_wrapper_shared_url_loader_factory.h" |
| #include "services/network/test/test_url_loader_factory.h" |
| #include "testing/gmock/include/gmock/gmock.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| #include "ui/base/l10n/l10n_util.h" |
| #include "url/gurl.h" |
| #include "url/origin.h" |
| |
| namespace plus_addresses { |
| namespace { |
| |
| using SuggestionEvent = autofill::AutofillPlusAddressDelegate::SuggestionEvent; |
| using affiliations::FacetURI; |
| using autofill::AutofillSuggestionTriggerSource; |
| using autofill::EqualsSuggestion; |
| using autofill::FormData; |
| using autofill::FormFieldData; |
| using autofill::PasswordFormClassification; |
| using autofill::Suggestion; |
| using autofill::SuggestionType; |
| using base::Bucket; |
| using base::BucketsAre; |
| using base::test::RunOnceCallback; |
| using test::CreatePreallocatedPlusAddress; |
| using test::IsSingleCreatePlusAddressSuggestion; |
| using test::IsSingleFillPlusAddressSuggestion; |
| using ::testing::_; |
| using ::testing::AllOf; |
| using ::testing::ElementsAre; |
| using ::testing::Eq; |
| using ::testing::Field; |
| using ::testing::InSequence; |
| using ::testing::IsEmpty; |
| using ::testing::MockFunction; |
| using ::testing::NiceMock; |
| using ::testing::Pair; |
| using ::testing::UnorderedElementsAre; |
| |
| constexpr std::string_view kPlusAddressSuggestionMetric = |
| "PlusAddresses.Suggestion.Events"; |
| |
| constexpr char kPlusAddress[] = "plus+remote@plus.plus"; |
| |
| MATCHER_P(IsPreallocatedPlusAddress, address, "") { |
| if (!arg.is_dict()) { |
| return false; |
| } |
| const base::Value::Dict& d = arg.GetDict(); |
| const std::string* plus_address = |
| d.FindString(PlusAddressPreallocator::kPlusAddressKey); |
| return plus_address && *plus_address == address; |
| } |
| |
| MATCHER_P(IsCreateInlineSuggestion, has_proposed_address, "") { |
| if (arg.type != SuggestionType::kCreateNewPlusAddressInline) { |
| return false; |
| } |
| return arg.template GetPayload<Suggestion::PlusAddressPayload>() |
| .address.has_value() == has_proposed_address; |
| } |
| |
| url::Origin OriginFromFacet(const affiliations::FacetURI& facet) { |
| return url::Origin::Create(GURL(facet.canonical_spec())); |
| } |
| |
| class MockPlusAddressServiceObserver : public PlusAddressService::Observer { |
| public: |
| MockPlusAddressServiceObserver() = default; |
| ~MockPlusAddressServiceObserver() override = default; |
| MOCK_METHOD(void, |
| OnPlusAddressesChanged, |
| (const std::vector<PlusAddressDataChange>&), |
| (override)); |
| MOCK_METHOD(void, OnPlusAddressServiceShutdown, (), (override)); |
| }; |
| |
| class PlusAddressServiceTest : public ::testing::Test { |
| public: |
| PlusAddressServiceTest() |
| : test_shared_loader_factory_( |
| base::MakeRefCounted<network::WeakWrapperSharedURLLoaderFactory>( |
| &test_url_loader_factory_)) { |
| InitService(); |
| } |
| |
| std::vector<Suggestion> FetchPlusAddressSuggestions( |
| const url::Origin& origin, |
| bool is_off_the_record, |
| const PasswordFormClassification& focused_form_classification, |
| const FormData& form, |
| const FormFieldData& focused_field, |
| autofill::AutofillSuggestionTriggerSource trigger_source) { |
| // Empty psl extension by default. |
| ON_CALL(affiliation_service(), GetPSLExtensions) |
| .WillByDefault(RunOnceCallback<0>(std::vector<std::string>())); |
| |
| affiliations::GroupedFacets group; |
| group.facets.emplace_back( |
| FacetURI::FromPotentiallyInvalidSpec(origin.Serialize())); |
| ON_CALL(affiliation_service(), GetGroupingInfo) |
| .WillByDefault(RunOnceCallback<1>( |
| std::vector<affiliations::GroupedFacets>{group})); |
| |
| base::MockCallback<base::OnceCallback<void(std::vector<std::string>)>> |
| callback; |
| std::vector<std::string> affiliated_plus_addresses; |
| base::RunLoop run_loop; |
| ON_CALL(callback, Run) |
| .WillByDefault([&](std::vector<std::string> plus_addresses) { |
| affiliated_plus_addresses = std::move(plus_addresses); |
| run_loop.Quit(); |
| }); |
| service().GetAffiliatedPlusAddresses(origin, callback.Get()); |
| run_loop.Quit(); |
| |
| return service().GetSuggestionsFromPlusAddresses( |
| affiliated_plus_addresses, origin, is_off_the_record, form, |
| focused_field, |
| /*form_field_type_groups=*/{}, focused_form_classification, |
| trigger_source); |
| } |
| |
| protected: |
| // Constants that cannot be created at compile time: |
| const url::Origin kNoSubdomainOrigin = |
| url::Origin::Create(GURL("https://test.example")); |
| |
| affiliations::MockAffiliationService& affiliation_service() { |
| return plus_environment_.affiliation_service(); |
| } |
| |
| signin::IdentityTestEnvironment& identity_env() { |
| return plus_environment_.identity_env(); |
| } |
| signin::IdentityManager* identity_manager() { |
| return identity_env().identity_manager(); |
| } |
| PrefService& pref_service() { return plus_environment_.pref_service(); } |
| PlusAddressServiceImpl& service() { return *service_; } |
| FakePlusAddressSettingService& setting_service() { |
| return plus_environment_.setting_service(); |
| } |
| const scoped_refptr<network::SharedURLLoaderFactory>& |
| shared_loader_factory() { |
| return test_shared_loader_factory_; |
| } |
| base::test::TaskEnvironment& task_environment() { return task_environment_; } |
| network::TestURLLoaderFactory& url_loader_factory() { |
| return test_url_loader_factory_; |
| } |
| |
| // Forces (re-)initialization of the `PlusAddressService`, which can be useful |
| // when classes override feature parameters. |
| void InitService() { |
| service_.emplace(&plus_environment_.pref_service(), identity_manager(), |
| &setting_service(), |
| std::make_unique<PlusAddressHttpClientImpl>( |
| identity_manager(), shared_loader_factory()), |
| /*webdata_service=*/nullptr, |
| /*affiliation_service=*/ |
| &affiliation_service(), |
| /*feature_enabled_for_profile_check=*/ |
| base::BindRepeating(&base::FeatureList::IsEnabled)); |
| } |
| |
| private: |
| base::test::TaskEnvironment task_environment_{ |
| base::test::TaskEnvironment::TimeSource::MOCK_TIME}; |
| test::PlusAddressTestEnvironment plus_environment_; |
| network::TestURLLoaderFactory test_url_loader_factory_; |
| scoped_refptr<network::SharedURLLoaderFactory> test_shared_loader_factory_; |
| data_decoder::test::InProcessDataDecoder decoder_; |
| std::optional<PlusAddressServiceImpl> service_; |
| }; |
| |
| TEST_F(PlusAddressServiceTest, BasicTest) { |
| const PlusProfile profile = test::CreatePlusProfile(); |
| EXPECT_FALSE(service().IsPlusAddress(*profile.plus_address)); |
| service().SavePlusProfile(profile); |
| EXPECT_TRUE(service().IsPlusAddress(*profile.plus_address)); |
| EXPECT_EQ(service().GetPlusAddress(profile.facet), profile.plus_address); |
| EXPECT_EQ(service().GetPlusAddress(affiliations::FacetURI()), std::nullopt); |
| EXPECT_EQ(service().GetPlusProfile(profile.facet)->plus_address, |
| profile.plus_address); |
| } |
| |
| TEST_F(PlusAddressServiceTest, MatchesPlusAddressFormat) { |
| EXPECT_FALSE(service().MatchesPlusAddressFormat(u"invalid_email")); |
| EXPECT_FALSE(service().MatchesPlusAddressFormat(u"asd@foo.com")); |
| EXPECT_TRUE(service().MatchesPlusAddressFormat(u"asd@grelay.com")); |
| } |
| |
| TEST_F(PlusAddressServiceTest, GetPlusProfileByFacet) { |
| const PlusProfile profile = test::CreatePlusProfile(); |
| EXPECT_FALSE(service().IsPlusAddress(*profile.plus_address)); |
| service().SavePlusProfile(profile); |
| EXPECT_TRUE(service().IsPlusAddress(*profile.plus_address)); |
| EXPECT_EQ( |
| service().GetPlusProfile( |
| affiliations::FacetURI::FromPotentiallyInvalidSpec("invalid facet")), |
| std::nullopt); |
| EXPECT_EQ(service().GetPlusProfile(profile.facet), profile); |
| } |
| |
| TEST_F(PlusAddressServiceTest, DefaultShouldShowManualFallbackState) { |
| EXPECT_FALSE(service().IsPlusAddressFillingEnabled(kNoSubdomainOrigin)); |
| EXPECT_FALSE(service().IsPlusAddressCreationEnabled( |
| kNoSubdomainOrigin, /*is_off_the_record=*/false)); |
| // By default, the `ShouldShowManualFallback` function should return `false`. |
| EXPECT_FALSE(service().ShouldShowManualFallback(kNoSubdomainOrigin, |
| /*is_off_the_record=*/false)); |
| } |
| |
| // Ensure `ShouldShowManualFallback` is false without a server URL. |
| TEST_F(PlusAddressServiceTest, ShouldShowManualFallbackNoServer) { |
| // Enable the feature, but do not provide a server URL, which indicates no |
| // suggestion should be shown. |
| base::test::ScopedFeatureList scoped_feature_list{ |
| features::kPlusAddressesEnabled}; |
| InitService(); |
| EXPECT_FALSE(service().IsPlusAddressFillingEnabled(kNoSubdomainOrigin)); |
| EXPECT_FALSE(service().IsPlusAddressCreationEnabled( |
| kNoSubdomainOrigin, /*is_off_the_record=*/false)); |
| EXPECT_FALSE(service().ShouldShowManualFallback(kNoSubdomainOrigin, |
| /*is_off_the_record=*/false)); |
| } |
| |
| TEST_F(PlusAddressServiceTest, IsEligibleForPlusAddress) { |
| base::test::ScopedFeatureList scoped_feature_list{ |
| features::kPlusAddressSuggestionsOnUsernameFields}; |
| autofill::AutofillField field; |
| InitService(); |
| |
| // Address form with an email field is eligible. |
| field.set_heuristic_type(autofill::GetActiveHeuristicSource(), |
| autofill::FieldType::EMAIL_ADDRESS); |
| EXPECT_TRUE(service().IsFieldEligibleForPlusAddress(field)); |
| |
| // Password forms with fields that have server predictions USERNAME, |
| // SINGLE_USERNAME and heuristic type EMAIL_ADDRESS, should be eligible for |
| // plus addresses. |
| field = autofill::AutofillField(); |
| field.set_server_predictions( |
| {autofill::test::CreateFieldPrediction(autofill::FieldType::USERNAME)}); |
| field.set_heuristic_type(autofill::GetActiveHeuristicSource(), |
| autofill::FieldType::EMAIL_ADDRESS); |
| EXPECT_TRUE(service().IsFieldEligibleForPlusAddress(field)); |
| |
| field = autofill::AutofillField(); |
| field.set_server_predictions({autofill::test::CreateFieldPrediction( |
| autofill::FieldType::SINGLE_USERNAME)}); |
| field.set_heuristic_type(autofill::GetActiveHeuristicSource(), |
| autofill::FieldType::EMAIL_ADDRESS); |
| EXPECT_TRUE(service().IsFieldEligibleForPlusAddress(field)); |
| |
| // SINGLE_USERNAME_FORGOT_PASSWORD fields are not supported. |
| field = autofill::AutofillField(); |
| field.set_server_predictions({autofill::test::CreateFieldPrediction( |
| autofill::FieldType::SINGLE_USERNAME_FORGOT_PASSWORD)}); |
| field.set_heuristic_type(autofill::GetActiveHeuristicSource(), |
| autofill::FieldType::EMAIL_ADDRESS); |
| EXPECT_FALSE(service().IsFieldEligibleForPlusAddress(field)); |
| |
| // Heuristic type needs to be EMAIL_ADDRESS. |
| field = autofill::AutofillField(); |
| field.set_server_predictions( |
| {autofill::test::CreateFieldPrediction(autofill::FieldType::USERNAME)}); |
| field.set_heuristic_type(autofill::GetActiveHeuristicSource(), |
| autofill::FieldType::USERNAME); |
| EXPECT_FALSE(service().IsFieldEligibleForPlusAddress(field)); |
| } |
| |
| TEST_F(PlusAddressServiceTest, NoAccountPlusAddressCreation) { |
| base::test::TestFuture<const PlusProfileOrError&> future; |
| service().ReservePlusAddress(kNoSubdomainOrigin, future.GetCallback()); |
| EXPECT_THAT(future.Get(), base::test::ErrorIs(PlusAddressRequestError( |
| PlusAddressRequestErrorType::kUserSignedOut))); |
| |
| future.Clear(); |
| service().ConfirmPlusAddress(kNoSubdomainOrigin, PlusAddress(kPlusAddress), |
| future.GetCallback()); |
| EXPECT_THAT(future.Get(), base::test::ErrorIs(PlusAddressRequestError( |
| PlusAddressRequestErrorType::kUserSignedOut))); |
| } |
| |
| TEST_F(PlusAddressServiceTest, AbortPlusAddressCreation) { |
| const std::string invalid_email = "plus"; |
| identity_env().MakeAccountAvailable(invalid_email, |
| {signin::ConsentLevel::kSignin}); |
| InitService(); |
| |
| base::test::TestFuture<const PlusProfileOrError&> future; |
| service().ReservePlusAddress(kNoSubdomainOrigin, future.GetCallback()); |
| EXPECT_THAT(future.Get(), base::test::ErrorIs(PlusAddressRequestError( |
| PlusAddressRequestErrorType::kUserSignedOut))); |
| |
| future.Clear(); |
| service().ConfirmPlusAddress(kNoSubdomainOrigin, PlusAddress(kPlusAddress), |
| future.GetCallback()); |
| EXPECT_THAT(future.Get(), base::test::ErrorIs(PlusAddressRequestError( |
| PlusAddressRequestErrorType::kUserSignedOut))); |
| } |
| |
| // Tests that GetPlusProfiles returns all cached plus profiles. |
| TEST_F(PlusAddressServiceTest, GetPlusProfiles) { |
| PlusProfile profile1 = test::CreatePlusProfile(); |
| PlusProfile profile2 = test::CreatePlusProfile2(); |
| service().SavePlusProfile(profile1); |
| service().SavePlusProfile(profile2); |
| |
| EXPECT_THAT(service().GetPlusProfiles(), |
| UnorderedElementsAre(profile1, profile2)); |
| } |
| |
| // Tests the PlusAddressService ability to make network requests. |
| class PlusAddressServiceRequestsTest : public PlusAddressServiceTest { |
| public: |
| explicit PlusAddressServiceRequestsTest() |
| : kPlusProfilesEndpoint( |
| kServerUrl.Resolve(kServerPlusProfileEndpoint).spec()), |
| kReservePlusAddressEndpoint( |
| kServerUrl.Resolve(kServerReservePlusAddressEndpoint).spec()), |
| kCreatePlusAddressEndpoint( |
| kServerUrl.Resolve(kServerCreatePlusAddressEndpoint).spec()) { |
| scoped_feature_list_.InitAndEnableFeatureWithParameters( |
| features::kPlusAddressesEnabled, GetFieldTrialParams()); |
| identity_env().MakeAccountAvailable(kSigninAccount, |
| {signin::ConsentLevel::kSignin}); |
| identity_env().SetAutomaticIssueOfAccessTokens(true); |
| InitService(); |
| } |
| |
| protected: |
| static constexpr std::string_view kSigninAccount = "plus@plus.plus"; |
| |
| // Constants that cannot be created at compile time: |
| const GURL kServerUrl = GURL("https://server.example"); |
| const std::string kPlusProfilesEndpoint; |
| const std::string kReservePlusAddressEndpoint; |
| const std::string kCreatePlusAddressEndpoint; |
| |
| base::FieldTrialParams GetFieldTrialParams() const { |
| return {{"server-url", kServerUrl.spec()}, |
| {"oauth-scope", "scope.example"}}; |
| } |
| |
| private: |
| base::test::ScopedFeatureList scoped_feature_list_; |
| }; |
| |
| TEST_F(PlusAddressServiceRequestsTest, ReservePlusAddress_ReturnsUnconfirmed) { |
| PlusProfile profile = test::CreatePlusProfile(); |
| base::test::TestFuture<const PlusProfileOrError&> future; |
| service().ReservePlusAddress(OriginFromFacet(profile.facet), |
| future.GetCallback()); |
| |
| // Check that the future callback is still blocked, and unblock it. |
| profile.is_confirmed = false; |
| ASSERT_FALSE(future.IsReady()); |
| url_loader_factory().SimulateResponseForPendingRequest( |
| kReservePlusAddressEndpoint, test::MakeCreationResponse(profile)); |
| ASSERT_TRUE(future.Wait()); |
| EXPECT_EQ(future.Get()->plus_address, profile.plus_address); |
| |
| // The service should not save plus_address if it hasn't been confirmed yet. |
| EXPECT_FALSE(service().IsPlusAddress(*profile.plus_address)); |
| } |
| |
| TEST_F(PlusAddressServiceRequestsTest, ReservePlusAddress_ReturnsConfirmed) { |
| PlusProfile profile = test::CreatePlusProfile(); |
| base::test::TestFuture<const PlusProfileOrError&> future; |
| service().ReservePlusAddress(OriginFromFacet(profile.facet), |
| future.GetCallback()); |
| |
| // Check that the future callback is still blocked, and unblock it. |
| ASSERT_FALSE(future.IsReady()); |
| url_loader_factory().SimulateResponseForPendingRequest( |
| kReservePlusAddressEndpoint, test::MakeCreationResponse(profile)); |
| ASSERT_TRUE(future.Wait()); |
| EXPECT_EQ(future.Get()->plus_address, profile.plus_address); |
| |
| // The service should save kPlusAddress if it has already been confirmed. |
| EXPECT_TRUE(service().IsPlusAddress(*profile.plus_address)); |
| } |
| |
| TEST_F(PlusAddressServiceRequestsTest, ReservePlusAddress_Fails) { |
| base::test::TestFuture<const PlusProfileOrError&> future; |
| service().ReservePlusAddress(kNoSubdomainOrigin, future.GetCallback()); |
| |
| // Check that the future callback is still blocked, and unblock it. |
| ASSERT_FALSE(future.IsReady()); |
| url_loader_factory().SimulateResponseForPendingRequest( |
| kReservePlusAddressEndpoint, "", net::HTTP_BAD_REQUEST); |
| ASSERT_TRUE(future.Wait()); |
| EXPECT_FALSE(future.Get().has_value()); |
| } |
| |
| TEST_F(PlusAddressServiceRequestsTest, ConfirmPlusAddress_Successful) { |
| const PlusProfile& profile = test::CreatePlusProfile(); |
| MockPlusAddressServiceObserver observer; |
| service().AddObserver(&observer); |
| EXPECT_CALL(observer, |
| OnPlusAddressesChanged(ElementsAre(PlusAddressDataChange( |
| PlusAddressDataChange::Type::kAdd, profile)))); |
| base::test::TestFuture<const PlusProfileOrError&> future; |
| service().ConfirmPlusAddress(OriginFromFacet(profile.facet), |
| profile.plus_address, future.GetCallback()); |
| |
| // Check that the future callback is still blocked, and unblock it. |
| ASSERT_FALSE(future.IsReady()); |
| url_loader_factory().SimulateResponseForPendingRequest( |
| kCreatePlusAddressEndpoint, test::MakeCreationResponse(profile)); |
| ASSERT_TRUE(future.Wait()); |
| EXPECT_EQ(future.Get()->plus_address, profile.plus_address); |
| // Verify that the kPlusAddress is saved when confirmation is successful. |
| EXPECT_TRUE(service().IsPlusAddress(*profile.plus_address)); |
| |
| // Assert that ensuing calls to the same facet do not make a network request. |
| base::test::TestFuture<const PlusProfileOrError&> second_future; |
| service().ConfirmPlusAddress(OriginFromFacet(profile.facet), |
| profile.plus_address, |
| second_future.GetCallback()); |
| ASSERT_TRUE(second_future.Wait()); |
| EXPECT_EQ(second_future.Get()->plus_address, profile.plus_address); |
| service().RemoveObserver(&observer); |
| } |
| |
| TEST_F(PlusAddressServiceRequestsTest, ConfirmPlusAddress_Fails) { |
| ASSERT_FALSE(service().IsPlusAddress(kPlusAddress)); |
| |
| base::test::TestFuture<const PlusProfileOrError&> future; |
| service().ConfirmPlusAddress(kNoSubdomainOrigin, PlusAddress(kPlusAddress), |
| future.GetCallback()); |
| |
| // Check that the future callback is still blocked, and unblock it. |
| ASSERT_FALSE(future.IsReady()); |
| url_loader_factory().SimulateResponseForPendingRequest( |
| kCreatePlusAddressEndpoint, "", net::HTTP_BAD_REQUEST); |
| ASSERT_TRUE(future.Wait()); |
| |
| // An error is propagated from the callback and kPlusAddress is not saved. |
| EXPECT_FALSE(future.Get().has_value()); |
| EXPECT_FALSE(service().IsPlusAddress(kPlusAddress)); |
| } |
| |
| // Doesn't run on ChromeOS since ClearPrimaryAccount() doesn't exist for it. |
| #if !BUILDFLAG(IS_CHROMEOS) |
| TEST_F(PlusAddressServiceRequestsTest, |
| PrimaryAccountCleared_TogglesPlusAddressCreationOff) { |
| // Toggle creation off by removing the primary account. |
| identity_env().ClearPrimaryAccount(); |
| |
| // Verify that Plus Address creation doesn't occur. |
| PlusProfile profile = test::CreatePlusProfile(); |
| profile.is_confirmed = false; |
| base::test::TestFuture<const PlusProfileOrError&> reserve; |
| service().ReservePlusAddress(OriginFromFacet(profile.facet), |
| reserve.GetCallback()); |
| ASSERT_TRUE(reserve.Wait()); |
| base::test::TestFuture<const PlusProfileOrError&> confirm; |
| service().ConfirmPlusAddress(OriginFromFacet(profile.facet), |
| profile.plus_address, confirm.GetCallback()); |
| ASSERT_TRUE(confirm.Wait()); |
| EXPECT_EQ(url_loader_factory().NumPending(), 0); |
| |
| // Toggle creation back on by signing in again. |
| identity_env().MakePrimaryAccountAvailable("plus@plus.plus", |
| signin::ConsentLevel::kSignin); |
| |
| // Verify that Plus Address creation occurs and makes a network request. |
| reserve.Clear(); |
| service().ReservePlusAddress(OriginFromFacet(profile.facet), |
| reserve.GetCallback()); |
| EXPECT_EQ(url_loader_factory().NumPending(), 1); |
| url_loader_factory().SimulateResponseForPendingRequest( |
| kReservePlusAddressEndpoint, test::MakeCreationResponse(profile)); |
| EXPECT_EQ(reserve.Get()->plus_address, profile.plus_address); |
| |
| confirm.Clear(); |
| service().ConfirmPlusAddress(OriginFromFacet(profile.facet), |
| profile.plus_address, confirm.GetCallback()); |
| EXPECT_EQ(url_loader_factory().NumPending(), 1); |
| profile.is_confirmed = true; |
| url_loader_factory().SimulateResponseForPendingRequest( |
| kCreatePlusAddressEndpoint, test::MakeCreationResponse(profile)); |
| EXPECT_EQ(confirm.Get()->plus_address, profile.plus_address); |
| } |
| #endif // !BUILDFLAG(IS_CHROMEOS) |
| |
| // Tests that if an account error happens while a server request is ongoing, |
| // the request ends in an error and the eventual server response is ignored. |
| TEST_F(PlusAddressServiceRequestsTest, |
| PrimaryRefreshTokenError_ResetsHttpRequests) { |
| PlusProfile profile = test::CreatePlusProfile(); |
| base::test::TestFuture<const PlusProfileOrError&> future; |
| service().ReservePlusAddress(OriginFromFacet(profile.facet), |
| future.GetCallback()); |
| |
| // Check that the future callback is still blocked, and unblock it. |
| ASSERT_FALSE(future.IsReady()); |
| |
| // Simulate an auth error happening while the server response is still |
| // pending. |
| const CoreAccountInfo primary_account = |
| identity_manager()->GetPrimaryAccountInfo(signin::ConsentLevel::kSignin); |
| identity_env().UpdatePersistentErrorOfRefreshTokenForAccount( |
| primary_account.account_id, |
| GoogleServiceAuthError(GoogleServiceAuthError::INVALID_GAIA_CREDENTIALS)); |
| |
| // The auth change calls the callback with an error. |
| ASSERT_TRUE(future.Wait()); |
| EXPECT_THAT(future.Get(), base::test::ErrorIs(PlusAddressRequestError( |
| PlusAddressRequestErrorType::kUserSignedOut))); |
| |
| // Nothing happens once the server responds. |
| url_loader_factory().SimulateResponseForPendingRequest( |
| kReservePlusAddressEndpoint, test::MakeCreationResponse(profile)); |
| EXPECT_THAT(service().GetPlusProfiles(), IsEmpty()); |
| } |
| |
| TEST_F(PlusAddressServiceRequestsTest, |
| PrimaryRefreshTokenError_TogglesPlusAddressCreationOff) { |
| CoreAccountInfo primary_account = |
| identity_manager()->GetPrimaryAccountInfo(signin::ConsentLevel::kSignin); |
| |
| // Toggle creation off by triggering an error for the primary refresh token. |
| identity_env().UpdatePersistentErrorOfRefreshTokenForAccount( |
| primary_account.account_id, |
| GoogleServiceAuthError(GoogleServiceAuthError::INVALID_GAIA_CREDENTIALS)); |
| |
| // Verify that Plus Address creation doesn't occur. |
| PlusProfile profile = test::CreatePlusProfile(); |
| service().ReservePlusAddress(OriginFromFacet(profile.facet), |
| base::DoNothing()); |
| service().ConfirmPlusAddress(OriginFromFacet(profile.facet), |
| profile.plus_address, base::DoNothing()); |
| EXPECT_EQ(url_loader_factory().NumPending(), 0); |
| |
| // Toggle creation back on by removing the error. |
| identity_env().UpdatePersistentErrorOfRefreshTokenForAccount( |
| primary_account.account_id, |
| GoogleServiceAuthError(GoogleServiceAuthError::NONE)); |
| |
| // Verify that Plus Address creation occurs and makes a network request. |
| base::test::TestFuture<const PlusProfileOrError&> reserve; |
| service().ReservePlusAddress(OriginFromFacet(profile.facet), |
| reserve.GetCallback()); |
| EXPECT_EQ(url_loader_factory().NumPending(), 1); |
| profile.is_confirmed = false; |
| url_loader_factory().SimulateResponseForPendingRequest( |
| kReservePlusAddressEndpoint, test::MakeCreationResponse(profile)); |
| EXPECT_EQ(reserve.Get()->plus_address, profile.plus_address); |
| |
| base::test::TestFuture<const PlusProfileOrError&> confirm; |
| service().ConfirmPlusAddress(OriginFromFacet(profile.facet), |
| profile.plus_address, confirm.GetCallback()); |
| EXPECT_EQ(url_loader_factory().NumPending(), 1); |
| profile.is_confirmed = true; |
| url_loader_factory().SimulateResponseForPendingRequest( |
| kCreatePlusAddressEndpoint, test::MakeCreationResponse(profile)); |
| EXPECT_EQ(confirm.Get()->plus_address, profile.plus_address); |
| } |
| |
| // Tests that ongoing network requests are cancelled on signout. |
| #if !BUILDFLAG(IS_CHROMEOS) |
| TEST_F(PlusAddressServiceRequestsTest, OngoingRequestsCancelledOnSignout) { |
| base::test::TestFuture<const PlusProfileOrError&> future; |
| service().ReservePlusAddress(kNoSubdomainOrigin, future.GetCallback()); |
| EXPECT_FALSE(future.IsReady()); |
| |
| EXPECT_EQ(url_loader_factory().NumPending(), 1); |
| identity_env().ClearPrimaryAccount(); |
| EXPECT_EQ(url_loader_factory().NumPending(), 0); |
| ASSERT_TRUE(future.Wait()); |
| EXPECT_EQ(future.Get(), base::unexpected(PlusAddressRequestError( |
| PlusAddressRequestErrorType::kUserSignedOut))); |
| } |
| #endif // !BUILDFLAG(IS_CHROMEOS) |
| |
| #if !BUILDFLAG(IS_ANDROID) && !BUILDFLAG(IS_IOS) |
| // Tests that if an inline suggestion without a proposed address is shown, then |
| // a new reserve request is sent and the address updated on its completion. |
| TEST_F(PlusAddressServiceRequestsTest, |
| OnShowedInlineSuggestionWithoutProposedAddress) { |
| base::HistogramTester histogram_tester; |
| base::test::TestFuture<std::vector<Suggestion>, |
| AutofillSuggestionTriggerSource> |
| callback; |
| |
| Suggestion inline_suggestion(SuggestionType::kCreateNewPlusAddressInline); |
| inline_suggestion.payload = Suggestion::PlusAddressPayload(); |
| std::vector<Suggestion> current_suggestions = {std::move(inline_suggestion)}; |
| service().OnShowedInlineSuggestion( |
| url::Origin::Create(GURL("https://foo.com")), current_suggestions, |
| callback.GetCallback()); |
| histogram_tester.ExpectUniqueSample( |
| kPlusAddressSuggestionMetric, |
| SuggestionEvent::kCreateNewPlusAddressInlineReserveLoadingStateShown, 1); |
| |
| PlusProfile profile = test::CreatePlusProfile(); |
| profile.is_confirmed = false; |
| url_loader_factory().SimulateResponseForPendingRequest( |
| kReservePlusAddressEndpoint, test::MakeCreationResponse(profile)); |
| ASSERT_TRUE(callback.Wait()); |
| EXPECT_THAT( |
| callback.Get<0>(), |
| ElementsAre(IsCreateInlineSuggestion(/*has_proposed_address=*/true))); |
| } |
| |
| // Tests that an error suggestion is shown if the reserve call times out. |
| TEST_F(PlusAddressServiceRequestsTest, |
| OnShowedInlineSuggestionWithReserveError) { |
| base::HistogramTester histogram_tester; |
| base::MockCallback<PlusAddressService::UpdateSuggestionsCallback> callback; |
| |
| EXPECT_CALL( |
| callback, |
| Run(ElementsAre( |
| PlusAddressSuggestionGenerator::GetPlusAddressErrorSuggestion( |
| PlusAddressRequestError::AsNetworkError( |
| net::HTTP_REQUEST_TIMEOUT))), |
| AutofillSuggestionTriggerSource:: |
| kPlusAddressUpdatedInBrowserProcess)); |
| |
| Suggestion inline_suggestion(SuggestionType::kCreateNewPlusAddressInline); |
| inline_suggestion.payload = Suggestion::PlusAddressPayload(); |
| std::vector<Suggestion> current_suggestions = {std::move(inline_suggestion)}; |
| service().OnShowedInlineSuggestion( |
| url::Origin::Create(GURL("https://foo.com")), current_suggestions, |
| callback.Get()); |
| |
| PlusProfile profile = test::CreatePlusProfile(); |
| profile.is_confirmed = false; |
| url_loader_factory().SimulateResponseForPendingRequest( |
| kReservePlusAddressEndpoint, "", net::HTTP_REQUEST_TIMEOUT); |
| using enum SuggestionEvent; |
| EXPECT_THAT( |
| histogram_tester.GetAllSamples(kPlusAddressSuggestionMetric), |
| BucketsAre( |
| Bucket(base::to_underlying( |
| kCreateNewPlusAddressInlineReserveLoadingStateShown), |
| 1), |
| Bucket(base::to_underlying(kErrorDuringReserve), 1))); |
| } |
| |
| // Tests that if an inline suggestion with a proposed address is shown, no |
| // additional address is reserved. |
| TEST_F(PlusAddressServiceRequestsTest, |
| OnShowedInlineSuggestionWithProposedAddress) { |
| base::HistogramTester histogram_tester; |
| base::UserActionTester user_action_tester; |
| base::MockCallback<PlusAddressService::UpdateSuggestionsCallback> callback; |
| |
| EXPECT_CALL(callback, Run).Times(0); |
| |
| Suggestion inline_suggestion(SuggestionType::kCreateNewPlusAddressInline); |
| inline_suggestion.payload = Suggestion::PlusAddressPayload(u"foo@moo.com"); |
| std::vector<Suggestion> current_suggestions = {std::move(inline_suggestion)}; |
| service().OnShowedInlineSuggestion( |
| url::Origin::Create(GURL("https://foo.com")), current_suggestions, |
| callback.Get()); |
| |
| histogram_tester.ExpectUniqueSample( |
| kPlusAddressSuggestionMetric, |
| SuggestionEvent::kCreateNewPlusAddressInlineSuggested, 1); |
| EXPECT_EQ( |
| user_action_tester.GetActionCount("PlusAddresses.CreateSuggestionShown"), |
| 1); |
| EXPECT_EQ(url_loader_factory().NumPending(), 0); |
| } |
| |
| // Tests that if an inline suggestion is accepted, a server call to the create |
| // endpoint is made. On success, the popup is hidden and the plus address is |
| // filled. |
| TEST_F(PlusAddressServiceRequestsTest, OnAcceptedInlineSuggestion) { |
| base::HistogramTester histogram_tester; |
| base::UserActionTester user_action_tester; |
| base::test::TestFuture<std::vector<Suggestion>, |
| AutofillSuggestionTriggerSource> |
| update_callback; |
| base::test::TestFuture<autofill::SuggestionHidingReason> hide_callback; |
| base::test::TestFuture<const std::string&> fill_callback; |
| |
| // Simulate the scenario when the user has already created 2 other plus |
| // addresses. This is relevant only for the HaTS survey triggering |
| // verification. |
| service().SavePlusProfile(test::CreatePlusProfileWithFacet( |
| FacetURI::FromPotentiallyInvalidSpec("https://example1.com"))); |
| service().SavePlusProfile(test::CreatePlusProfileWithFacet( |
| FacetURI::FromPotentiallyInvalidSpec("https://example2.com"))); |
| |
| PlusProfile profile = test::CreatePlusProfile(); |
| |
| Suggestion inline_suggestion(SuggestionType::kCreateNewPlusAddressInline); |
| inline_suggestion.payload = |
| Suggestion::PlusAddressPayload(base::UTF8ToUTF16(*profile.plus_address)); |
| std::vector<Suggestion> current_suggestions = {std::move(inline_suggestion)}; |
| |
| service().OnAcceptedInlineSuggestion( |
| url::Origin::Create(GURL("https://foo.com")), current_suggestions, |
| /*current_suggestion_index=*/0, update_callback.GetCallback(), |
| hide_callback.GetCallback(), fill_callback.GetCallback(), |
| /*show_affiliation_error_dialog=*/base::DoNothing(), |
| /*show_error_dialog=*/base::DoNothing(), |
| /*reshow_suggestions=*/base::DoNothing()); |
| |
| histogram_tester.ExpectUniqueSample( |
| kPlusAddressSuggestionMetric, |
| SuggestionEvent::kCreateNewPlusAddressInlineChosen, 1); |
| EXPECT_EQ(user_action_tester.GetActionCount( |
| "PlusAddresses.OfferedPlusAddressAccepted"), |
| 1); |
| url_loader_factory().SimulateResponseForPendingRequest( |
| kCreatePlusAddressEndpoint, test::MakeCreationResponse(profile)); |
| |
| ASSERT_TRUE(update_callback.Wait()); |
| EXPECT_THAT(update_callback.Get<0>(), ElementsAre(IsCreateInlineSuggestion( |
| /*has_proposed_address=*/true))); |
| EXPECT_THAT( |
| update_callback.Get<1>(), |
| AutofillSuggestionTriggerSource::kPlusAddressUpdatedInBrowserProcess); |
| |
| ASSERT_TRUE(fill_callback.Wait()); |
| EXPECT_THAT(fill_callback.Get(), Eq(*profile.plus_address)); |
| ASSERT_TRUE(hide_callback.Wait()); |
| EXPECT_THAT(hide_callback.Get(), |
| Eq(autofill::SuggestionHidingReason::kAcceptSuggestion)); |
| } |
| |
| // Tests that when the server call to create a plus address from an inline |
| // suggestion returns with an affiliation error, a call is made to show an error |
| // dialog. |
| TEST_F(PlusAddressServiceRequestsTest, |
| OnAcceptedInlineSuggestionAffiliationError) { |
| base::test::TestFuture<std::vector<Suggestion>, |
| AutofillSuggestionTriggerSource> |
| update_callback; |
| base::test::TestFuture<autofill::SuggestionHidingReason> hide_callback; |
| base::test::TestFuture<std::u16string, std::u16string> |
| show_affiliation_error_callback; |
| |
| // Simulate the scenario when the user has already created 2 other plus |
| // addresses. This is relevant only for the HaTS survey triggering |
| // verification. |
| service().SavePlusProfile(test::CreatePlusProfileWithFacet( |
| FacetURI::FromPotentiallyInvalidSpec("https://example1.com"))); |
| service().SavePlusProfile(test::CreatePlusProfileWithFacet( |
| FacetURI::FromPotentiallyInvalidSpec("https://example2.com"))); |
| |
| PlusProfile profile = test::CreatePlusProfile(); |
| PlusProfile affiliated_profile = test::CreatePlusProfile2(); |
| |
| Suggestion inline_suggestion(SuggestionType::kCreateNewPlusAddressInline); |
| inline_suggestion.payload = |
| Suggestion::PlusAddressPayload(base::UTF8ToUTF16(*profile.plus_address)); |
| std::vector<Suggestion> current_suggestions = {std::move(inline_suggestion)}; |
| |
| service().OnAcceptedInlineSuggestion( |
| url::Origin::Create(GURL("https://foo.com")), current_suggestions, |
| /*current_suggestion_index=*/0, update_callback.GetCallback(), |
| hide_callback.GetCallback(), /*fill_field_callback=*/base::DoNothing(), |
| show_affiliation_error_callback.GetCallback(), |
| /*show_error_dialog=*/base::DoNothing(), |
| /*reshow_suggestions=*/base::DoNothing()); |
| |
| url_loader_factory().SimulateResponseForPendingRequest( |
| kCreatePlusAddressEndpoint, |
| test::MakeCreationResponse(affiliated_profile)); |
| |
| ASSERT_TRUE(update_callback.Wait()); |
| EXPECT_THAT(update_callback.Get<0>(), ElementsAre(IsCreateInlineSuggestion( |
| /*has_proposed_address=*/true))); |
| EXPECT_THAT( |
| update_callback.Get<1>(), |
| AutofillSuggestionTriggerSource::kPlusAddressUpdatedInBrowserProcess); |
| |
| ASSERT_TRUE(hide_callback.Wait()); |
| EXPECT_THAT(hide_callback.Get(), |
| autofill::SuggestionHidingReason::kAcceptSuggestion); |
| |
| ASSERT_TRUE(show_affiliation_error_callback.Wait()); |
| EXPECT_THAT(show_affiliation_error_callback.Get<0>(), Eq(u"bar.com")); |
| EXPECT_THAT(show_affiliation_error_callback.Get<1>(), |
| Eq(base::UTF8ToUTF16(*affiliated_profile.plus_address))); |
| } |
| |
| // Tests that when the server call to create a plus address from an inline |
| // suggestion returns with a HTTP_REQUEST_TIMEOUT error, a call is made to show |
| // an error dialog that allows trying again |
| TEST_F(PlusAddressServiceRequestsTest, OnAcceptedInlineSuggestionTimeoutError) { |
| base::MockCallback<PlusAddressService::UpdateSuggestionsCallback> |
| update_callback; |
| base::MockCallback<PlusAddressService::HideSuggestionsCallback> hide_callback; |
| base::MockCallback<PlusAddressService::ShowErrorDialogCallback> |
| show_error_callback; |
| base::MockCallback<base::OnceClosure> reshow_callback; |
| |
| // Simulate the scenario when the user has already created 2 other plus |
| // addresses. This is relevant only for the HaTS survey triggering |
| // verification. |
| service().SavePlusProfile(test::CreatePlusProfileWithFacet( |
| FacetURI::FromPotentiallyInvalidSpec("https://example1.com"))); |
| service().SavePlusProfile(test::CreatePlusProfileWithFacet( |
| FacetURI::FromPotentiallyInvalidSpec("https://example2.com"))); |
| |
| PlusProfile profile = test::CreatePlusProfile(); |
| PlusProfile affiliated_profile = test::CreatePlusProfile2(); |
| |
| Suggestion inline_suggestion(SuggestionType::kCreateNewPlusAddressInline); |
| inline_suggestion.payload = |
| Suggestion::PlusAddressPayload(base::UTF8ToUTF16(*profile.plus_address)); |
| std::vector<Suggestion> current_suggestions = {std::move(inline_suggestion)}; |
| |
| MockFunction<void()> check; |
| { |
| InSequence s; |
| |
| EXPECT_CALL(update_callback, Run(ElementsAre(IsCreateInlineSuggestion( |
| /*has_proposed_address=*/true)), |
| AutofillSuggestionTriggerSource:: |
| kPlusAddressUpdatedInBrowserProcess)); |
| EXPECT_CALL(check, Call); |
| EXPECT_CALL(hide_callback, |
| Run(autofill::SuggestionHidingReason::kAcceptSuggestion)); |
| // Simulate accepting by running the callback. |
| EXPECT_CALL( |
| show_error_callback, |
| Run(PlusAddressService::PlusAddressErrorDialogType::kTimeout, _)) |
| .WillOnce(RunOnceCallback<1>()); |
| EXPECT_CALL(reshow_callback, Run); |
| } |
| service().OnAcceptedInlineSuggestion( |
| url::Origin::Create(GURL("https://foo.com")), current_suggestions, |
| /*current_suggestion_index=*/0, update_callback.Get(), |
| hide_callback.Get(), |
| /*fill_field_callback=*/base::DoNothing(), |
| /*show_affiliation_error_dialog=*/base::DoNothing(), |
| show_error_callback.Get(), reshow_callback.Get()); |
| check.Call(); |
| |
| url_loader_factory().SimulateResponseForPendingRequest( |
| kCreatePlusAddressEndpoint, "", net::HTTP_REQUEST_TIMEOUT); |
| } |
| #endif // !BUILDFLAG(IS_ANDROID) && !BUILDFLAG(IS_IOS) |
| |
| TEST_F(PlusAddressServiceRequestsTest, GetPlusAddressHatsData_PrefsNotSet) { |
| std::map<std::string, std::string> hats_data = |
| service().GetPlusAddressHatsData(); |
| EXPECT_THAT(hats_data, |
| UnorderedElementsAre( |
| Pair(hats::kPlusAddressesCount, std::string("0")), |
| Pair(hats::kFirstPlusAddressCreationTime, std::string("-1")), |
| Pair(hats::kLastPlusAddressFillingTime, std::string("-1")))); |
| } |
| |
| TEST_F(PlusAddressServiceRequestsTest, GetPlusAddressHatsData_PrefsSet) { |
| const PlusProfile profile1 = test::CreatePlusProfile(); |
| const PlusProfile profile2 = test::CreatePlusProfile2(); |
| service().SavePlusProfile(profile1); |
| service().SavePlusProfile(profile2); |
| |
| pref_service().SetTime(prefs::kFirstPlusAddressCreationTime, |
| base::Time::Now()); |
| pref_service().SetTime(prefs::kLastPlusAddressFillingTime, base::Time::Now()); |
| |
| task_environment().FastForwardBy(base::Seconds(100)); |
| |
| std::map<std::string, std::string> hats_data = |
| service().GetPlusAddressHatsData(); |
| EXPECT_THAT(hats_data, |
| UnorderedElementsAre( |
| Pair(hats::kPlusAddressesCount, std::string("2")), |
| Pair(hats::kFirstPlusAddressCreationTime, std::string("100")), |
| Pair(hats::kLastPlusAddressFillingTime, std::string("100")))); |
| } |
| |
| class PlusAddressServicePreAllocationTest |
| : public PlusAddressServiceRequestsTest { |
| public: |
| PlusAddressServicePreAllocationTest() { |
| preallocation_feature_.InitAndEnableFeatureWithParameters( |
| features::kPlusAddressPreallocation, |
| {{features::kPlusAddressPreallocationMinimumSize.name, "1"}}); |
| InitService(); |
| } |
| |
| const base::Value::List& GetPreallocatedAddresses() { |
| return pref_service().GetList(prefs::kPreallocatedAddresses); |
| } |
| void SetPreallocatedAddresses(base::Value::List addresses) { |
| pref_service().SetList(prefs::kPreallocatedAddresses, std::move(addresses)); |
| } |
| |
| private: |
| base::test::ScopedFeatureList preallocation_feature_; |
| }; |
| |
| // Tests that a successful plus address confirmation removes the pre-allocated |
| // email from the pre-allocated pool of addresses. |
| TEST_F(PlusAddressServicePreAllocationTest, |
| ConfirmationRemovesAllocatedPlusAddress) { |
| const base::Time kFuture = base::Time::Now() + base::Days(1); |
| const std::string kPlusAddress1 = "plus1@plus.com"; |
| const std::string kPlusAddress2 = "plus2@plus.com"; |
| const auto kOrigin = url::Origin::Create(GURL("https://foo.com")); |
| SetPreallocatedAddresses( |
| base::Value::List() |
| .Append(CreatePreallocatedPlusAddress(kFuture, kPlusAddress1)) |
| .Append(CreatePreallocatedPlusAddress(kFuture, kPlusAddress2))); |
| |
| base::test::TestFuture<const PlusProfileOrError&> reserve; |
| service().ReservePlusAddress(kOrigin, reserve.GetCallback()); |
| ASSERT_TRUE(reserve.Get().has_value()); |
| PlusProfile profile = *reserve.Get(); |
| EXPECT_EQ(profile.plus_address.value(), kPlusAddress1); |
| |
| // Simulate a response. |
| profile.is_confirmed = true; |
| profile.profile_id = "123"; |
| base::test::TestFuture<const PlusProfileOrError&> confirm; |
| service().ConfirmPlusAddress(kOrigin, profile.plus_address, |
| confirm.GetCallback()); |
| ASSERT_TRUE(url_loader_factory().SimulateResponseForPendingRequest( |
| kCreatePlusAddressEndpoint, test::MakeCreationResponse(profile))); |
| EXPECT_TRUE(confirm.Get().has_value()); |
| |
| EXPECT_THAT(GetPreallocatedAddresses(), |
| ElementsAre(IsPreallocatedPlusAddress(kPlusAddress2))); |
| } |
| |
| // Tests that communication with `PlusAddressTable` works. |
| class PlusAddressServiceWebDataTest : public ::testing::Test { |
| protected: |
| PlusAddressServiceWebDataTest() |
| : os_crypt_(os_crypt_async::GetTestOSCryptAsyncForTesting( |
| /*is_sync_for_unittests=*/true)) { |
| // Create an in-memory PlusAddressTable fully operating on the UI sequence. |
| webdatabase_service_ = base::MakeRefCounted<WebDatabaseService>( |
| base::FilePath(WebDatabase::kInMemoryPath), |
| base::SingleThreadTaskRunner::GetCurrentDefault(), |
| base::SingleThreadTaskRunner::GetCurrentDefault()); |
| webdatabase_service_->AddTable(std::make_unique<PlusAddressTable>()); |
| webdatabase_service_->LoadDatabase(os_crypt_.get()); |
| plus_webdata_service_ = base::MakeRefCounted<PlusAddressWebDataService>( |
| webdatabase_service_, |
| base::SingleThreadTaskRunner::GetCurrentDefault()); |
| plus_webdata_service_->Init(base::DoNothing()); |
| // Even though `PlusAddressTable` operates on the UI sequence in this test, |
| // it is still implemented using `PostTask()`. |
| task_environment_.RunUntilIdle(); |
| // Initialize the `service_` using the `plus_webdata_service_`. |
| service_.emplace( |
| &plus_environment_.pref_service(), identity_manager(), |
| &plus_environment_.setting_service(), |
| std::make_unique<PlusAddressHttpClientImpl>( |
| /*identity_manager=*/identity_manager(), |
| /*url_loader_factory=*/nullptr), |
| plus_webdata_service_, |
| /*affiliation_service=*/&plus_environment_.affiliation_service(), |
| /*feature_enabled_for_profile_check=*/ |
| base::BindRepeating(&base::FeatureList::IsEnabled)); |
| } |
| |
| signin::IdentityManager* identity_manager() { |
| return plus_environment_.identity_env().identity_manager(); |
| } |
| |
| PlusAddressServiceImpl& service() { return *service_; } |
| |
| PlusAddressTable& table() { |
| return *PlusAddressTable::FromWebDatabase( |
| webdatabase_service_->GetBackend()->database()); |
| } |
| |
| private: |
| base::test::TaskEnvironment task_environment_; |
| test::PlusAddressTestEnvironment plus_environment_; |
| std::unique_ptr<os_crypt_async::OSCryptAsync> os_crypt_; |
| scoped_refptr<WebDatabaseService> webdatabase_service_; |
| scoped_refptr<PlusAddressWebDataService> plus_webdata_service_; |
| // Except briefly during initialisation, it always has a value. |
| std::optional<PlusAddressServiceImpl> service_; |
| }; |
| |
| TEST_F(PlusAddressServiceWebDataTest, OnWebDataChangedBySync) { |
| const PlusProfile profile1 = test::CreatePlusProfile(); |
| const PlusProfile profile2 = test::CreatePlusProfile2(); |
| // Simulate adding and removing profiles to the database directly, as sync |
| // would. This triggers `OnWebDataChangedBySync()`. Prior to the notification, |
| // `service()` has no way of knowing about this data. |
| table().AddOrUpdatePlusProfile(profile1); |
| table().AddOrUpdatePlusProfile(profile2); |
| |
| service().SavePlusProfile(profile1); |
| EXPECT_THAT(service().GetPlusProfiles(), ElementsAre(profile1)); |
| |
| MockPlusAddressServiceObserver observer; |
| service().AddObserver(&observer); |
| // Simulate incoming changes from sync. Note that `profile1` already exists in |
| // the service and therefore should not be included as part of the updates |
| // sent to the `observer`. |
| EXPECT_CALL(observer, |
| OnPlusAddressesChanged(ElementsAre(PlusAddressDataChange( |
| PlusAddressDataChange::Type::kAdd, profile2)))); |
| service().OnWebDataChangedBySync( |
| {PlusAddressDataChange(PlusAddressDataChange::Type::kAdd, profile1), |
| PlusAddressDataChange(PlusAddressDataChange::Type::kAdd, profile2)}); |
| EXPECT_THAT(service().GetPlusProfiles(), |
| UnorderedElementsAre(profile1, profile2)); |
| |
| table().RemovePlusProfile(*profile1.profile_id); |
| std::vector<PlusAddressDataChange> remove_changes = { |
| PlusAddressDataChange(PlusAddressDataChange::Type::kRemove, profile1)}; |
| EXPECT_CALL(observer, OnPlusAddressesChanged(remove_changes)); |
| service().OnWebDataChangedBySync(remove_changes); |
| EXPECT_THAT(service().GetPlusProfiles(), UnorderedElementsAre(profile2)); |
| service().RemoveObserver(&observer); |
| } |
| |
| class PlusAddressServiceDisabledTest : public PlusAddressServiceTest { |
| protected: |
| PlusAddressServiceDisabledTest() { |
| scoped_feature_list_.InitAndDisableFeature(features::kPlusAddressesEnabled); |
| InitService(); |
| } |
| |
| private: |
| base::test::ScopedFeatureList scoped_feature_list_; |
| }; |
| |
| TEST_F(PlusAddressServiceDisabledTest, FeatureExplicitlyDisabled) { |
| // `ShouldShowManualFallback` should return `false`, even if there's a |
| // signed-in user. |
| identity_env().MakeAccountAvailable("plus@plus.plus", |
| {signin::ConsentLevel::kSignin}); |
| InitService(); |
| const url::Origin origin = url::Origin::Create(GURL("https://test.example")); |
| EXPECT_FALSE(service().IsPlusAddressFillingEnabled(origin)); |
| EXPECT_FALSE(service().IsPlusAddressCreationEnabled( |
| origin, /*is_off_the_record=*/false)); |
| EXPECT_FALSE( |
| service().ShouldShowManualFallback(origin, /*is_off_the_record=*/false)); |
| } |
| |
| class PlusAddressServiceEnabledTest : public PlusAddressServiceTest { |
| public: |
| PlusAddressServiceEnabledTest() { |
| scoped_feature_list_.InitAndEnableFeatureWithParameters( |
| features::kPlusAddressesEnabled, |
| {{features::kEnterprisePlusAddressServerUrl.name, "mattwashere"}}); |
| InitService(); |
| } |
| |
| private: |
| base::test::ScopedFeatureList scoped_feature_list_; |
| }; |
| |
| TEST_F(PlusAddressServiceEnabledTest, NoSignedInUser) { |
| EXPECT_FALSE(service().IsPlusAddressFillingEnabled(kNoSubdomainOrigin)); |
| EXPECT_FALSE(service().IsPlusAddressCreationEnabled( |
| kNoSubdomainOrigin, /*is_off_the_record=*/false)); |
| // Without a signed in user, the `ShouldShowManualFallback` should return |
| // `false`. |
| EXPECT_FALSE(service().ShouldShowManualFallback(kNoSubdomainOrigin, |
| /*is_off_the_record=*/false)); |
| } |
| |
| TEST_F(PlusAddressServiceEnabledTest, FullySupported) { |
| // With a signed in user, the `ShouldShowManualFallback` function should |
| // return `true`. |
| identity_env().MakeAccountAvailable("plus@plus.plus", |
| {signin::ConsentLevel::kSignin}); |
| InitService(); |
| EXPECT_TRUE(service().IsPlusAddressFillingEnabled(kNoSubdomainOrigin)); |
| EXPECT_TRUE(service().IsPlusAddressCreationEnabled( |
| kNoSubdomainOrigin, /*is_off_the_record=*/false)); |
| EXPECT_TRUE(service().ShouldShowManualFallback(kNoSubdomainOrigin, |
| /*is_off_the_record=*/false)); |
| } |
| |
| // Ensure filling is offered on both http and https domains. |
| TEST_F(PlusAddressServiceEnabledTest, FillingEnabledOnHttpAndHttps) { |
| identity_env().MakeAccountAvailable("plus@plus.plus", |
| {signin::ConsentLevel::kSignin}); |
| InitService(); |
| EXPECT_TRUE(service().IsPlusAddressFillingEnabled( |
| url::Origin::Create(GURL("https://test.example")))); |
| EXPECT_TRUE(service().IsPlusAddressFillingEnabled( |
| url::Origin::Create(GURL("http://test.example")))); |
| } |
| |
| // Ensure creation is not offered on http domains but it is on https domains. |
| TEST_F(PlusAddressServiceEnabledTest, CreationDisabledOnHttp) { |
| identity_env().MakeAccountAvailable("plus@plus.plus", |
| {signin::ConsentLevel::kSignin}); |
| InitService(); |
| EXPECT_TRUE(service().IsPlusAddressCreationEnabled( |
| url::Origin::Create(GURL("https://test.example")), |
| /*is_off_the_record=*/false)); |
| EXPECT_FALSE(service().IsPlusAddressCreationEnabled( |
| url::Origin::Create(GURL("http://test.example")), |
| /*is_off_the_record=*/false)); |
| } |
| |
| // Tests that the blocklist data is available and used to check for domain |
| // support in the plus address service. |
| TEST_F(PlusAddressServiceEnabledTest, BlocklistMechanism) { |
| identity_env().MakeAccountAvailable("plus@plus.plus", |
| {signin::ConsentLevel::kSignin}); |
| InitService(); |
| CompactPlusAddressBlockedFacets blocked_facets; |
| blocked_facets.set_exclusion_pattern( |
| "\\.forbidden\\.com$|\\.disallowed\\.com$"); |
| blocked_facets.set_exception_pattern("exclude\\.forbidden\\.com$"); |
| plus_addresses::PlusAddressBlocklistData::GetInstance() |
| .PopulateDataFromComponent(blocked_facets.SerializeAsString()); |
| |
| // Verify that a url that is not on the excluded site continues to work. |
| EXPECT_TRUE(service().ShouldShowManualFallback( |
| url::Origin::Create(GURL("https://www.allowed.com")), |
| /*is_off_the_record=*/false)); |
| |
| // Sites matching the excluded pattern are not supported. |
| EXPECT_FALSE(service().ShouldShowManualFallback( |
| url::Origin::Create(GURL("https://www.forbidden.com")), |
| /*is_off_the_record=*/false)); |
| EXPECT_FALSE(service().ShouldShowManualFallback( |
| url::Origin::Create(GURL("https://www.example.disallowed.com")), |
| /*is_off_the_record=*/false)); |
| |
| // Sites matching the exception pattern are supported. |
| EXPECT_TRUE(service().ShouldShowManualFallback( |
| url::Origin::Create(GURL("https://exclude.forbidden.com")), |
| /*is_off_the_record=*/false)); |
| } |
| |
| // `ShouldShowManualFallback` returns false when `origin` scheme is not http or |
| // https. |
| TEST_F(PlusAddressServiceEnabledTest, NonHTTPSchemesAreNotSupported) { |
| identity_env().MakeAccountAvailable("plus@plus.plus", |
| {signin::ConsentLevel::kSignin}); |
| InitService(); |
| EXPECT_TRUE(service().ShouldShowManualFallback(kNoSubdomainOrigin, |
| /*is_off_the_record=*/false)); |
| |
| const url::Origin different_scheme = |
| url::Origin::Create(GURL("other://hello")); |
| EXPECT_FALSE(service().IsPlusAddressFillingEnabled(different_scheme)); |
| EXPECT_FALSE(service().IsPlusAddressCreationEnabled( |
| different_scheme, /*is_off_the_record=*/false)); |
| EXPECT_FALSE(service().ShouldShowManualFallback(different_scheme, |
| /*is_off_the_record=*/false)); |
| } |
| |
| // `ShouldShowManualFallback` returns false when `origin` is opaque. |
| TEST_F(PlusAddressServiceEnabledTest, OpaqueOriginIsNotSupported) { |
| identity_env().MakeAccountAvailable("plus@plus.plus", |
| {signin::ConsentLevel::kSignin}); |
| InitService(); |
| EXPECT_FALSE(service().IsPlusAddressFillingEnabled(url::Origin())); |
| EXPECT_FALSE(service().IsPlusAddressCreationEnabled( |
| url::Origin(), /*is_off_the_record=*/false)); |
| EXPECT_FALSE(service().ShouldShowManualFallback(url::Origin(), false)); |
| } |
| |
| TEST_F(PlusAddressServiceEnabledTest, OTRWithNoExistingAddress) { |
| // With a signed in user, an off-the-record session, and no existing address, |
| // the `ShouldShowManualFallback` function should return `false`. |
| identity_env().MakeAccountAvailable("plus@plus.plus", |
| {signin::ConsentLevel::kSignin}); |
| InitService(); |
| EXPECT_TRUE(service().IsPlusAddressFillingEnabled(kNoSubdomainOrigin)); |
| EXPECT_FALSE(service().IsPlusAddressCreationEnabled( |
| kNoSubdomainOrigin, /*is_off_the_record=*/true)); |
| EXPECT_FALSE(service().ShouldShowManualFallback(kNoSubdomainOrigin, |
| /*is_off_the_record=*/true)); |
| } |
| |
| TEST_F(PlusAddressServiceEnabledTest, OTRWithExistingAddress) { |
| // With a signed in user, an off-the-record session, and an existing address, |
| // the `ShouldShowManualFallback` function should return `true`. |
| identity_env().MakeAccountAvailable("plus@plus.plus", |
| {signin::ConsentLevel::kSignin}); |
| InitService(); |
| |
| const PlusProfile profile = test::CreatePlusProfile(); |
| service().SavePlusProfile(profile); |
| EXPECT_TRUE( |
| service().IsPlusAddressFillingEnabled(OriginFromFacet(profile.facet))); |
| EXPECT_FALSE(service().IsPlusAddressCreationEnabled( |
| OriginFromFacet(profile.facet), /*is_off_the_record=*/true)); |
| EXPECT_TRUE(service().ShouldShowManualFallback(OriginFromFacet(profile.facet), |
| /*is_off_the_record=*/true)); |
| } |
| |
| TEST_F(PlusAddressServiceEnabledTest, GlobalSettingsToggleOff) { |
| identity_env().MakeAccountAvailable("plus@plus.plus", |
| {signin::ConsentLevel::kSignin}); |
| InitService(); |
| setting_service().set_is_plus_addresses_enabled(false); |
| EXPECT_TRUE(service().IsPlusAddressFillingEnabled(kNoSubdomainOrigin)); |
| EXPECT_FALSE(service().IsPlusAddressCreationEnabled( |
| kNoSubdomainOrigin, /*is_off_the_record=*/false)); |
| EXPECT_FALSE(service().ShouldShowManualFallback(kNoSubdomainOrigin, |
| /*is_off_the_record=*/false)); |
| } |
| |
| TEST_F(PlusAddressServiceEnabledTest, |
| GlobalSettingsToggleOffButTheUserHasPlusAddress) { |
| identity_env().MakeAccountAvailable("plus@plus.plus", |
| {signin::ConsentLevel::kSignin}); |
| InitService(); |
| PlusProfile profile = test::CreatePlusProfile(); |
| profile.facet = |
| FacetURI::FromPotentiallyInvalidSpec(kNoSubdomainOrigin.GetURL().spec()); |
| service().SavePlusProfile(profile); |
| EXPECT_TRUE(service().GetPlusProfile(profile.facet)); |
| |
| setting_service().set_is_plus_addresses_enabled(false); |
| |
| EXPECT_TRUE(service().IsPlusAddressFillingEnabled(kNoSubdomainOrigin)); |
| EXPECT_FALSE(service().IsPlusAddressCreationEnabled( |
| kNoSubdomainOrigin, /*is_off_the_record=*/false)); |
| EXPECT_TRUE(service().ShouldShowManualFallback(kNoSubdomainOrigin, |
| /*is_off_the_record=*/false)); |
| } |
| |
| TEST_F(PlusAddressServiceEnabledTest, SignedOutGetEmail) { |
| EXPECT_EQ(service().GetPrimaryEmail(), std::nullopt); |
| } |
| |
| TEST_F(PlusAddressServiceEnabledTest, SignedInGetEmail) { |
| constexpr std::string_view expected_email = "plus@plus.plus"; |
| identity_env().MakeAccountAvailable(expected_email, |
| {signin::ConsentLevel::kSignin}); |
| InitService(); |
| |
| EXPECT_EQ(service().GetPrimaryEmail(), expected_email); |
| } |
| |
| // Tests that PlusAddresses is "disabled" in the following states: |
| // - When a primary account is unset after login. |
| // - When a primary account's refresh token has an auth error. |
| // |
| // If PlusAddressService is "disabled" it should stop offering the feature, |
| // clear any local storage, and not issue network requests. |
| class PlusAddressServiceSignoutTest : public PlusAddressServiceTest { |
| public: |
| PlusAddressServiceSignoutTest() { |
| scoped_feature_list_.InitAndEnableFeatureWithParameters( |
| features::kPlusAddressesEnabled, |
| {{features::kEnterprisePlusAddressServerUrl.name, "mattwashere"}, |
| {features::kEnterprisePlusAddressOAuthScope.name, "scope.example"}}); |
| secondary_account_ = identity_env().MakeAccountAvailable( |
| "beta@plus.plus", {signin::ConsentLevel::kSignin}); |
| primary_account_ = identity_env().MakePrimaryAccountAvailable( |
| "alpha@plus.plus", signin::ConsentLevel::kSignin); |
| InitService(); |
| } |
| |
| const CoreAccountInfo& primary_account() const { return primary_account_; } |
| const AccountInfo& secondary_account() const { return secondary_account_; } |
| |
| private: |
| base::test::ScopedFeatureList scoped_feature_list_; |
| CoreAccountInfo primary_account_; |
| AccountInfo secondary_account_; |
| }; |
| |
| // Doesn't run on ChromeOS since ClearPrimaryAccount() doesn't exist for it. |
| #if !BUILDFLAG(IS_CHROMEOS) |
| TEST_F(PlusAddressServiceSignoutTest, PrimaryAccountCleared_TogglesIsEnabled) { |
| ASSERT_TRUE(service().IsEnabled()); |
| |
| // Verify behaviors expected when service is enabled. |
| const PlusProfile profile = test::CreatePlusProfile(); |
| const url::Origin origin = OriginFromFacet(profile.facet); |
| service().SavePlusProfile(profile); |
| EXPECT_TRUE( |
| service().ShouldShowManualFallback(origin, /*is_off_the_record=*/false)); |
| EXPECT_TRUE(service().GetPlusAddress(profile.facet)); |
| EXPECT_EQ(service().GetPlusAddress(profile.facet).value(), |
| profile.plus_address); |
| EXPECT_TRUE(service().IsPlusAddress(*profile.plus_address)); |
| |
| identity_env().ClearPrimaryAccount(); |
| EXPECT_FALSE(service().IsEnabled()); |
| |
| // Ensure that the local data is cleared on disabling. |
| EXPECT_FALSE(service().ShouldShowManualFallback(origin, |
| /*is_off_the_record=*/false)); |
| } |
| #endif // !BUILDFLAG(IS_CHROMEOS) |
| |
| TEST_F(PlusAddressServiceSignoutTest, |
| PrimaryRefreshTokenError_TogglesIsEnabled) { |
| ASSERT_TRUE(service().IsEnabled()); |
| |
| // Verify behaviors expected when service is enabled. |
| const PlusProfile profile = test::CreatePlusProfile(); |
| const url::Origin origin = OriginFromFacet(profile.facet); |
| service().SavePlusProfile(profile); |
| EXPECT_TRUE( |
| service().ShouldShowManualFallback(origin, /*is_off_the_record=*/false)); |
| EXPECT_TRUE(service().GetPlusAddress(profile.facet)); |
| EXPECT_EQ(service().GetPlusAddress(profile.facet).value(), |
| profile.plus_address); |
| EXPECT_TRUE(service().IsPlusAddress(*profile.plus_address)); |
| |
| // Setting to NONE doesn't disable the service. |
| identity_env().UpdatePersistentErrorOfRefreshTokenForAccount( |
| primary_account().account_id, |
| GoogleServiceAuthError(GoogleServiceAuthError::NONE)); |
| EXPECT_TRUE(service().IsEnabled()); |
| |
| // The PlusAddressService isn't disabled for secondary account auth errors. |
| identity_env().UpdatePersistentErrorOfRefreshTokenForAccount( |
| secondary_account().account_id, |
| GoogleServiceAuthError(GoogleServiceAuthError::INVALID_GAIA_CREDENTIALS)); |
| EXPECT_TRUE(service().IsEnabled()); |
| |
| // Being in the "sync-paused" state results in this error. |
| identity_env().UpdatePersistentErrorOfRefreshTokenForAccount( |
| primary_account().account_id, |
| GoogleServiceAuthError(GoogleServiceAuthError::INVALID_GAIA_CREDENTIALS)); |
| EXPECT_FALSE(service().IsEnabled()); |
| |
| // Ensure that the local data is cleared on disabling. |
| EXPECT_FALSE( |
| service().ShouldShowManualFallback(origin, /*is_off_the_record=*/false)); |
| } |
| |
| // A test fixture with a `PlusAddressService` that is enabled to allow testing |
| // suggestion generation. |
| class PlusAddressSuggestionsTest : public PlusAddressServiceTest { |
| public: |
| PlusAddressSuggestionsTest() { |
| scoped_feature_list_.InitAndEnableFeatureWithParameters( |
| features::kPlusAddressesEnabled, |
| {{"server-url", "https://server.example"}, |
| {"oauth-scope", "scope.example"}}); |
| identity_env().MakePrimaryAccountAvailable("plus@plus.plus", |
| signin::ConsentLevel::kSignin); |
| identity_env().SetAutomaticIssueOfAccessTokens(true); |
| InitService(); |
| } |
| |
| private: |
| base::test::ScopedFeatureList scoped_feature_list_; |
| autofill::test::AutofillUnitTestEnvironment autofill_test_environment_; |
| }; |
| |
| // Tests that fill plus address suggestions are offered iff the value in the |
| // focused field matches the prefix of an existing plus address. |
| TEST_F(PlusAddressSuggestionsTest, SuggestionsForExistingPlusAddress) { |
| base::HistogramTester histogram_tester; |
| base::UserActionTester user_action_tester; |
| const PlusProfile profile = test::CreatePlusProfile(); |
| const url::Origin origin = OriginFromFacet(profile.facet); |
| service().SavePlusProfile(profile); |
| |
| // We offer filling if the field is empty. |
| FormFieldData focused_field; |
| FormData form; |
| form.set_fields({focused_field}); |
| EXPECT_THAT(FetchPlusAddressSuggestions( |
| origin, /*is_off_the_record=*/false, |
| PasswordFormClassification(), form, focused_field, |
| AutofillSuggestionTriggerSource::kFormControlElementClicked), |
| IsSingleFillPlusAddressSuggestion(*profile.plus_address)); |
| histogram_tester.ExpectUniqueSample( |
| kPlusAddressSuggestionMetric, |
| SuggestionEvent::kExistingPlusAddressSuggested, 1); |
| EXPECT_EQ(user_action_tester.GetActionCount( |
| "PlusAddresses.StandaloneFillSuggestionShown"), |
| 1); |
| // If the user types a letter and it matches the plus address (after |
| // normalization), the plus address continues to be offered. |
| focused_field.set_value(u"P"); |
| EXPECT_THAT(FetchPlusAddressSuggestions( |
| origin, /*is_off_the_record=*/false, |
| PasswordFormClassification(), form, focused_field, |
| AutofillSuggestionTriggerSource::kFormControlElementClicked), |
| IsSingleFillPlusAddressSuggestion(*profile.plus_address)); |
| histogram_tester.ExpectUniqueSample( |
| kPlusAddressSuggestionMetric, |
| SuggestionEvent::kExistingPlusAddressSuggested, 2); |
| EXPECT_EQ(user_action_tester.GetActionCount( |
| "PlusAddresses.StandaloneFillSuggestionShown"), |
| 2); |
| // If the value does not match the prefix of the plus address, nothing is |
| // shown. |
| focused_field.set_value(u"pp"); |
| EXPECT_THAT(FetchPlusAddressSuggestions( |
| origin, /*is_off_the_record=*/false, |
| PasswordFormClassification(), form, focused_field, |
| AutofillSuggestionTriggerSource::kFormControlElementClicked), |
| IsEmpty()); |
| histogram_tester.ExpectUniqueSample( |
| kPlusAddressSuggestionMetric, |
| SuggestionEvent::kExistingPlusAddressSuggested, 2); |
| } |
| |
| // Tests that fill plus address suggestions regardless of whether there is |
| // already text in the field if the trigger source was manual fallback. |
| TEST_F(PlusAddressSuggestionsTest, |
| SuggestionsForExistingPlusAddressWithManualFallback) { |
| base::HistogramTester histogram_tester; |
| base::UserActionTester user_action_tester; |
| const PlusProfile profile = test::CreatePlusProfile(); |
| const url::Origin origin = OriginFromFacet(profile.facet); |
| service().SavePlusProfile(profile); |
| |
| // We offer filling if the field is empty. |
| FormData form; |
| FormFieldData focused_field; |
| form.set_fields({focused_field}); |
| EXPECT_THAT( |
| FetchPlusAddressSuggestions( |
| origin, /*is_off_the_record=*/false, PasswordFormClassification(), |
| form, focused_field, |
| AutofillSuggestionTriggerSource::kManualFallbackPlusAddresses), |
| IsSingleFillPlusAddressSuggestion(*profile.plus_address)); |
| histogram_tester.ExpectUniqueSample( |
| kPlusAddressSuggestionMetric, |
| SuggestionEvent::kExistingPlusAddressSuggested, 1); |
| EXPECT_EQ(user_action_tester.GetActionCount( |
| "PlusAddresses.StandaloneFillSuggestionShown"), |
| 1); |
| // We also offer filling if the field is not empty and the prefix does not |
| // match the address. |
| focused_field.set_value(u"pp"); |
| EXPECT_THAT( |
| FetchPlusAddressSuggestions( |
| origin, /*is_off_the_record=*/false, PasswordFormClassification(), |
| form, focused_field, |
| AutofillSuggestionTriggerSource::kManualFallbackPlusAddresses), |
| IsSingleFillPlusAddressSuggestion(*profile.plus_address)); |
| histogram_tester.ExpectUniqueSample( |
| kPlusAddressSuggestionMetric, |
| SuggestionEvent::kExistingPlusAddressSuggested, 2); |
| EXPECT_EQ(user_action_tester.GetActionCount( |
| "PlusAddresses.StandaloneFillSuggestionShown"), |
| 2); |
| } |
| |
| // Tests that a create plus address suggestion is offered if there is no |
| // existing plus address for the domain and the field value is empty. |
| TEST_F(PlusAddressSuggestionsTest, SuggestionsForCreateNewPlusAddress) { |
| base::HistogramTester histogram_tester; |
| const auto origin = url::Origin::Create(GURL("https://foo.com")); |
| |
| // We offer creation if the field is empty. |
| FormData form; |
| FormFieldData focused_field; |
| form.set_fields({focused_field}); |
| EXPECT_THAT(FetchPlusAddressSuggestions( |
| origin, /*is_off_the_record=*/false, |
| PasswordFormClassification(), form, focused_field, |
| AutofillSuggestionTriggerSource::kFormControlElementClicked), |
| IsSingleCreatePlusAddressSuggestion()); |
| histogram_tester.ExpectUniqueSample( |
| kPlusAddressSuggestionMetric, |
| SuggestionEvent::kCreateNewPlusAddressSuggested, 1); |
| |
| // If the field value is not empty, nothing is shown. |
| focused_field.set_value(u"some text"); |
| EXPECT_THAT(FetchPlusAddressSuggestions( |
| origin, /*is_off_the_record=*/false, |
| PasswordFormClassification(), form, focused_field, |
| AutofillSuggestionTriggerSource::kFormControlElementClicked), |
| IsEmpty()); |
| histogram_tester.ExpectUniqueSample( |
| kPlusAddressSuggestionMetric, |
| SuggestionEvent::kCreateNewPlusAddressSuggested, 1); |
| } |
| |
| // Tests that a user action is recorded when a create plus address suggestion is |
| // shown to the user, and the user has never accepted the notice. |
| TEST_F(PlusAddressSuggestionsTest, |
| RecordCreateSuggestionUserActionFirstTimeNotice) { |
| base::HistogramTester histogram_tester; |
| base::UserActionTester user_action_tester; |
| setting_service().set_has_accepted_notice(false); |
| const auto origin = url::Origin::Create(GURL("https://foo.com")); |
| |
| // We offer creation if the field is empty. |
| FormData form; |
| FormFieldData focused_field; |
| form.set_fields({focused_field}); |
| FetchPlusAddressSuggestions( |
| origin, /*is_off_the_record=*/false, PasswordFormClassification(), form, |
| focused_field, |
| AutofillSuggestionTriggerSource::kFormControlElementClicked); |
| histogram_tester.ExpectUniqueSample( |
| kPlusAddressSuggestionMetric, |
| SuggestionEvent::kCreateNewPlusAddressSuggested, 1); |
| EXPECT_EQ(user_action_tester.GetActionCount( |
| "PlusAddresses.CreateSuggestionFirstTimeNoticeShown"), |
| 1); |
| } |
| |
| // Tests that a user action is recorded when a plus address suggestion fill is |
| // reported. |
| TEST_F(PlusAddressSuggestionsTest, RecordExistingPlusAddressChosenUserAction) { |
| base::UserActionTester user_action_tester; |
| service().RecordAutofillSuggestionEvent( |
| SuggestionEvent::kExistingPlusAddressChosen); |
| EXPECT_EQ(user_action_tester.GetActionCount( |
| "PlusAddresses.FillStandaloneSuggestionAccepted"), |
| 1); |
| } |
| |
| // Tests that a user action is recorded when a create plus address suggestion is |
| // shown to the user, and the user has already accepted the notice. |
| TEST_F(PlusAddressSuggestionsTest, RecordCreateSuggestionUserActionShown) { |
| base::HistogramTester histogram_tester; |
| base::UserActionTester user_action_tester; |
| setting_service().set_has_accepted_notice(true); |
| const auto origin = url::Origin::Create(GURL("https://foo.com")); |
| |
| // We offer creation if the field is empty. |
| FormData form; |
| FormFieldData focused_field; |
| form.set_fields({focused_field}); |
| FetchPlusAddressSuggestions( |
| origin, /*is_off_the_record=*/false, PasswordFormClassification(), form, |
| focused_field, |
| AutofillSuggestionTriggerSource::kFormControlElementClicked); |
| histogram_tester.ExpectUniqueSample( |
| kPlusAddressSuggestionMetric, |
| SuggestionEvent::kCreateNewPlusAddressSuggested, 1); |
| EXPECT_EQ( |
| user_action_tester.GetActionCount("PlusAddresses.CreateSuggestionShown"), |
| 1); |
| } |
| |
| // Tests that a user action is recorded when the user selects the plus address |
| // creation option. |
| TEST_F(PlusAddressSuggestionsTest, RecordCreateSuggestionUserActionChosen) { |
| base::UserActionTester user_action_tester; |
| service().RecordAutofillSuggestionEvent( |
| SuggestionEvent::kCreateNewPlusAddressChosen); |
| EXPECT_EQ(user_action_tester.GetActionCount( |
| "PlusAddresses.CreateSuggestionAccepted"), |
| 1); |
| } |
| |
| // Tests that a create plus address suggestion is offered regardless of the |
| // field's value if there is no existing plus address for the domain and the |
| // trigger source is a manual fallback. |
| TEST_F(PlusAddressSuggestionsTest, |
| SuggestionsForCreateNewPlusAddressWithManualFallback) { |
| base::HistogramTester histogram_tester; |
| const auto origin = url::Origin::Create(GURL("https://foo.com")); |
| |
| FormData form; |
| FormFieldData focused_field; |
| form.set_fields({focused_field}); |
| EXPECT_THAT( |
| FetchPlusAddressSuggestions( |
| origin, /*is_off_the_record=*/false, PasswordFormClassification(), |
| form, focused_field, |
| AutofillSuggestionTriggerSource::kManualFallbackPlusAddresses), |
| IsSingleCreatePlusAddressSuggestion()); |
| histogram_tester.ExpectUniqueSample( |
| kPlusAddressSuggestionMetric, |
| SuggestionEvent::kCreateNewPlusAddressSuggested, 1); |
| |
| focused_field.set_value(u"some text"); |
| EXPECT_THAT( |
| FetchPlusAddressSuggestions( |
| origin, /*is_off_the_record=*/false, PasswordFormClassification(), |
| form, focused_field, |
| AutofillSuggestionTriggerSource::kManualFallbackPlusAddresses), |
| IsSingleCreatePlusAddressSuggestion()); |
| histogram_tester.ExpectUniqueSample( |
| kPlusAddressSuggestionMetric, |
| SuggestionEvent::kCreateNewPlusAddressSuggested, 2); |
| } |
| |
| // Tests that no suggestions are returned when plus address are disabled. |
| TEST_F(PlusAddressSuggestionsTest, NoSuggestionsWhenDisabled) { |
| base::test::ScopedFeatureList feature_list; |
| feature_list.InitAndDisableFeature(features::kPlusAddressesEnabled); |
| |
| EXPECT_THAT(FetchPlusAddressSuggestions( |
| url::Origin::Create(GURL("https://foo.com")), |
| /*is_off_the_record=*/false, PasswordFormClassification(), |
| FormData(), FormFieldData(), |
| AutofillSuggestionTriggerSource::kFormControlElementClicked), |
| IsEmpty()); |
| } |
| |
| // Tests that the only password form on which create suggestions are offered on |
| // click is a signup form if the username field is the focused field, but that |
| // filling suggestions are always offered. |
| // TODO(crbug.com/322279583): Move to |
| // `plus_address_suggestion_generator_unittest`, since this should make it |
| // easier to test. |
| TEST_F(PlusAddressSuggestionsTest, SuggestionsOnPasswordFormsUsernameField) { |
| const PlusProfile profile = test::CreatePlusProfile(); |
| const url::Origin origin = OriginFromFacet(profile.facet); |
| auto get_suggestions_for_form_type = |
| [&](PasswordFormClassification::Type type) { |
| FormData form = autofill::test::CreateTestPasswordFormData(); |
| auto form_classification = PasswordFormClassification{ |
| .type = type, |
| .username_field = form.fields()[0].global_id(), |
| .password_field = form.fields()[1].global_id()}; |
| return FetchPlusAddressSuggestions( |
| origin, |
| /*is_off_the_record=*/false, form_classification, form, |
| form.fields()[0], |
| AutofillSuggestionTriggerSource::kFormControlElementClicked); |
| }; |
| |
| using enum PasswordFormClassification::Type; |
| EXPECT_THAT(get_suggestions_for_form_type(kLoginForm), IsEmpty()); |
| EXPECT_THAT(get_suggestions_for_form_type(kChangePasswordForm), IsEmpty()); |
| EXPECT_THAT(get_suggestions_for_form_type(kResetPasswordForm), IsEmpty()); |
| EXPECT_THAT(get_suggestions_for_form_type(kSingleUsernameForm), |
| IsSingleCreatePlusAddressSuggestion()); |
| EXPECT_THAT(get_suggestions_for_form_type(kSignupForm), |
| IsSingleCreatePlusAddressSuggestion()); |
| |
| service().SavePlusProfile(profile); |
| EXPECT_THAT(get_suggestions_for_form_type(kLoginForm), |
| IsSingleFillPlusAddressSuggestion(*profile.plus_address)); |
| EXPECT_THAT(get_suggestions_for_form_type(kChangePasswordForm), |
| IsSingleFillPlusAddressSuggestion(*profile.plus_address)); |
| EXPECT_THAT(get_suggestions_for_form_type(kResetPasswordForm), |
| IsSingleFillPlusAddressSuggestion(*profile.plus_address)); |
| EXPECT_THAT(get_suggestions_for_form_type(kSingleUsernameForm), |
| IsSingleFillPlusAddressSuggestion(*profile.plus_address)); |
| EXPECT_THAT(get_suggestions_for_form_type(kSignupForm), |
| IsSingleFillPlusAddressSuggestion(*profile.plus_address)); |
| } |
| |
| // Tests that creation is offered on all password forms if the focused field is |
| // not the username field. |
| TEST_F(PlusAddressSuggestionsTest, SuggestionsOnPasswordFormsNonUsernameField) { |
| base::test::ScopedFeatureList feature_list{ |
| features::kPlusAddressOfferCreationOnAllNonUsernameFields}; |
| |
| const PlusProfile profile = test::CreatePlusProfile(); |
| const url::Origin origin = OriginFromFacet(profile.facet); |
| auto get_suggestions_for_form_type = |
| [&](PasswordFormClassification::Type type) { |
| FormFieldData focused_field; |
| focused_field.set_host_frame(autofill::test::MakeLocalFrameToken()); |
| focused_field.set_renderer_id(autofill::test::MakeFieldRendererId()); |
| auto form_classification = PasswordFormClassification{ |
| .type = type, .username_field = focused_field.global_id()}; |
| focused_field.set_renderer_id( |
| autofill::FieldRendererId(focused_field.renderer_id().value() + 1)); |
| FormData form; |
| form.set_fields({focused_field}); |
| return FetchPlusAddressSuggestions( |
| origin, |
| /*is_off_the_record=*/false, form_classification, form, |
| focused_field, |
| AutofillSuggestionTriggerSource::kFormControlElementClicked); |
| }; |
| |
| using enum PasswordFormClassification::Type; |
| EXPECT_THAT(get_suggestions_for_form_type(kLoginForm), |
| IsSingleCreatePlusAddressSuggestion()); |
| EXPECT_THAT(get_suggestions_for_form_type(kChangePasswordForm), |
| IsSingleCreatePlusAddressSuggestion()); |
| EXPECT_THAT(get_suggestions_for_form_type(kResetPasswordForm), |
| IsSingleCreatePlusAddressSuggestion()); |
| EXPECT_THAT(get_suggestions_for_form_type(kSingleUsernameForm), |
| IsSingleCreatePlusAddressSuggestion()); |
| EXPECT_THAT(get_suggestions_for_form_type(kSignupForm), |
| IsSingleCreatePlusAddressSuggestion()); |
| } |
| |
| // Tests that plus address creation is offered on signup forms and single |
| // username forms even if the focused field is the username field. |
| TEST_F(PlusAddressSuggestionsTest, |
| SuggestionsOnPasswordFormWithSingleUsernameCreationEnabled) { |
| const PlusProfile profile = test::CreatePlusProfile(); |
| const url::Origin origin = OriginFromFacet(profile.facet); |
| auto get_suggestions_for_form_type = |
| [&](PasswordFormClassification::Type type) { |
| FormData form = autofill::test::CreateTestPasswordFormData(); |
| auto form_classification = PasswordFormClassification{ |
| .type = type, |
| .username_field = form.fields()[0].global_id(), |
| .password_field = form.fields()[1].global_id()}; |
| return FetchPlusAddressSuggestions( |
| origin, |
| /*is_off_the_record=*/false, form_classification, form, |
| form.fields()[0], |
| AutofillSuggestionTriggerSource::kFormControlElementClicked); |
| }; |
| using enum PasswordFormClassification::Type; |
| EXPECT_THAT(get_suggestions_for_form_type(kLoginForm), IsEmpty()); |
| EXPECT_THAT(get_suggestions_for_form_type(kChangePasswordForm), IsEmpty()); |
| EXPECT_THAT(get_suggestions_for_form_type(kResetPasswordForm), IsEmpty()); |
| EXPECT_THAT(get_suggestions_for_form_type(kSingleUsernameForm), |
| IsSingleCreatePlusAddressSuggestion()); |
| EXPECT_THAT(get_suggestions_for_form_type(kSignupForm), |
| IsSingleCreatePlusAddressSuggestion()); |
| |
| service().SavePlusProfile(profile); |
| EXPECT_THAT(get_suggestions_for_form_type(kLoginForm), |
| IsSingleFillPlusAddressSuggestion(*profile.plus_address)); |
| EXPECT_THAT(get_suggestions_for_form_type(kChangePasswordForm), |
| IsSingleFillPlusAddressSuggestion(*profile.plus_address)); |
| EXPECT_THAT(get_suggestions_for_form_type(kResetPasswordForm), |
| IsSingleFillPlusAddressSuggestion(*profile.plus_address)); |
| EXPECT_THAT(get_suggestions_for_form_type(kSingleUsernameForm), |
| IsSingleFillPlusAddressSuggestion(*profile.plus_address)); |
| EXPECT_THAT(get_suggestions_for_form_type(kSignupForm), |
| IsSingleFillPlusAddressSuggestion(*profile.plus_address)); |
| } |
| |
| // Tests that create suggestions are offered regardless of form type if the |
| // trigger source is a manual fallback. |
| TEST_F(PlusAddressSuggestionsTest, |
| SuggestionsOnPasswordFormsWithManualFallbacks) { |
| const PlusProfile profile = test::CreatePlusProfile(); |
| const url::Origin origin = OriginFromFacet(profile.facet); |
| auto get_suggestions_for_form_type = |
| [&](PasswordFormClassification::Type type) { |
| FormFieldData focused_field; |
| focused_field.set_host_frame(autofill::test::MakeLocalFrameToken()); |
| focused_field.set_renderer_id(autofill::test::MakeFieldRendererId()); |
| auto form_classification = PasswordFormClassification{ |
| .type = type, .username_field = focused_field.global_id()}; |
| FormData form; |
| form.set_fields({focused_field}); |
| return FetchPlusAddressSuggestions( |
| origin, |
| /*is_off_the_record=*/false, form_classification, form, |
| focused_field, |
| AutofillSuggestionTriggerSource::kManualFallbackPlusAddresses); |
| }; |
| |
| using enum PasswordFormClassification::Type; |
| EXPECT_THAT(get_suggestions_for_form_type(kLoginForm), |
| IsSingleCreatePlusAddressSuggestion()); |
| EXPECT_THAT(get_suggestions_for_form_type(kChangePasswordForm), |
| IsSingleCreatePlusAddressSuggestion()); |
| EXPECT_THAT(get_suggestions_for_form_type(kResetPasswordForm), |
| IsSingleCreatePlusAddressSuggestion()); |
| EXPECT_THAT(get_suggestions_for_form_type(kSingleUsernameForm), |
| IsSingleCreatePlusAddressSuggestion()); |
| EXPECT_THAT(get_suggestions_for_form_type(kSignupForm), |
| IsSingleCreatePlusAddressSuggestion()); |
| |
| service().SavePlusProfile(profile); |
| EXPECT_THAT(get_suggestions_for_form_type(kLoginForm), |
| IsSingleFillPlusAddressSuggestion(*profile.plus_address)); |
| EXPECT_THAT(get_suggestions_for_form_type(kChangePasswordForm), |
| IsSingleFillPlusAddressSuggestion(*profile.plus_address)); |
| EXPECT_THAT(get_suggestions_for_form_type(kResetPasswordForm), |
| IsSingleFillPlusAddressSuggestion(*profile.plus_address)); |
| EXPECT_THAT(get_suggestions_for_form_type(kSingleUsernameForm), |
| IsSingleFillPlusAddressSuggestion(*profile.plus_address)); |
| EXPECT_THAT(get_suggestions_for_form_type(kSignupForm), |
| IsSingleFillPlusAddressSuggestion(*profile.plus_address)); |
| } |
| |
| // Tests the content of the "Manage plus addresses..." suggestion. |
| TEST_F(PlusAddressSuggestionsTest, GetManagePlusAddressSuggestion) { |
| EXPECT_THAT(service().GetManagePlusAddressSuggestion(), |
| EqualsSuggestion(SuggestionType::kManagePlusAddress, |
| l10n_util::GetStringUTF16( |
| IDS_PLUS_ADDRESS_MANAGE_PLUS_ADDRESSES_TEXT), |
| Suggestion::Icon::kGoogleMonochrome)); |
| } |
| |
| // Tests that the last plus address usage time is recorded correctly. |
| TEST_F(PlusAddressSuggestionsTest, DidFillPlusAddress) { |
| service().DidFillPlusAddress(); |
| EXPECT_EQ(pref_service().GetTime(prefs::kLastPlusAddressFillingTime), |
| base::Time::Now()); |
| } |
| |
| #if !BUILDFLAG(IS_ANDROID) && !BUILDFLAG(IS_IOS) |
| TEST_F(PlusAddressSuggestionsTest, OnClickedRefreshInlineSuggestion) { |
| base::HistogramTester histogram_tester; |
| base::UserActionTester user_action_tester; |
| base::MockCallback<PlusAddressService::UpdateSuggestionsCallback> callback; |
| EXPECT_CALL(callback, |
| Run(ElementsAre(EqualsSuggestion( |
| SuggestionType::kCreateNewPlusAddressInline, |
| l10n_util::GetStringUTF16( |
| IDS_PLUS_ADDRESS_CREATE_SUGGESTION_MAIN_TEXT))), |
| AutofillSuggestionTriggerSource:: |
| kPlusAddressUpdatedInBrowserProcess)); |
| |
| std::vector<Suggestion> current_suggestions = { |
| Suggestion(SuggestionType::kCreateNewPlusAddressInline)}; |
| service().OnClickedRefreshInlineSuggestion( |
| url::Origin::Create(GURL("https://foo.com")), current_suggestions, |
| /*current_suggestion_index=*/0, callback.Get()); |
| histogram_tester.ExpectUniqueSample( |
| kPlusAddressSuggestionMetric, |
| SuggestionEvent::kRefreshPlusAddressInlineClicked, 1); |
| EXPECT_EQ(user_action_tester.GetActionCount("PlusAddresses.Refreshed"), 1); |
| } |
| #endif // !BUILDFLAG(IS_ANDROID) && !BUILDFLAG(IS_IOS) |
| |
| class PlusAddressAffiliationsTest : public PlusAddressServiceTest { |
| public: |
| PlusAddressAffiliationsTest() { |
| scoped_feature_list_.InitWithFeaturesAndParameters( |
| // Enable features: |
| {base::test::FeatureRefAndParams( |
| features::kPlusAddressesEnabled, |
| {{"server-url", "https://server.example"}, |
| {"oauth-scope", "scope.example"}})}, |
| // Disable features: |
| {}); |
| identity_env().MakePrimaryAccountAvailable("plus@plus.plus", |
| signin::ConsentLevel::kSignin); |
| identity_env().SetAutomaticIssueOfAccessTokens(true); |
| InitService(); |
| } |
| |
| testing::AssertionResult ExpectServiceToReturnAffiliatedPlusProfiles( |
| const url::Origin& origin, |
| const auto& matcher) { |
| base::MockCallback<PlusAddressService::GetPlusProfilesCallback> callback; |
| int calls = 0; |
| ON_CALL(callback, Run) |
| .WillByDefault([&](std::vector<PlusProfile> plus_profiles) { |
| EXPECT_THAT(plus_profiles, matcher); |
| ++calls; |
| }); |
| service().GetAffiliatedPlusProfiles(origin, callback.Get()); |
| return calls == 1 |
| ? testing::AssertionSuccess() |
| : (testing::AssertionFailure() << "Error fetching suggestions."); |
| } |
| |
| testing::AssertionResult ExpectServiceToReturnAffiliatedPlusAddresses( |
| const url::Origin& origin, |
| const auto& matcher) { |
| base::MockCallback<base::OnceCallback<void(std::vector<std::string>)>> |
| callback; |
| int calls = 0; |
| ON_CALL(callback, Run) |
| .WillByDefault([&](std::vector<std::string> plus_addresses) { |
| EXPECT_THAT(plus_addresses, matcher); |
| ++calls; |
| }); |
| service().GetAffiliatedPlusAddresses(origin, callback.Get()); |
| return calls == 1 ? testing::AssertionSuccess() |
| : (testing::AssertionFailure() |
| << "Error fetching plus addresses."); |
| } |
| |
| private: |
| base::test::ScopedFeatureList scoped_feature_list_; |
| }; |
| |
| // Verifies that affiliated PSL suggestions are returned. It also validates that |
| // entries in the PSL extensions list are respected. |
| TEST_F(PlusAddressAffiliationsTest, GetAffiliatedPSLSuggestions) { |
| PlusProfile profile1 = PlusProfile( |
| /*profile_id=*/"123", |
| /*facet=*/FacetURI::FromCanonicalSpec("https://one.foo.example.com"), |
| PlusAddress("plus+one@plus.plus"), |
| /*is_confirmed=*/true); |
| PlusProfile profile2 = PlusProfile( |
| /*profile_id=*/"234", |
| /*facet=*/FacetURI::FromCanonicalSpec("https://two.foo.example.com"), |
| PlusAddress("plus+foo@plus.plus"), |
| /*is_confirmed=*/true); |
| PlusProfile profile3 = PlusProfile( |
| /*profile_id=*/"345", |
| /*facet=*/FacetURI::FromCanonicalSpec("https://bar.example.com"), |
| PlusAddress("plus+bar@plus.plus"), |
| /*is_confirmed=*/true); |
| |
| service().SavePlusProfile(profile1); |
| service().SavePlusProfile(profile2); |
| service().SavePlusProfile(profile3); |
| ASSERT_THAT(service().GetPlusProfiles(), |
| UnorderedElementsAre(profile1, profile2, profile3)); |
| |
| EXPECT_CALL(affiliation_service(), GetPSLExtensions) |
| .WillOnce(RunOnceCallback<0>(std::vector<std::string>{"example.com"})); |
| |
| // Empty affiliation group. |
| affiliations::GroupedFacets group; |
| group.facets.emplace_back(profile1.facet); |
| EXPECT_CALL(affiliation_service(), GetGroupingInfo) |
| .WillOnce( |
| RunOnceCallback<1>(std::vector<affiliations::GroupedFacets>{group})); |
| |
| // Request the same URL as the `profile1.facet`. |
| const url::Origin origin = |
| url::Origin::Create(GURL(profile1.facet.canonical_spec())); |
| |
| // Note that `profile3` is not a PSL match due to the PSL extensions list. |
| EXPECT_TRUE(ExpectServiceToReturnAffiliatedPlusProfiles( |
| origin, UnorderedElementsAre(profile1, profile2))); |
| } |
| |
| // Verifies that affiliated group suggestions are returned. |
| TEST_F(PlusAddressAffiliationsTest, GetAffiliatedGroupSuggestions) { |
| PlusProfile group_profile = test::CreatePlusProfileWithFacet( |
| FacetURI::FromCanonicalSpec("https://group.affiliated.com")); |
| |
| // The user has the `group_profile` stored. |
| service().SavePlusProfile(group_profile); |
| ASSERT_THAT(service().GetPlusProfiles(), ElementsAre(group_profile)); |
| |
| EXPECT_CALL(affiliation_service(), GetPSLExtensions) |
| .WillOnce(RunOnceCallback<0>(std::vector<std::string>())); |
| |
| // Prepares the `group_profile` facet to be returned as part of the |
| // affiliation group. |
| affiliations::GroupedFacets group; |
| group.facets.emplace_back(group_profile.facet); |
| |
| EXPECT_CALL(affiliation_service(), GetGroupingInfo) |
| .WillOnce( |
| RunOnceCallback<1>(std::vector<affiliations::GroupedFacets>{group})); |
| |
| const url::Origin origin = url::Origin::Create(GURL("https://example.com")); |
| EXPECT_TRUE(ExpectServiceToReturnAffiliatedPlusProfiles( |
| origin, UnorderedElementsAre(group_profile))); |
| } |
| |
| // Tests that filling suggestions are returned even if they are affiliated |
| // matches and the profile is off the record. |
| TEST_F(PlusAddressAffiliationsTest, |
| GetSuggestionsIsAffiliationAwareWhenOffTheRecord) { |
| PlusProfile group_profile = test::CreatePlusProfileWithFacet( |
| FacetURI::FromCanonicalSpec("https://group.affiliated.com")); |
| service().SavePlusProfile(group_profile); |
| ASSERT_THAT(service().GetPlusProfiles(), ElementsAre(group_profile)); |
| |
| ON_CALL(affiliation_service(), GetPSLExtensions) |
| .WillByDefault(RunOnceCallback<0>(std::vector<std::string>())); |
| affiliations::GroupedFacets group; |
| group.facets.emplace_back(group_profile.facet); |
| ON_CALL(affiliation_service(), GetGroupingInfo) |
| .WillByDefault( |
| RunOnceCallback<1>(std::vector<affiliations::GroupedFacets>{group})); |
| |
| const url::Origin origin = url::Origin::Create(GURL("https://example.com")); |
| EXPECT_TRUE(ExpectServiceToReturnAffiliatedPlusProfiles( |
| origin, UnorderedElementsAre(group_profile))); |
| } |
| |
| // Tests that no creation suggestion is offered when the profile is off the |
| // record. |
| TEST_F(PlusAddressAffiliationsTest, |
| GetSuggestionsDoesNotOfferCreationWhenOffTheRecord) { |
| ON_CALL(affiliation_service(), GetPSLExtensions) |
| .WillByDefault(RunOnceCallback<0>(std::vector<std::string>())); |
| affiliations::GroupedFacets group; |
| ON_CALL(affiliation_service(), GetGroupingInfo) |
| .WillByDefault( |
| RunOnceCallback<1>(std::vector<affiliations::GroupedFacets>{group})); |
| |
| const url::Origin origin = url::Origin::Create(GURL("https://example.com")); |
| EXPECT_TRUE(ExpectServiceToReturnAffiliatedPlusProfiles(origin, IsEmpty())); |
| } |
| |
| // Tests that no creation suggestion is offered when the global toggle is off. |
| TEST_F(PlusAddressAffiliationsTest, |
| GetSuggestionsDoesNotOfferCreationWhenToggleIsOff) { |
| ON_CALL(affiliation_service(), GetPSLExtensions) |
| .WillByDefault(RunOnceCallback<0>(std::vector<std::string>())); |
| affiliations::GroupedFacets group; |
| ON_CALL(affiliation_service(), GetGroupingInfo) |
| .WillByDefault( |
| RunOnceCallback<1>(std::vector<affiliations::GroupedFacets>{group})); |
| setting_service().set_is_plus_addresses_enabled(false); |
| |
| const url::Origin origin = url::Origin::Create(GURL("https://example.com")); |
| EXPECT_TRUE(ExpectServiceToReturnAffiliatedPlusProfiles(origin, IsEmpty())); |
| } |
| |
| // Tests that filling suggestions are returned even if they are affiliated |
| // matches and the global settings toggle is off. |
| TEST_F(PlusAddressAffiliationsTest, |
| FillingSuggestionsAreOfferedWhenGlobalToggleIsOff) { |
| PlusProfile group_profile = test::CreatePlusProfileWithFacet( |
| FacetURI::FromCanonicalSpec("https://group.affiliated.com")); |
| service().SavePlusProfile(group_profile); |
| ASSERT_THAT(service().GetPlusProfiles(), ElementsAre(group_profile)); |
| |
| ON_CALL(affiliation_service(), GetPSLExtensions) |
| .WillByDefault(RunOnceCallback<0>(std::vector<std::string>())); |
| affiliations::GroupedFacets group; |
| group.facets.emplace_back(group_profile.facet); |
| ON_CALL(affiliation_service(), GetGroupingInfo) |
| .WillByDefault( |
| RunOnceCallback<1>(std::vector<affiliations::GroupedFacets>{group})); |
| setting_service().set_is_plus_addresses_enabled(false); |
| |
| const url::Origin origin = url::Origin::Create(GURL("https://example.com")); |
| EXPECT_TRUE(ExpectServiceToReturnAffiliatedPlusProfiles( |
| origin, UnorderedElementsAre(group_profile))); |
| } |
| |
| // Verifies that no affiliated suggestions are returned when there are no |
| // matches. Instead, the creation chip is offered. |
| TEST_F(PlusAddressAffiliationsTest, GetEmptyAffiliatedSuggestionMatches) { |
| PlusProfile stored_profile1 = test::CreatePlusProfileWithFacet( |
| FacetURI::FromCanonicalSpec("https://foo.com")); |
| PlusProfile stored_profile2 = test::CreatePlusProfileWithFacet( |
| FacetURI::FromCanonicalSpec("https://bar.com")); |
| |
| service().SavePlusProfile(stored_profile1); |
| service().SavePlusProfile(stored_profile2); |
| ASSERT_THAT(service().GetPlusProfiles(), |
| UnorderedElementsAre(stored_profile1, stored_profile2)); |
| |
| EXPECT_CALL(affiliation_service(), GetPSLExtensions) |
| .WillOnce(RunOnceCallback<0>(std::vector<std::string>())); |
| |
| affiliations::GroupedFacets group; |
| group.facets.emplace_back( |
| FacetURI::FromCanonicalSpec("https://group.affiliated.com")); |
| |
| EXPECT_CALL(affiliation_service(), GetGroupingInfo) |
| .WillOnce( |
| RunOnceCallback<1>(std::vector<affiliations::GroupedFacets>{group})); |
| |
| const url::Origin origin = url::Origin::Create(GURL("https://example.com")); |
| EXPECT_THAT(FetchPlusAddressSuggestions( |
| origin, |
| /*is_off_the_record=*/false, PasswordFormClassification(), |
| FormData(), FormFieldData(), |
| AutofillSuggestionTriggerSource::kFormControlElementClicked), |
| // There are no PLS, group or exact matches. |
| IsSingleCreatePlusAddressSuggestion()); |
| } |
| |
| // Verifies that affiliated plus profiles are returned. |
| TEST_F(PlusAddressAffiliationsTest, GetAffiliatedPSLProfiles) { |
| PlusProfile profile1 = test::CreatePlusProfileWithFacet( |
| FacetURI::FromCanonicalSpec("https://one.foo.example.com")); |
| PlusProfile profile2 = test::CreatePlusProfileWithFacet( |
| FacetURI::FromCanonicalSpec("https://two.foo.example.com")); |
| PlusProfile profile3 = test::CreatePlusProfileWithFacet( |
| FacetURI::FromCanonicalSpec("https://bar.example.com")); |
| |
| service().SavePlusProfile(profile1); |
| service().SavePlusProfile(profile2); |
| service().SavePlusProfile(profile3); |
| ASSERT_THAT(service().GetPlusProfiles(), |
| UnorderedElementsAre(profile1, profile2, profile3)); |
| |
| EXPECT_CALL(affiliation_service(), GetPSLExtensions) |
| .WillOnce(RunOnceCallback<0>(std::vector<std::string>{"example.com"})); |
| |
| // Empty affiliation group. |
| affiliations::GroupedFacets group; |
| group.facets.emplace_back(profile1.facet); |
| EXPECT_CALL(affiliation_service(), GetGroupingInfo) |
| .WillOnce( |
| RunOnceCallback<1>(std::vector<affiliations::GroupedFacets>{group})); |
| |
| // Request the same URL as the `profile1.facet`. |
| const url::Origin origin = |
| url::Origin::Create(GURL(profile1.facet.canonical_spec())); |
| |
| // Note that `profile3` is not a PSL match due to the PSL extensions list. |
| EXPECT_TRUE(ExpectServiceToReturnAffiliatedPlusProfiles( |
| origin, UnorderedElementsAre(profile1, profile2))); |
| } |
| |
| // Verifies that the service returns profiles from affiliated domains even if |
| // the requested domain doesn't have an affiliated plus address. |
| // |
| // TODO(crbug.com/399184823): Reenable after fixing the failed expectation. |
| TEST_F(PlusAddressAffiliationsTest, |
| DISABLED_AffiliatedProfilesForDomainWithNoPlusAddresses) { |
| PlusProfile group_profile = test::CreatePlusProfileWithFacet( |
| FacetURI::FromCanonicalSpec("https://group.affiliated.com")); |
| |
| service().SavePlusProfile(group_profile); |
| ASSERT_THAT(service().GetPlusProfiles(), UnorderedElementsAre(group_profile)); |
| |
| EXPECT_CALL(affiliation_service(), GetPSLExtensions) |
| .WillOnce(RunOnceCallback<0>(std::vector<std::string>())); |
| |
| // Prepares the `group_profile` facet to be returned as part of the |
| // affiliation group. |
| affiliations::GroupedFacets group; |
| group.facets.emplace_back(group_profile.facet); |
| |
| const url::Origin origin = |
| url::Origin::Create(GURL("https://bar.example.com")); |
| |
| EXPECT_TRUE(ExpectServiceToReturnAffiliatedPlusProfiles( |
| origin, UnorderedElementsAre(group_profile))); |
| } |
| |
| // Verifies that affiliated plus addresses are returned. |
| TEST_F(PlusAddressAffiliationsTest, GetAffiliatedPSLPlusAddresses) { |
| PlusProfile profile1 = PlusProfile( |
| /*profile_id=*/"123", |
| /*facet=*/FacetURI::FromCanonicalSpec("https://one.foo.example.com"), |
| PlusAddress("plus+one@plus.plus"), |
| /*is_confirmed=*/true); |
| PlusProfile profile2 = PlusProfile( |
| /*profile_id=*/"234", |
| /*facet=*/FacetURI::FromCanonicalSpec("https://two.foo.example.com"), |
| PlusAddress("plus+foo@plus.plus"), |
| /*is_confirmed=*/true); |
| PlusProfile profile3 = PlusProfile( |
| /*profile_id=*/"345", |
| /*facet=*/FacetURI::FromCanonicalSpec("https://bar.example.com"), |
| PlusAddress("plus+bar@plus.plus"), |
| /*is_confirmed=*/true); |
| |
| service().SavePlusProfile(profile1); |
| service().SavePlusProfile(profile2); |
| service().SavePlusProfile(profile3); |
| ASSERT_THAT(service().GetPlusProfiles(), |
| UnorderedElementsAre(profile1, profile2, profile3)); |
| |
| EXPECT_CALL(affiliation_service(), GetPSLExtensions) |
| .WillOnce(RunOnceCallback<0>(std::vector<std::string>{"example.com"})); |
| |
| // Empty affiliation group. |
| affiliations::GroupedFacets group; |
| group.facets.emplace_back(profile1.facet); |
| EXPECT_CALL(affiliation_service(), GetGroupingInfo) |
| .WillOnce( |
| RunOnceCallback<1>(std::vector<affiliations::GroupedFacets>{group})); |
| |
| // Request the same URL as the `profile1.facet`. |
| const url::Origin origin = |
| url::Origin::Create(GURL(profile1.facet.canonical_spec())); |
| |
| // Note that `profile3` is not a PSL match due to the PSL extensions list. |
| EXPECT_TRUE(ExpectServiceToReturnAffiliatedPlusAddresses( |
| origin, |
| UnorderedElementsAre("plus+one@plus.plus", "plus+foo@plus.plus"))); |
| } |
| |
| // Verifies that the service returns plus addresses from affiliated group |
| // domains. |
| // |
| // TODO(crbug.com/399184823): Reenable after fixing the failed expectation. |
| TEST_F(PlusAddressAffiliationsTest, |
| DISABLED_AffiliatedPlusAddressesForGroupMatches) { |
| PlusProfile group_profile = test::CreatePlusProfileWithFacet( |
| FacetURI::FromCanonicalSpec("https://group.affiliated.com")); |
| |
| service().SavePlusProfile(group_profile); |
| ASSERT_THAT(service().GetPlusProfiles(), UnorderedElementsAre(group_profile)); |
| |
| EXPECT_CALL(affiliation_service(), GetPSLExtensions) |
| .WillOnce(RunOnceCallback<0>(std::vector<std::string>())); |
| |
| // Prepares the `group_profile` facet to be returned as part of the |
| // affiliation group. |
| affiliations::GroupedFacets group; |
| group.facets.emplace_back(group_profile.facet); |
| |
| const url::Origin origin = |
| url::Origin::Create(GURL("https://bar.example.com")); |
| |
| EXPECT_TRUE(ExpectServiceToReturnAffiliatedPlusAddresses( |
| origin, UnorderedElementsAre("https://group.affiliated.com"))); |
| } |
| |
| } // namespace |
| } // namespace plus_addresses |