OLD | NEW |
(Empty) | |
| 1 // Copyright (c) 2010 The Chromium Authors. All rights reserved. |
| 2 // Use of this source code is governed by a BSD-style license that can be |
| 3 // found in the LICENSE file. |
| 4 |
| 5 #include <vector> |
| 6 |
| 7 #include "testing/gtest/include/gtest/gtest.h" |
| 8 |
| 9 #include "base/task.h" |
| 10 #include "base/time.h" |
| 11 #include "base/utf_string_conversions.h" |
| 12 #include "base/waitable_event.h" |
| 13 #include "chrome/browser/password_manager/password_store.h" |
| 14 #include "chrome/browser/sync/engine/syncapi.h" |
| 15 #include "chrome/browser/sync/glue/password_change_processor.h" |
| 16 #include "chrome/browser/sync/glue/password_data_type_controller.h" |
| 17 #include "chrome/browser/sync/glue/password_model_associator.h" |
| 18 #include "chrome/browser/sync/glue/sync_backend_host_mock.h" |
| 19 #include "chrome/browser/sync/profile_sync_factory.h" |
| 20 #include "chrome/browser/sync/profile_sync_factory_mock.h" |
| 21 #include "chrome/browser/sync/profile_sync_service.h" |
| 22 #include "chrome/browser/sync/profile_sync_test_util.h" |
| 23 #include "chrome/browser/sync/protocol/password_specifics.pb.h" |
| 24 #include "chrome/browser/sync/syncable/directory_manager.h" |
| 25 #include "chrome/browser/sync/syncable/syncable.h" |
| 26 #include "chrome/browser/sync/test_profile_sync_service.h" |
| 27 #include "chrome/common/notification_source.h" |
| 28 #include "chrome/common/notification_type.h" |
| 29 #include "chrome/test/sync/engine/test_id_factory.h" |
| 30 #include "chrome/test/profile_mock.h" |
| 31 #include "testing/gmock/include/gmock/gmock.h" |
| 32 #include "webkit/glue/password_form.h" |
| 33 |
| 34 using base::Time; |
| 35 using base::WaitableEvent; |
| 36 using browser_sync::PasswordChangeProcessor; |
| 37 using browser_sync::PasswordDataTypeController; |
| 38 using browser_sync::PasswordModelAssociator; |
| 39 using browser_sync::SyncBackendHostMock; |
| 40 using browser_sync::TestIdFactory; |
| 41 using browser_sync::UnrecoverableErrorHandler; |
| 42 using sync_api::SyncManager; |
| 43 using sync_api::UserShare; |
| 44 using syncable::BASE_VERSION; |
| 45 using syncable::CREATE; |
| 46 using syncable::DirectoryManager; |
| 47 using syncable::ID; |
| 48 using syncable::IS_DEL; |
| 49 using syncable::IS_DIR; |
| 50 using syncable::IS_UNAPPLIED_UPDATE; |
| 51 using syncable::IS_UNSYNCED; |
| 52 using syncable::MutableEntry; |
| 53 using syncable::SERVER_IS_DIR; |
| 54 using syncable::SERVER_VERSION; |
| 55 using syncable::SPECIFICS; |
| 56 using syncable::ScopedDirLookup; |
| 57 using syncable::UNIQUE_SERVER_TAG; |
| 58 using syncable::UNITTEST; |
| 59 using syncable::WriteTransaction; |
| 60 using testing::_; |
| 61 using testing::DoAll; |
| 62 using testing::DoDefault; |
| 63 using testing::ElementsAre; |
| 64 using testing::Eq; |
| 65 using testing::Invoke; |
| 66 using testing::Return; |
| 67 using testing::SaveArg; |
| 68 using testing::SetArgumentPointee; |
| 69 using webkit_glue::PasswordForm; |
| 70 |
| 71 ACTION_P3(MakePasswordSyncComponents, service, ps, dtc) { |
| 72 DCHECK(ChromeThread::CurrentlyOn(ChromeThread::DB)); |
| 73 PasswordModelAssociator* model_associator = |
| 74 new PasswordModelAssociator(service, ps, dtc); |
| 75 PasswordChangeProcessor* change_processor = |
| 76 new PasswordChangeProcessor(model_associator, ps, dtc); |
| 77 return ProfileSyncFactory::SyncComponents(model_associator, |
| 78 change_processor); |
| 79 } |
| 80 |
| 81 class MockPasswordStore : public PasswordStore { |
| 82 public: |
| 83 MOCK_METHOD1(RemoveLogin, void(const PasswordForm&)); |
| 84 MOCK_METHOD2(GetLogins, int(const PasswordForm&, PasswordStoreConsumer*)); |
| 85 MOCK_METHOD1(AddLogin, void(const PasswordForm&)); |
| 86 MOCK_METHOD1(UpdateLogin, void(const PasswordForm&)); |
| 87 MOCK_METHOD1(AddLoginImpl, void(const PasswordForm&)); |
| 88 MOCK_METHOD1(UpdateLoginImpl, void(const PasswordForm&)); |
| 89 MOCK_METHOD1(RemoveLoginImpl, void(const PasswordForm&)); |
| 90 MOCK_METHOD2(RemoveLoginsCreatedBetweenImpl, void(const base::Time&, |
| 91 const base::Time&)); |
| 92 MOCK_METHOD2(GetLoginsImpl, void(GetLoginsRequest*, const PasswordForm&)); |
| 93 MOCK_METHOD1(GetAutofillableLoginsImpl, void(GetLoginsRequest*)); |
| 94 MOCK_METHOD1(GetBlacklistLoginsImpl, void(GetLoginsRequest*)); |
| 95 MOCK_METHOD1(FillAutofillableLogins, |
| 96 bool(std::vector<PasswordForm*>*)); |
| 97 MOCK_METHOD1(FillBlacklistLogins, |
| 98 bool(std::vector<PasswordForm*>*)); |
| 99 }; |
| 100 |
| 101 class ProfileSyncServicePasswordTest : public testing::Test { |
| 102 protected: |
| 103 ProfileSyncServicePasswordTest() |
| 104 : ui_thread_(ChromeThread::UI, &message_loop_), |
| 105 db_thread_(ChromeThread::DB) { |
| 106 } |
| 107 |
| 108 virtual void SetUp() { |
| 109 password_store_ = new MockPasswordStore(); |
| 110 db_thread_.Start(); |
| 111 |
| 112 notification_service_ = new ThreadNotificationService(&db_thread_); |
| 113 notification_service_->Init(); |
| 114 } |
| 115 |
| 116 virtual void TearDown() { |
| 117 service_.reset(); |
| 118 notification_service_->TearDown(); |
| 119 db_thread_.Stop(); |
| 120 MessageLoop::current()->RunAllPending(); |
| 121 } |
| 122 |
| 123 void StartSyncService(Task* task) { |
| 124 if (!service_.get()) { |
| 125 service_.reset(new TestProfileSyncService(&factory_, &profile_, |
| 126 false, false)); |
| 127 service_->AddObserver(&observer_); |
| 128 PasswordDataTypeController* data_type_controller = |
| 129 new PasswordDataTypeController(&factory_, |
| 130 &profile_, |
| 131 service_.get()); |
| 132 |
| 133 EXPECT_CALL(factory_, CreatePasswordSyncComponents(_, _, _)). |
| 134 WillOnce(MakePasswordSyncComponents(service_.get(), |
| 135 password_store_.get(), |
| 136 data_type_controller)); |
| 137 EXPECT_CALL(factory_, CreateDataTypeManager(_, _)). |
| 138 WillOnce(MakeDataTypeManager(&backend_)); |
| 139 |
| 140 EXPECT_CALL(profile_, GetPasswordStore(_)). |
| 141 WillOnce(Return(password_store_.get())); |
| 142 |
| 143 // State changes once for the backend init and once for startup done. |
| 144 EXPECT_CALL(observer_, OnStateChanged()). |
| 145 WillOnce(InvokeTask(task)). |
| 146 WillOnce(Return()). |
| 147 WillOnce(QuitUIMessageLoop()); |
| 148 service_->RegisterDataTypeController(data_type_controller); |
| 149 service_->Initialize(); |
| 150 MessageLoop::current()->Run(); |
| 151 } |
| 152 } |
| 153 |
| 154 void CreatePasswordRoot() { |
| 155 UserShare* user_share = service_->backend()->GetUserShareHandle(); |
| 156 DirectoryManager* dir_manager = user_share->dir_manager.get(); |
| 157 |
| 158 ScopedDirLookup dir(dir_manager, user_share->authenticated_name); |
| 159 ASSERT_TRUE(dir.good()); |
| 160 |
| 161 WriteTransaction wtrans(dir, UNITTEST, __FILE__, __LINE__); |
| 162 MutableEntry node(&wtrans, |
| 163 CREATE, |
| 164 wtrans.root_id(), |
| 165 browser_sync::kPasswordTag); |
| 166 node.Put(UNIQUE_SERVER_TAG, browser_sync::kPasswordTag); |
| 167 node.Put(IS_DIR, true); |
| 168 node.Put(SERVER_IS_DIR, false); |
| 169 node.Put(IS_UNSYNCED, false); |
| 170 node.Put(IS_UNAPPLIED_UPDATE, false); |
| 171 node.Put(SERVER_VERSION, 20); |
| 172 node.Put(BASE_VERSION, 20); |
| 173 node.Put(IS_DEL, false); |
| 174 node.Put(ID, ids_.MakeServer(browser_sync::kPasswordTag)); |
| 175 sync_pb::EntitySpecifics specifics; |
| 176 specifics.MutableExtension(sync_pb::password); |
| 177 node.Put(SPECIFICS, specifics); |
| 178 } |
| 179 |
| 180 void AddPasswordSyncNode(const PasswordForm& entry) { |
| 181 sync_api::WriteTransaction trans( |
| 182 service_->backend()->GetUserShareHandle()); |
| 183 sync_api::ReadNode password_root(&trans); |
| 184 ASSERT_TRUE(password_root.InitByTagLookup(browser_sync::kPasswordTag)); |
| 185 |
| 186 sync_api::WriteNode node(&trans); |
| 187 std::string tag = PasswordModelAssociator::MakeTag(entry); |
| 188 ASSERT_TRUE(node.InitUniqueByCreation(syncable::PASSWORD, |
| 189 password_root, |
| 190 tag)); |
| 191 PasswordModelAssociator::WriteToSyncNode(entry, &node); |
| 192 } |
| 193 |
| 194 void GetPasswordEntriesFromSyncDB(std::vector<PasswordForm>* entries) { |
| 195 sync_api::ReadTransaction trans(service_->backend()->GetUserShareHandle()); |
| 196 sync_api::ReadNode password_root(&trans); |
| 197 ASSERT_TRUE(password_root.InitByTagLookup(browser_sync::kPasswordTag)); |
| 198 |
| 199 int64 child_id = password_root.GetFirstChildId(); |
| 200 while (child_id != sync_api::kInvalidId) { |
| 201 sync_api::ReadNode child_node(&trans); |
| 202 ASSERT_TRUE(child_node.InitByIdLookup(child_id)); |
| 203 |
| 204 sync_pb::PasswordSpecificsData password; |
| 205 ASSERT_TRUE(child_node.GetPasswordSpecifics(&password)); |
| 206 |
| 207 PasswordForm form; |
| 208 PasswordModelAssociator::CopyPassword(password, &form); |
| 209 |
| 210 entries->push_back(form); |
| 211 |
| 212 child_id = child_node.GetSuccessorId(); |
| 213 } |
| 214 } |
| 215 |
| 216 bool ComparePasswords(const PasswordForm& lhs, const PasswordForm& rhs) { |
| 217 return lhs.scheme == rhs.scheme && |
| 218 lhs.signon_realm == rhs.signon_realm && |
| 219 lhs.origin == rhs.origin && |
| 220 lhs.action == rhs.action && |
| 221 lhs.username_element == rhs.username_element && |
| 222 lhs.username_value == rhs.username_value && |
| 223 lhs.password_element == rhs.password_element && |
| 224 lhs.password_value == rhs.password_value && |
| 225 lhs.ssl_valid == rhs.ssl_valid && |
| 226 lhs.preferred == rhs.preferred && |
| 227 lhs.date_created == rhs.date_created && |
| 228 lhs.blacklisted_by_user == rhs.blacklisted_by_user; |
| 229 } |
| 230 |
| 231 void SetIdleChangeProcessorExpectations() { |
| 232 EXPECT_CALL(*(password_store_.get()), AddLoginImpl(_)).Times(0); |
| 233 EXPECT_CALL(*(password_store_.get()), UpdateLoginImpl(_)).Times(0); |
| 234 EXPECT_CALL(*(password_store_.get()), RemoveLoginImpl(_)).Times(0); |
| 235 } |
| 236 |
| 237 friend class CreatePasswordRootTask; |
| 238 friend class AddPasswordEntriesTask; |
| 239 |
| 240 MessageLoopForUI message_loop_; |
| 241 ChromeThread ui_thread_; |
| 242 ChromeThread db_thread_; |
| 243 scoped_refptr<ThreadNotificationService> notification_service_; |
| 244 |
| 245 scoped_ptr<TestProfileSyncService> service_; |
| 246 ProfileMock profile_; |
| 247 ProfileSyncFactoryMock factory_; |
| 248 ProfileSyncServiceObserverMock observer_; |
| 249 SyncBackendHostMock backend_; |
| 250 scoped_refptr<MockPasswordStore> password_store_; |
| 251 |
| 252 TestIdFactory ids_; |
| 253 }; |
| 254 |
| 255 class CreatePasswordRootTask : public Task { |
| 256 public: |
| 257 explicit CreatePasswordRootTask(ProfileSyncServicePasswordTest* test) |
| 258 : test_(test) { |
| 259 } |
| 260 |
| 261 virtual void Run() { |
| 262 test_->CreatePasswordRoot(); |
| 263 } |
| 264 |
| 265 private: |
| 266 ProfileSyncServicePasswordTest* test_; |
| 267 }; |
| 268 |
| 269 class AddPasswordEntriesTask : public Task { |
| 270 public: |
| 271 AddPasswordEntriesTask(ProfileSyncServicePasswordTest* test, |
| 272 const std::vector<PasswordForm>& entries) |
| 273 : test_(test), entries_(entries) { |
| 274 } |
| 275 |
| 276 virtual void Run() { |
| 277 test_->CreatePasswordRoot(); |
| 278 for (size_t i = 0; i < entries_.size(); ++i) { |
| 279 test_->AddPasswordSyncNode(entries_[i]); |
| 280 } |
| 281 } |
| 282 |
| 283 private: |
| 284 ProfileSyncServicePasswordTest* test_; |
| 285 const std::vector<PasswordForm>& entries_; |
| 286 }; |
| 287 |
| 288 TEST_F(ProfileSyncServicePasswordTest, FailModelAssociation) { |
| 289 // Backend will be paused but not resumed. |
| 290 EXPECT_CALL(backend_, RequestPause()). |
| 291 WillOnce(testing::DoAll(Notify(NotificationType::SYNC_PAUSED), |
| 292 testing::Return(true))); |
| 293 // Don't create the root password node so startup fails. |
| 294 StartSyncService(NULL); |
| 295 EXPECT_TRUE(service_->unrecoverable_error_detected()); |
| 296 } |
| 297 |
| 298 TEST_F(ProfileSyncServicePasswordTest, EmptyNativeEmptySync) { |
| 299 EXPECT_CALL(*(password_store_.get()), FillAutofillableLogins(_)) |
| 300 .WillOnce(Return(true)); |
| 301 EXPECT_CALL(*(password_store_.get()), FillBlacklistLogins(_)) |
| 302 .WillOnce(Return(true)); |
| 303 SetIdleChangeProcessorExpectations(); |
| 304 CreatePasswordRootTask task(this); |
| 305 StartSyncService(&task); |
| 306 std::vector<PasswordForm> sync_entries; |
| 307 GetPasswordEntriesFromSyncDB(&sync_entries); |
| 308 EXPECT_EQ(0U, sync_entries.size()); |
| 309 } |
| 310 |
| 311 TEST_F(ProfileSyncServicePasswordTest, HasNativeEntriesEmptySync) { |
| 312 std::vector<PasswordForm*> forms; |
| 313 std::vector<PasswordForm> expected_forms; |
| 314 PasswordForm* new_form = new PasswordForm; |
| 315 new_form->scheme = PasswordForm::SCHEME_HTML; |
| 316 new_form->signon_realm = "pie"; |
| 317 new_form->origin = GURL("http://pie.com"); |
| 318 new_form->action = GURL("http://pie.com/submit"); |
| 319 new_form->username_element = UTF8ToUTF16("name"); |
| 320 new_form->username_value = UTF8ToUTF16("tom"); |
| 321 new_form->password_element = UTF8ToUTF16("cork"); |
| 322 new_form->password_value = UTF8ToUTF16("password1"); |
| 323 new_form->ssl_valid = true; |
| 324 new_form->preferred = false; |
| 325 new_form->date_created = base::Time::FromInternalValue(1234); |
| 326 new_form->blacklisted_by_user = false; |
| 327 forms.push_back(new_form); |
| 328 expected_forms.push_back(*new_form); |
| 329 EXPECT_CALL(*(password_store_.get()), FillAutofillableLogins(_)) |
| 330 .WillOnce(DoAll(SetArgumentPointee<0>(forms), Return(true))); |
| 331 EXPECT_CALL(*(password_store_.get()), FillBlacklistLogins(_)) |
| 332 .WillOnce(Return(true)); |
| 333 SetIdleChangeProcessorExpectations(); |
| 334 CreatePasswordRootTask task(this); |
| 335 StartSyncService(&task); |
| 336 std::vector<PasswordForm> sync_forms; |
| 337 GetPasswordEntriesFromSyncDB(&sync_forms); |
| 338 ASSERT_EQ(1U, sync_forms.size()); |
| 339 EXPECT_TRUE(ComparePasswords(expected_forms[0], sync_forms[0])); |
| 340 } |
| 341 |
| 342 TEST_F(ProfileSyncServicePasswordTest, HasNativeHasSyncNoMerge) { |
| 343 std::vector<PasswordForm*> native_forms; |
| 344 std::vector<PasswordForm> sync_forms; |
| 345 std::vector<PasswordForm> expected_forms; |
| 346 { |
| 347 PasswordForm* new_form = new PasswordForm; |
| 348 new_form->scheme = PasswordForm::SCHEME_HTML; |
| 349 new_form->signon_realm = "pie"; |
| 350 new_form->origin = GURL("http://pie.com"); |
| 351 new_form->action = GURL("http://pie.com/submit"); |
| 352 new_form->username_element = UTF8ToUTF16("name"); |
| 353 new_form->username_value = UTF8ToUTF16("tom"); |
| 354 new_form->password_element = UTF8ToUTF16("cork"); |
| 355 new_form->password_value = UTF8ToUTF16("password1"); |
| 356 new_form->ssl_valid = true; |
| 357 new_form->preferred = false; |
| 358 new_form->date_created = base::Time::FromInternalValue(1234); |
| 359 new_form->blacklisted_by_user = false; |
| 360 |
| 361 native_forms.push_back(new_form); |
| 362 expected_forms.push_back(*new_form); |
| 363 } |
| 364 |
| 365 { |
| 366 PasswordForm new_form; |
| 367 new_form.scheme = PasswordForm::SCHEME_HTML; |
| 368 new_form.signon_realm = "pie2"; |
| 369 new_form.origin = GURL("http://pie2.com"); |
| 370 new_form.action = GURL("http://pie2.com/submit"); |
| 371 new_form.username_element = UTF8ToUTF16("name2"); |
| 372 new_form.username_value = UTF8ToUTF16("tom2"); |
| 373 new_form.password_element = UTF8ToUTF16("cork2"); |
| 374 new_form.password_value = UTF8ToUTF16("password12"); |
| 375 new_form.ssl_valid = false; |
| 376 new_form.preferred = true; |
| 377 new_form.date_created = base::Time::FromInternalValue(12345); |
| 378 new_form.blacklisted_by_user = false; |
| 379 sync_forms.push_back(new_form); |
| 380 expected_forms.push_back(new_form); |
| 381 } |
| 382 |
| 383 EXPECT_CALL(*(password_store_.get()), FillAutofillableLogins(_)) |
| 384 .WillOnce(DoAll(SetArgumentPointee<0>(native_forms), Return(true))); |
| 385 EXPECT_CALL(*(password_store_.get()), FillBlacklistLogins(_)) |
| 386 .WillOnce(Return(true)); |
| 387 |
| 388 AddPasswordEntriesTask task(this, sync_forms); |
| 389 |
| 390 EXPECT_CALL(*(password_store_.get()), AddLoginImpl(_)).Times(1); |
| 391 StartSyncService(&task); |
| 392 |
| 393 std::vector<PasswordForm> new_sync_forms; |
| 394 GetPasswordEntriesFromSyncDB(&new_sync_forms); |
| 395 |
| 396 EXPECT_EQ(2U, new_sync_forms.size()); |
| 397 EXPECT_TRUE(ComparePasswords(expected_forms[0], new_sync_forms[0])); |
| 398 EXPECT_TRUE(ComparePasswords(expected_forms[1], new_sync_forms[1])); |
| 399 } |
| 400 |
| 401 TEST_F(ProfileSyncServicePasswordTest, HasNativeHasSyncMergeEntry) { |
| 402 std::vector<PasswordForm*> native_forms; |
| 403 std::vector<PasswordForm> sync_forms; |
| 404 std::vector<PasswordForm> expected_forms; |
| 405 { |
| 406 PasswordForm* new_form = new PasswordForm; |
| 407 new_form->scheme = PasswordForm::SCHEME_HTML; |
| 408 new_form->signon_realm = "pie"; |
| 409 new_form->origin = GURL("http://pie.com"); |
| 410 new_form->action = GURL("http://pie.com/submit"); |
| 411 new_form->username_element = UTF8ToUTF16("name"); |
| 412 new_form->username_value = UTF8ToUTF16("tom"); |
| 413 new_form->password_element = UTF8ToUTF16("cork"); |
| 414 new_form->password_value = UTF8ToUTF16("password1"); |
| 415 new_form->ssl_valid = true; |
| 416 new_form->preferred = false; |
| 417 new_form->date_created = base::Time::FromInternalValue(1234); |
| 418 new_form->blacklisted_by_user = false; |
| 419 |
| 420 native_forms.push_back(new_form); |
| 421 } |
| 422 |
| 423 { |
| 424 PasswordForm new_form; |
| 425 new_form.scheme = PasswordForm::SCHEME_HTML; |
| 426 new_form.signon_realm = "pie"; |
| 427 new_form.origin = GURL("http://pie.com"); |
| 428 new_form.action = GURL("http://pie.com/submit"); |
| 429 new_form.username_element = UTF8ToUTF16("name2"); |
| 430 new_form.username_value = UTF8ToUTF16("tom2"); |
| 431 new_form.password_element = UTF8ToUTF16("cork2"); |
| 432 new_form.password_value = UTF8ToUTF16("password12"); |
| 433 new_form.ssl_valid = false; |
| 434 new_form.preferred = true; |
| 435 new_form.date_created = base::Time::FromInternalValue(12345); |
| 436 new_form.blacklisted_by_user = false; |
| 437 sync_forms.push_back(new_form); |
| 438 } |
| 439 |
| 440 { |
| 441 PasswordForm new_form; |
| 442 new_form.scheme = PasswordForm::SCHEME_HTML; |
| 443 new_form.signon_realm = "pie"; |
| 444 new_form.origin = GURL("http://pie.com"); |
| 445 new_form.action = GURL("http://pie.com/submit"); |
| 446 new_form.username_element = UTF8ToUTF16("name2"); |
| 447 new_form.username_value = UTF8ToUTF16("tom2"); |
| 448 new_form.password_element = UTF8ToUTF16("cork2"); |
| 449 new_form.password_value = UTF8ToUTF16("password12"); |
| 450 new_form.ssl_valid = false; |
| 451 new_form.preferred = true; |
| 452 new_form.date_created = base::Time::FromInternalValue(12345); |
| 453 new_form.blacklisted_by_user = false; |
| 454 expected_forms.push_back(new_form); |
| 455 } |
| 456 |
| 457 EXPECT_CALL(*(password_store_.get()), FillAutofillableLogins(_)) |
| 458 .WillOnce(DoAll(SetArgumentPointee<0>(native_forms), Return(true))); |
| 459 EXPECT_CALL(*(password_store_.get()), FillBlacklistLogins(_)) |
| 460 .WillOnce(Return(true)); |
| 461 |
| 462 AddPasswordEntriesTask task(this, sync_forms); |
| 463 |
| 464 EXPECT_CALL(*(password_store_.get()), UpdateLoginImpl(_)).Times(1); |
| 465 StartSyncService(&task); |
| 466 |
| 467 std::vector<PasswordForm> new_sync_forms; |
| 468 GetPasswordEntriesFromSyncDB(&new_sync_forms); |
| 469 |
| 470 EXPECT_EQ(1U, new_sync_forms.size()); |
| 471 EXPECT_TRUE(ComparePasswords(expected_forms[0], new_sync_forms[0])); |
| 472 } |
OLD | NEW |