| // Copyright 2014 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "components/password_manager/core/browser/sync/password_syncable_service.h" |
| |
| #include <algorithm> |
| #include <memory> |
| #include <string> |
| #include <utility> |
| #include <vector> |
| |
| #include "base/location.h" |
| #include "base/macros.h" |
| #include "base/memory/ref_counted.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "base/test/scoped_feature_list.h" |
| #include "base/test/scoped_task_environment.h" |
| #include "components/password_manager/core/browser/mock_password_store.h" |
| #include "components/password_manager/core/common/password_manager_features.h" |
| #include "components/sync/model/sync_change_processor.h" |
| #include "components/sync/model/sync_change_processor_wrapper_for_test.h" |
| #include "components/sync/model/sync_error.h" |
| #include "components/sync/model/sync_error_factory_mock.h" |
| #include "testing/gmock/include/gmock/gmock.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| |
| using syncer::SyncChange; |
| using syncer::SyncData; |
| using syncer::SyncDataList; |
| using syncer::SyncError; |
| using testing::AnyNumber; |
| using testing::DoAll; |
| using testing::ElementsAre; |
| using testing::IgnoreResult; |
| using testing::Invoke; |
| using testing::IsEmpty; |
| using testing::Matches; |
| using testing::Return; |
| using testing::SetArgPointee; |
| using testing::UnorderedElementsAre; |
| using testing::_; |
| |
| namespace password_manager { |
| |
| // Defined in the implementation file corresponding to this test. |
| syncer::SyncData SyncDataFromPassword(const autofill::PasswordForm& password); |
| autofill::PasswordForm PasswordFromSpecifics( |
| const sync_pb::PasswordSpecificsData& password); |
| std::string MakePasswordSyncTag(const sync_pb::PasswordSpecificsData& password); |
| std::string MakePasswordSyncTag(const autofill::PasswordForm& password); |
| |
| namespace { |
| |
| // PasswordForm values for tests. |
| constexpr autofill::PasswordForm::Type kArbitraryType = |
| autofill::PasswordForm::TYPE_GENERATED; |
| constexpr char kIconUrl[] = "https://fb.com/Icon"; |
| constexpr char kDisplayName[] = "Agent Smith"; |
| constexpr char kFederationUrl[] = "https://fb.com/"; |
| constexpr char kPassword[] = "abcdef"; |
| constexpr char kSignonRealm[] = "abc"; |
| constexpr char kSignonRealm2[] = "def"; |
| constexpr char kSignonRealm3[] = "xyz"; |
| constexpr int kTimesUsed = 5; |
| constexpr char kUsername[] = "godzilla"; |
| |
| typedef std::vector<SyncChange> SyncChangeList; |
| |
| const sync_pb::PasswordSpecificsData& GetPasswordSpecifics( |
| const syncer::SyncData& sync_data) { |
| return sync_data.GetSpecifics().password().client_only_encrypted_data(); |
| } |
| |
| MATCHER(HasDateSynced, "") { |
| return !arg.date_synced.is_null() && !arg.date_synced.is_max(); |
| } |
| |
| MATCHER_P(PasswordIs, form, "") { |
| sync_pb::PasswordSpecificsData actual_password = |
| GetPasswordSpecifics(SyncDataFromPassword(arg)); |
| sync_pb::PasswordSpecificsData expected_password = |
| GetPasswordSpecifics(SyncDataFromPassword(form)); |
| if (expected_password.scheme() == actual_password.scheme() && |
| expected_password.signon_realm() == actual_password.signon_realm() && |
| expected_password.origin() == actual_password.origin() && |
| expected_password.action() == actual_password.action() && |
| expected_password.username_element() == |
| actual_password.username_element() && |
| expected_password.password_element() == |
| actual_password.password_element() && |
| expected_password.username_value() == actual_password.username_value() && |
| expected_password.password_value() == actual_password.password_value() && |
| expected_password.preferred() == actual_password.preferred() && |
| expected_password.date_created() == actual_password.date_created() && |
| expected_password.blacklisted() == actual_password.blacklisted() && |
| expected_password.type() == actual_password.type() && |
| expected_password.times_used() == actual_password.times_used() && |
| expected_password.display_name() == actual_password.display_name() && |
| expected_password.avatar_url() == actual_password.avatar_url() && |
| expected_password.federation_url() == actual_password.federation_url()) |
| return true; |
| |
| *result_listener << "Password protobuf does not match; expected:\n" |
| << form << '\n' |
| << "actual:" << '\n' |
| << arg; |
| return false; |
| } |
| |
| MATCHER_P2(SyncChangeIs, change_type, password, "") { |
| const SyncData& data = arg.sync_data(); |
| autofill::PasswordForm form = |
| PasswordFromSpecifics(GetPasswordSpecifics(data)); |
| return (arg.change_type() == change_type && |
| syncer::SyncDataLocal(data).GetTag() == |
| MakePasswordSyncTag(password) && |
| (change_type == SyncChange::ACTION_DELETE || |
| Matches(PasswordIs(password))(form))); |
| } |
| |
| // The argument is std::vector<autofill::PasswordForm*>*. The caller is |
| // responsible for the lifetime of all the password forms. |
| ACTION_P(AppendForm, form) { |
| arg0->push_back(std::make_unique<autofill::PasswordForm>(form)); |
| return true; |
| } |
| |
| // Creates a sync data consisting of password specifics. The sign on realm is |
| // set to |signon_realm|. |
| SyncData CreateSyncData(const std::string& signon_realm) { |
| sync_pb::EntitySpecifics password_data; |
| sync_pb::PasswordSpecificsData* password_specifics = |
| password_data.mutable_password()->mutable_client_only_encrypted_data(); |
| password_specifics->set_signon_realm(signon_realm); |
| password_specifics->set_type(autofill::PasswordForm::TYPE_GENERATED); |
| password_specifics->set_times_used(3); |
| password_specifics->set_display_name("Mr. X"); |
| password_specifics->set_avatar_url("https://accounts.google.com/Icon"); |
| password_specifics->set_federation_url("https://google.com"); |
| password_specifics->set_username_value("kingkong"); |
| password_specifics->set_password_value("sicrit"); |
| |
| std::string tag = MakePasswordSyncTag(*password_specifics); |
| return syncer::SyncData::CreateLocalData(tag, tag, password_data); |
| } |
| |
| SyncChange CreateSyncChange(const autofill::PasswordForm& password, |
| SyncChange::SyncChangeType type) { |
| SyncData data = SyncDataFromPassword(password); |
| return SyncChange(FROM_HERE, type, data); |
| } |
| |
| // Mock implementation of SyncChangeProcessor. |
| class MockSyncChangeProcessor : public syncer::SyncChangeProcessor { |
| public: |
| MockSyncChangeProcessor() {} |
| |
| MOCK_METHOD2(ProcessSyncChanges, |
| SyncError(const base::Location&, const SyncChangeList& list)); |
| SyncDataList GetAllSyncData(syncer::ModelType type) const override { |
| NOTREACHED(); |
| return SyncDataList(); |
| } |
| |
| private: |
| DISALLOW_COPY_AND_ASSIGN(MockSyncChangeProcessor); |
| }; |
| |
| // Convenience wrapper around a PasswordSyncableService and PasswordStore |
| // pair. |
| class PasswordSyncableServiceWrapper { |
| public: |
| PasswordSyncableServiceWrapper() { |
| password_store_ = new testing::StrictMock<MockPasswordStore>; |
| password_store_->Init(syncer::SyncableService::StartSyncFlare(), nullptr); |
| service_.reset( |
| new PasswordSyncableService(password_store_->GetSyncInterface())); |
| |
| ON_CALL(*password_store_, AddLoginImpl(HasDateSynced())) |
| .WillByDefault(Return(PasswordStoreChangeList())); |
| ON_CALL(*password_store_, RemoveLoginImpl(_)) |
| .WillByDefault(Return(PasswordStoreChangeList())); |
| ON_CALL(*password_store_, UpdateLoginImpl(HasDateSynced())) |
| .WillByDefault(Return(PasswordStoreChangeList())); |
| EXPECT_CALL(*password_store(), NotifyLoginsChanged(_)).Times(AnyNumber()); |
| } |
| |
| ~PasswordSyncableServiceWrapper() { password_store_->ShutdownOnUIThread(); } |
| |
| MockPasswordStore* password_store() { return password_store_.get(); } |
| |
| PasswordSyncableService* service() { return service_.get(); } |
| |
| private: |
| scoped_refptr<MockPasswordStore> password_store_; |
| std::unique_ptr<PasswordSyncableService> service_; |
| |
| DISALLOW_COPY_AND_ASSIGN(PasswordSyncableServiceWrapper); |
| }; |
| |
| class PasswordSyncableServiceTest : public testing::Test { |
| public: |
| PasswordSyncableServiceTest() |
| : processor_(new testing::StrictMock<MockSyncChangeProcessor>) { |
| ON_CALL(*processor_, ProcessSyncChanges(_, _)) |
| .WillByDefault(Return(SyncError())); |
| } |
| MockPasswordStore* password_store() { return wrapper_.password_store(); } |
| PasswordSyncableService* service() { return wrapper_.service(); } |
| |
| MOCK_METHOD1(StartSyncFlare, void(syncer::ModelType)); |
| |
| protected: |
| std::unique_ptr<MockSyncChangeProcessor> processor_; |
| |
| private: |
| // Used by the password store. |
| base::test::ScopedTaskEnvironment scoped_task_environment_; |
| PasswordSyncableServiceWrapper wrapper_; |
| }; |
| |
| // Both sync and password db have data that are not present in the other. |
| TEST_F(PasswordSyncableServiceTest, AdditionsInBoth) { |
| autofill::PasswordForm form; |
| form.signon_realm = kSignonRealm; |
| form.username_value = base::ASCIIToUTF16(kUsername); |
| form.password_value = base::ASCIIToUTF16(kPassword); |
| |
| SyncDataList list; |
| list.push_back(CreateSyncData(kSignonRealm2)); |
| autofill::PasswordForm new_from_sync = |
| PasswordFromSpecifics(GetPasswordSpecifics(list.back())); |
| |
| EXPECT_CALL(*password_store(), FillAutofillableLogins(_)) |
| .WillOnce(AppendForm(form)); |
| EXPECT_CALL(*password_store(), FillBlacklistLogins(_)).WillOnce(Return(true)); |
| EXPECT_CALL(*password_store(), AddLoginImpl(PasswordIs(new_from_sync))); |
| EXPECT_CALL(*processor_, |
| ProcessSyncChanges( |
| _, ElementsAre(SyncChangeIs(SyncChange::ACTION_ADD, form)))); |
| |
| service()->MergeDataAndStartSyncing( |
| syncer::PASSWORDS, list, std::move(processor_), |
| std::unique_ptr<syncer::SyncErrorFactory>()); |
| } |
| |
| // Sync has data that is not present in the password db. |
| TEST_F(PasswordSyncableServiceTest, AdditionOnlyInSync) { |
| SyncDataList list; |
| list.push_back(CreateSyncData(kSignonRealm)); |
| autofill::PasswordForm new_from_sync = |
| PasswordFromSpecifics(GetPasswordSpecifics(list.back())); |
| |
| EXPECT_CALL(*password_store(), FillAutofillableLogins(_)) |
| .WillOnce(Return(true)); |
| EXPECT_CALL(*password_store(), FillBlacklistLogins(_)).WillOnce(Return(true)); |
| EXPECT_CALL(*password_store(), AddLoginImpl(PasswordIs(new_from_sync))); |
| EXPECT_CALL(*processor_, ProcessSyncChanges(_, IsEmpty())); |
| |
| service()->MergeDataAndStartSyncing( |
| syncer::PASSWORDS, list, std::move(processor_), |
| std::unique_ptr<syncer::SyncErrorFactory>()); |
| } |
| |
| // Passwords db has data that is not present in sync. |
| TEST_F(PasswordSyncableServiceTest, AdditionOnlyInPasswordStore) { |
| autofill::PasswordForm form; |
| form.signon_realm = kSignonRealm; |
| form.times_used = kTimesUsed; |
| form.type = kArbitraryType; |
| form.display_name = base::ASCIIToUTF16(kDisplayName); |
| form.icon_url = GURL(kIconUrl); |
| form.federation_origin = url::Origin::Create(GURL(kFederationUrl)); |
| form.username_value = base::ASCIIToUTF16(kUsername); |
| form.password_value = base::ASCIIToUTF16(kPassword); |
| EXPECT_CALL(*password_store(), FillAutofillableLogins(_)) |
| .WillOnce(AppendForm(form)); |
| EXPECT_CALL(*password_store(), FillBlacklistLogins(_)).WillOnce(Return(true)); |
| |
| EXPECT_CALL(*processor_, |
| ProcessSyncChanges( |
| _, ElementsAre(SyncChangeIs(SyncChange::ACTION_ADD, form)))); |
| |
| service()->MergeDataAndStartSyncing( |
| syncer::PASSWORDS, SyncDataList(), std::move(processor_), |
| std::unique_ptr<syncer::SyncErrorFactory>()); |
| } |
| |
| // Both passwords db and sync contain the same data. |
| TEST_F(PasswordSyncableServiceTest, BothInSync) { |
| autofill::PasswordForm form; |
| form.signon_realm = kSignonRealm; |
| form.times_used = kTimesUsed; |
| form.type = kArbitraryType; |
| form.username_value = base::ASCIIToUTF16(kUsername); |
| form.password_value = base::ASCIIToUTF16(kPassword); |
| EXPECT_CALL(*password_store(), FillAutofillableLogins(_)) |
| .WillOnce(AppendForm(form)); |
| EXPECT_CALL(*password_store(), FillBlacklistLogins(_)).WillOnce(Return(true)); |
| |
| EXPECT_CALL(*processor_, ProcessSyncChanges(_, IsEmpty())); |
| |
| service()->MergeDataAndStartSyncing( |
| syncer::PASSWORDS, SyncDataList(1, SyncDataFromPassword(form)), |
| std::move(processor_), std::unique_ptr<syncer::SyncErrorFactory>()); |
| } |
| |
| // Both passwords db and sync have the same data but they need to be merged |
| // as some fields of the data differ. |
| TEST_F(PasswordSyncableServiceTest, Merge) { |
| autofill::PasswordForm form1; |
| form1.signon_realm = kSignonRealm; |
| form1.action = GURL("http://pie.com"); |
| form1.date_created = base::Time::Now(); |
| form1.preferred = true; |
| form1.username_value = base::ASCIIToUTF16(kUsername); |
| form1.password_value = base::ASCIIToUTF16(kPassword); |
| |
| autofill::PasswordForm form2(form1); |
| form2.preferred = false; |
| EXPECT_CALL(*password_store(), FillAutofillableLogins(_)) |
| .WillOnce(AppendForm(form1)); |
| EXPECT_CALL(*password_store(), FillBlacklistLogins(_)).WillOnce(Return(true)); |
| EXPECT_CALL(*password_store(), UpdateLoginImpl(PasswordIs(form2))); |
| EXPECT_CALL(*processor_, ProcessSyncChanges(_, IsEmpty())); |
| |
| service()->MergeDataAndStartSyncing( |
| syncer::PASSWORDS, SyncDataList(1, SyncDataFromPassword(form2)), |
| std::move(processor_), std::unique_ptr<syncer::SyncErrorFactory>()); |
| } |
| |
| // Initiate sync due to local DB changes. |
| TEST_F(PasswordSyncableServiceTest, PasswordStoreChanges) { |
| // Save the reference to the processor because |processor_| is NULL after |
| // MergeDataAndStartSyncing(). |
| MockSyncChangeProcessor& weak_processor = *processor_; |
| EXPECT_CALL(weak_processor, ProcessSyncChanges(_, IsEmpty())); |
| EXPECT_CALL(*password_store(), FillAutofillableLogins(_)) |
| .WillOnce(Return(true)); |
| EXPECT_CALL(*password_store(), FillBlacklistLogins(_)).WillOnce(Return(true)); |
| service()->MergeDataAndStartSyncing( |
| syncer::PASSWORDS, SyncDataList(), std::move(processor_), |
| std::unique_ptr<syncer::SyncErrorFactory>()); |
| |
| autofill::PasswordForm form1; |
| form1.signon_realm = kSignonRealm; |
| autofill::PasswordForm form2; |
| form2.signon_realm = kSignonRealm2; |
| autofill::PasswordForm form3; |
| form3.signon_realm = kSignonRealm3; |
| |
| SyncChangeList sync_list; |
| sync_list.push_back(CreateSyncChange(form1, SyncChange::ACTION_ADD)); |
| sync_list.push_back(CreateSyncChange(form2, SyncChange::ACTION_UPDATE)); |
| sync_list.push_back(CreateSyncChange(form3, SyncChange::ACTION_DELETE)); |
| |
| PasswordStoreChangeList list; |
| list.push_back(PasswordStoreChange(PasswordStoreChange::ADD, form1)); |
| list.push_back(PasswordStoreChange(PasswordStoreChange::UPDATE, form2)); |
| list.push_back(PasswordStoreChange(PasswordStoreChange::REMOVE, form3)); |
| EXPECT_CALL( |
| weak_processor, |
| ProcessSyncChanges( |
| _, ElementsAre(SyncChangeIs(SyncChange::ACTION_ADD, form1), |
| SyncChangeIs(SyncChange::ACTION_UPDATE, form2), |
| SyncChangeIs(SyncChange::ACTION_DELETE, form3)))); |
| service()->ActOnPasswordStoreChanges(list); |
| } |
| |
| // Process all types of changes from sync. |
| TEST_F(PasswordSyncableServiceTest, ProcessSyncChanges) { |
| autofill::PasswordForm updated_form; |
| updated_form.signon_realm = kSignonRealm; |
| updated_form.action = GURL("http://foo.com"); |
| updated_form.date_created = base::Time::Now(); |
| updated_form.username_value = base::ASCIIToUTF16(kUsername); |
| updated_form.password_value = base::ASCIIToUTF16(kPassword); |
| autofill::PasswordForm deleted_form; |
| deleted_form.signon_realm = kSignonRealm2; |
| deleted_form.action = GURL("http://bar.com"); |
| deleted_form.blacklisted_by_user = true; |
| |
| SyncData add_data = CreateSyncData(kSignonRealm3); |
| autofill::PasswordForm new_from_sync = |
| PasswordFromSpecifics(GetPasswordSpecifics(add_data)); |
| |
| SyncChangeList list; |
| list.push_back( |
| SyncChange(FROM_HERE, syncer::SyncChange::ACTION_ADD, add_data)); |
| list.push_back( |
| CreateSyncChange(updated_form, syncer::SyncChange::ACTION_UPDATE)); |
| list.push_back( |
| CreateSyncChange(deleted_form, syncer::SyncChange::ACTION_DELETE)); |
| EXPECT_CALL(*password_store(), AddLoginImpl(PasswordIs(new_from_sync))); |
| EXPECT_CALL(*password_store(), UpdateLoginImpl(PasswordIs(updated_form))); |
| EXPECT_CALL(*password_store(), RemoveLoginImpl(PasswordIs(deleted_form))); |
| service()->ProcessSyncChanges(FROM_HERE, list); |
| } |
| |
| // Retrives sync data from the model. |
| TEST_F(PasswordSyncableServiceTest, GetAllSyncData) { |
| autofill::PasswordForm form1; |
| form1.signon_realm = kSignonRealm; |
| form1.action = GURL("http://foo.com"); |
| form1.times_used = kTimesUsed; |
| form1.type = kArbitraryType; |
| form1.display_name = base::ASCIIToUTF16(kDisplayName); |
| form1.icon_url = GURL(kIconUrl); |
| form1.federation_origin = url::Origin::Create(GURL(kFederationUrl)); |
| form1.username_value = base::ASCIIToUTF16(kUsername); |
| form1.password_value = base::ASCIIToUTF16(kPassword); |
| autofill::PasswordForm form2; |
| form2.signon_realm = kSignonRealm2; |
| form2.action = GURL("http://bar.com"); |
| form2.blacklisted_by_user = true; |
| EXPECT_CALL(*password_store(), FillAutofillableLogins(_)) |
| .WillOnce(AppendForm(form1)); |
| EXPECT_CALL(*password_store(), FillBlacklistLogins(_)) |
| .WillOnce(AppendForm(form2)); |
| |
| SyncDataList actual_list = service()->GetAllSyncData(syncer::PASSWORDS); |
| std::vector<autofill::PasswordForm> actual_form_list; |
| for (auto it = actual_list.begin(); it != actual_list.end(); ++it) { |
| actual_form_list.push_back( |
| PasswordFromSpecifics(GetPasswordSpecifics(*it))); |
| } |
| EXPECT_THAT(actual_form_list, |
| UnorderedElementsAre(PasswordIs(form1), PasswordIs(form2))); |
| } |
| |
| // Creates 2 PasswordSyncableService instances, merges the content of the first |
| // one to the second one and back. |
| TEST_F(PasswordSyncableServiceTest, MergeDataAndPushBack) { |
| autofill::PasswordForm form1; |
| form1.signon_realm = kSignonRealm; |
| form1.action = GURL("http://foo.com"); |
| form1.username_value = base::ASCIIToUTF16(kUsername); |
| form1.password_value = base::ASCIIToUTF16(kPassword); |
| |
| PasswordSyncableServiceWrapper other_service_wrapper; |
| autofill::PasswordForm form2; |
| form2.signon_realm = kSignonRealm2; |
| form2.action = GURL("http://bar.com"); |
| form2.username_value = base::ASCIIToUTF16(kUsername); |
| form2.password_value = base::ASCIIToUTF16(kPassword); |
| EXPECT_CALL(*password_store(), FillAutofillableLogins(_)) |
| .WillOnce(AppendForm(form1)); |
| EXPECT_CALL(*password_store(), FillBlacklistLogins(_)).WillOnce(Return(true)); |
| EXPECT_CALL(*other_service_wrapper.password_store(), |
| FillAutofillableLogins(_)) |
| .WillOnce(AppendForm(form2)); |
| EXPECT_CALL(*other_service_wrapper.password_store(), FillBlacklistLogins(_)) |
| .WillOnce(Return(true)); |
| // This method reads all passwords from the database. Make sure that the |
| // database is not read twice if there was no problem getting all the |
| // passwords during the first read. |
| EXPECT_CALL(*password_store(), DeleteUndecryptableLogins()).Times(0); |
| |
| EXPECT_CALL(*password_store(), AddLoginImpl(PasswordIs(form2))); |
| EXPECT_CALL(*other_service_wrapper.password_store(), |
| AddLoginImpl(PasswordIs(form1))); |
| |
| syncer::SyncDataList other_service_data = |
| other_service_wrapper.service()->GetAllSyncData(syncer::PASSWORDS); |
| service()->MergeDataAndStartSyncing( |
| syncer::PASSWORDS, other_service_data, |
| std::make_unique<syncer::SyncChangeProcessorWrapperForTest>( |
| other_service_wrapper.service()), |
| std::unique_ptr<syncer::SyncErrorFactory>()); |
| } |
| |
| // Calls ActOnPasswordStoreChanges without SyncChangeProcessor. StartSyncFlare |
| // should be called. |
| TEST_F(PasswordSyncableServiceTest, StartSyncFlare) { |
| autofill::PasswordForm form; |
| form.signon_realm = kSignonRealm; |
| form.username_value = base::ASCIIToUTF16(kUsername); |
| form.password_value = base::ASCIIToUTF16(kPassword); |
| PasswordStoreChangeList list; |
| list.push_back(PasswordStoreChange(PasswordStoreChange::ADD, form)); |
| |
| // No flare and no SyncChangeProcessor, the call shouldn't crash. |
| service()->ActOnPasswordStoreChanges(list); |
| |
| // Set the flare. It should be called as there is no SyncChangeProcessor. |
| service()->InjectStartSyncFlare(base::Bind( |
| &PasswordSyncableServiceTest::StartSyncFlare, base::Unretained(this))); |
| EXPECT_CALL(*this, StartSyncFlare(syncer::PASSWORDS)); |
| service()->ActOnPasswordStoreChanges(list); |
| } |
| |
| // Start syncing with an error. Subsequent password store updates shouldn't be |
| // propagated to Sync. |
| TEST_F(PasswordSyncableServiceTest, FailedReadFromPasswordStore) { |
| std::unique_ptr<syncer::SyncErrorFactoryMock> error_factory( |
| new syncer::SyncErrorFactoryMock); |
| syncer::SyncError error(FROM_HERE, syncer::SyncError::DATATYPE_ERROR, |
| "Failed to get passwords from store.", |
| syncer::PASSWORDS); |
| EXPECT_CALL(*password_store(), FillAutofillableLogins(_)) |
| .WillOnce(Return(false)); |
| if (base::FeatureList::IsEnabled(features::kRecoverPasswordsForSyncUsers)) { |
| EXPECT_CALL(*password_store(), DeleteUndecryptableLogins()) |
| .WillOnce(Return(DatabaseCleanupResult::kDatabaseUnavailable)); |
| } |
| EXPECT_CALL(*error_factory, CreateAndUploadError(_, _)) |
| .WillOnce(Return(error)); |
| // ActOnPasswordStoreChanges() below shouldn't generate any changes for Sync. |
| // |processor_| will be destroyed in MergeDataAndStartSyncing(). |
| EXPECT_CALL(*processor_, ProcessSyncChanges(_, _)).Times(0); |
| syncer::SyncMergeResult result = service()->MergeDataAndStartSyncing( |
| syncer::PASSWORDS, syncer::SyncDataList(), std::move(processor_), |
| std::move(error_factory)); |
| EXPECT_TRUE(result.error().IsSet()); |
| |
| autofill::PasswordForm form; |
| form.signon_realm = kSignonRealm; |
| PasswordStoreChangeList list; |
| list.push_back(PasswordStoreChange(PasswordStoreChange::ADD, form)); |
| service()->ActOnPasswordStoreChanges(list); |
| } |
| |
| // Disable feature for deleting undecryptable logins. |
| TEST_F(PasswordSyncableServiceTest, RecoverPasswordsForSyncUsersDisabled) { |
| base::test::ScopedFeatureList scoped_feature_list; |
| scoped_feature_list.InitAndDisableFeature( |
| features::kRecoverPasswordsForSyncUsers); |
| auto error_factory = std::make_unique<syncer::SyncErrorFactoryMock>(); |
| syncer::SyncError error(FROM_HERE, syncer::SyncError::DATATYPE_ERROR, |
| "Failed to get passwords from store.", |
| syncer::PASSWORDS); |
| EXPECT_CALL(*error_factory, CreateAndUploadError(_, _)) |
| .WillOnce(Return(error)); |
| |
| EXPECT_CALL(*password_store(), FillAutofillableLogins(_)) |
| .WillOnce(Return(false)); |
| EXPECT_CALL(*password_store(), DeleteUndecryptableLogins()).Times(0); |
| |
| syncer::SyncMergeResult result = service()->MergeDataAndStartSyncing( |
| syncer::PASSWORDS, SyncDataList(), std::move(processor_), |
| std::move(error_factory)); |
| EXPECT_TRUE(result.error().IsSet()); |
| } |
| |
| // Test that passwords are recovered for Sync users using the older feature |
| // (kRecoverPasswordsForSyncUsers) when feature for recovering passwords for |
| // Sync users is enabled, while feature for deleting corrupted passwords for |
| // all users is disabled. |
| TEST_F(PasswordSyncableServiceTest, RecoverPasswordsForSyncUsersEnabled) { |
| base::test::ScopedFeatureList scoped_feature_list; |
| scoped_feature_list.InitWithFeatures( |
| {features::kRecoverPasswordsForSyncUsers}, |
| {features::kDeleteCorruptedPasswords}); |
| |
| EXPECT_CALL(*processor_, ProcessSyncChanges(_, IsEmpty())); |
| EXPECT_CALL(*password_store(), FillAutofillableLogins(_)) |
| .Times(2) |
| .WillOnce(Return(false)) |
| .WillOnce(Return(true)); |
| EXPECT_CALL(*password_store(), DeleteUndecryptableLogins()) |
| .WillOnce(Return(DatabaseCleanupResult::kSuccess)); |
| EXPECT_CALL(*password_store(), FillBlacklistLogins(_)).WillOnce(Return(true)); |
| |
| syncer::SyncMergeResult result = service()->MergeDataAndStartSyncing( |
| syncer::PASSWORDS, SyncDataList(), std::move(processor_), nullptr); |
| EXPECT_FALSE(result.error().IsSet()); |
| } |
| |
| // Test that passwords are not recovered using the older feature |
| // (kRecoverPasswordForSyncUsers) when merging data if both features for |
| // recovering passwords for Sync users and deleting passwords for all users |
| // are enabled. |
| TEST_F(PasswordSyncableServiceTest, PasswordRecoveryForAllUsersEnabled) { |
| base::test::ScopedFeatureList scoped_feature_list; |
| scoped_feature_list.InitWithFeatures({features::kRecoverPasswordsForSyncUsers, |
| features::kDeleteCorruptedPasswords}, |
| {}); |
| |
| auto error_factory = std::make_unique<syncer::SyncErrorFactoryMock>(); |
| syncer::SyncError error(FROM_HERE, syncer::SyncError::DATATYPE_ERROR, |
| "Failed to get passwords from store.", |
| syncer::PASSWORDS); |
| EXPECT_CALL(*error_factory, CreateAndUploadError(_, _)) |
| .WillOnce(Return(error)); |
| |
| EXPECT_CALL(*password_store(), FillAutofillableLogins(_)) |
| .WillOnce(Return(false)); |
| EXPECT_CALL(*password_store(), DeleteUndecryptableLogins()).Times(0); |
| |
| syncer::SyncMergeResult result = service()->MergeDataAndStartSyncing( |
| syncer::PASSWORDS, SyncDataList(), std::move(processor_), |
| std::move(error_factory)); |
| EXPECT_TRUE(result.error().IsSet()); |
| } |
| |
| // Database cleanup fails because encryption is unavailable. |
| TEST_F(PasswordSyncableServiceTest, FailedDeleteUndecryptableLogins) { |
| base::test::ScopedFeatureList scoped_feature_list; |
| scoped_feature_list.InitWithFeatures( |
| {features::kRecoverPasswordsForSyncUsers}, |
| {features::kDeleteCorruptedPasswords}); |
| auto error_factory = std::make_unique<syncer::SyncErrorFactoryMock>(); |
| syncer::SyncError error( |
| FROM_HERE, syncer::SyncError::DATATYPE_ERROR, |
| "Failed to get encryption key during database cleanup.", |
| syncer::PASSWORDS); |
| EXPECT_CALL(*error_factory, CreateAndUploadError(_, _)) |
| .WillOnce(Return(error)); |
| |
| EXPECT_CALL(*password_store(), FillAutofillableLogins(_)) |
| .WillOnce(Return(false)); |
| EXPECT_CALL(*password_store(), DeleteUndecryptableLogins()) |
| .WillOnce(Return(DatabaseCleanupResult::kEncryptionUnavailable)); |
| |
| syncer::SyncMergeResult result = service()->MergeDataAndStartSyncing( |
| syncer::PASSWORDS, SyncDataList(), std::move(processor_), |
| std::move(error_factory)); |
| EXPECT_TRUE(result.error().IsSet()); |
| } |
| |
| // Start syncing with an error in ProcessSyncChanges. Subsequent password store |
| // updates shouldn't be propagated to Sync. |
| TEST_F(PasswordSyncableServiceTest, FailedProcessSyncChanges) { |
| autofill::PasswordForm form; |
| form.signon_realm = kSignonRealm; |
| form.username_value = base::ASCIIToUTF16(kUsername); |
| form.password_value = base::ASCIIToUTF16(kPassword); |
| std::unique_ptr<syncer::SyncErrorFactoryMock> error_factory( |
| new syncer::SyncErrorFactoryMock); |
| syncer::SyncError error(FROM_HERE, syncer::SyncError::DATATYPE_ERROR, |
| "There is a problem", syncer::PASSWORDS); |
| EXPECT_CALL(*password_store(), FillAutofillableLogins(_)) |
| .WillOnce(AppendForm(form)); |
| EXPECT_CALL(*password_store(), FillBlacklistLogins(_)).WillOnce(Return(true)); |
| |
| // ActOnPasswordStoreChanges() below shouldn't generate any changes for Sync. |
| // |processor_| will be destroyed in MergeDataAndStartSyncing(). |
| EXPECT_CALL(*processor_, ProcessSyncChanges(_, _)) |
| .Times(1) |
| .WillOnce(Return(error)); |
| syncer::SyncMergeResult result = service()->MergeDataAndStartSyncing( |
| syncer::PASSWORDS, syncer::SyncDataList(), std::move(processor_), |
| std::move(error_factory)); |
| EXPECT_TRUE(result.error().IsSet()); |
| |
| form.signon_realm = kSignonRealm2; |
| PasswordStoreChangeList list; |
| list.push_back(PasswordStoreChange(PasswordStoreChange::ADD, form)); |
| service()->ActOnPasswordStoreChanges(list); |
| } |
| |
| // Serialize and deserialize empty federation_origin and make sure it's an empty |
| // string. |
| TEST_F(PasswordSyncableServiceTest, SerializeEmptyFederation) { |
| autofill::PasswordForm form; |
| EXPECT_TRUE(form.federation_origin.opaque()); |
| syncer::SyncData data = SyncDataFromPassword(form); |
| const sync_pb::PasswordSpecificsData& specifics = GetPasswordSpecifics(data); |
| EXPECT_TRUE(specifics.has_federation_url()); |
| EXPECT_EQ(std::string(), specifics.federation_url()); |
| |
| // Deserialize back. |
| form = PasswordFromSpecifics(specifics); |
| EXPECT_TRUE(form.federation_origin.opaque()); |
| |
| // Make sure that the Origins uploaded incorrectly are still deserialized |
| // correctly. |
| // crbug.com/593380. |
| sync_pb::PasswordSpecificsData specifics1; |
| specifics1.set_federation_url("null"); |
| form = PasswordFromSpecifics(specifics1); |
| EXPECT_TRUE(form.federation_origin.opaque()); |
| } |
| |
| // Serialize empty PasswordForm and make sure the Sync representation is |
| // matching the expectations |
| TEST_F(PasswordSyncableServiceTest, SerializeEmptyPasswordForm) { |
| autofill::PasswordForm form; |
| syncer::SyncData data = SyncDataFromPassword(form); |
| const sync_pb::PasswordSpecificsData& specifics = GetPasswordSpecifics(data); |
| EXPECT_TRUE(specifics.has_scheme()); |
| EXPECT_EQ(0, specifics.scheme()); |
| EXPECT_TRUE(specifics.has_signon_realm()); |
| EXPECT_EQ("", specifics.signon_realm()); |
| EXPECT_TRUE(specifics.has_origin()); |
| EXPECT_EQ("", specifics.origin()); |
| EXPECT_TRUE(specifics.has_action()); |
| EXPECT_EQ("", specifics.action()); |
| EXPECT_TRUE(specifics.has_username_element()); |
| EXPECT_EQ("", specifics.username_element()); |
| EXPECT_TRUE(specifics.has_username_value()); |
| EXPECT_EQ("", specifics.username_value()); |
| EXPECT_TRUE(specifics.has_password_element()); |
| EXPECT_EQ("", specifics.password_element()); |
| EXPECT_TRUE(specifics.has_password_value()); |
| EXPECT_EQ("", specifics.password_value()); |
| EXPECT_TRUE(specifics.has_preferred()); |
| EXPECT_FALSE(specifics.preferred()); |
| EXPECT_TRUE(specifics.has_date_created()); |
| EXPECT_EQ(0, specifics.date_created()); |
| EXPECT_TRUE(specifics.has_blacklisted()); |
| EXPECT_FALSE(specifics.blacklisted()); |
| EXPECT_TRUE(specifics.has_type()); |
| EXPECT_EQ(0, specifics.type()); |
| EXPECT_TRUE(specifics.has_times_used()); |
| EXPECT_EQ(0, specifics.times_used()); |
| EXPECT_TRUE(specifics.has_display_name()); |
| EXPECT_EQ("", specifics.display_name()); |
| EXPECT_TRUE(specifics.has_avatar_url()); |
| EXPECT_EQ("", specifics.avatar_url()); |
| EXPECT_TRUE(specifics.has_federation_url()); |
| EXPECT_EQ("", specifics.federation_url()); |
| } |
| |
| // Serialize a PasswordForm with non-default member values and make sure the |
| // Sync representation is matching the expectations. |
| TEST_F(PasswordSyncableServiceTest, SerializeNonEmptyPasswordForm) { |
| autofill::PasswordForm form; |
| form.scheme = autofill::PasswordForm::SCHEME_USERNAME_ONLY; |
| form.signon_realm = "http://google.com/"; |
| form.origin = GURL("https://google.com/origin"); |
| form.action = GURL("https://google.com/action"); |
| form.username_element = base::ASCIIToUTF16("username_element"); |
| form.username_value = base::ASCIIToUTF16("god@google.com"); |
| form.password_element = base::ASCIIToUTF16("password_element"); |
| form.password_value = base::ASCIIToUTF16("!@#$%^&*()"); |
| form.preferred = true; |
| form.date_created = base::Time::FromInternalValue(100); |
| form.blacklisted_by_user = true; |
| form.type = autofill::PasswordForm::TYPE_LAST; |
| form.times_used = 11; |
| form.display_name = base::ASCIIToUTF16("Great Peter"); |
| form.icon_url = GURL("https://google.com/icon"); |
| form.federation_origin = url::Origin::Create(GURL("https://google.com/")); |
| |
| syncer::SyncData data = SyncDataFromPassword(form); |
| const sync_pb::PasswordSpecificsData& specifics = GetPasswordSpecifics(data); |
| EXPECT_TRUE(specifics.has_scheme()); |
| EXPECT_EQ(autofill::PasswordForm::SCHEME_USERNAME_ONLY, specifics.scheme()); |
| EXPECT_TRUE(specifics.has_signon_realm()); |
| EXPECT_EQ("http://google.com/", specifics.signon_realm()); |
| EXPECT_TRUE(specifics.has_origin()); |
| EXPECT_EQ("https://google.com/origin", specifics.origin()); |
| EXPECT_TRUE(specifics.has_action()); |
| EXPECT_EQ("https://google.com/action", specifics.action()); |
| EXPECT_TRUE(specifics.has_username_element()); |
| EXPECT_EQ("username_element", specifics.username_element()); |
| EXPECT_TRUE(specifics.has_username_value()); |
| EXPECT_EQ("god@google.com", specifics.username_value()); |
| EXPECT_TRUE(specifics.has_password_element()); |
| EXPECT_EQ("password_element", specifics.password_element()); |
| EXPECT_TRUE(specifics.has_password_value()); |
| EXPECT_EQ("!@#$%^&*()", specifics.password_value()); |
| EXPECT_TRUE(specifics.has_preferred()); |
| EXPECT_TRUE(specifics.preferred()); |
| EXPECT_TRUE(specifics.has_date_created()); |
| EXPECT_EQ(100, specifics.date_created()); |
| EXPECT_TRUE(specifics.has_blacklisted()); |
| EXPECT_TRUE(specifics.blacklisted()); |
| EXPECT_TRUE(specifics.has_type()); |
| EXPECT_EQ(autofill::PasswordForm::TYPE_LAST, specifics.type()); |
| EXPECT_TRUE(specifics.has_times_used()); |
| EXPECT_EQ(11, specifics.times_used()); |
| EXPECT_TRUE(specifics.has_display_name()); |
| EXPECT_EQ("Great Peter", specifics.display_name()); |
| EXPECT_TRUE(specifics.has_avatar_url()); |
| EXPECT_EQ("https://google.com/icon", specifics.avatar_url()); |
| EXPECT_TRUE(specifics.has_federation_url()); |
| EXPECT_EQ("https://google.com", specifics.federation_url()); |
| } |
| |
| } // namespace |
| |
| } // namespace password_manager |