Chromium Code Reviews| OLD | NEW |
|---|---|
| 1 // Copyright 2016 The Chromium Authors. All rights reserved. | 1 // Copyright 2012 The Chromium Authors. All rights reserved. |
| 2 // Use of this source code is governed by a BSD-style license that can be | 2 // Use of this source code is governed by a BSD-style license that can be |
| 3 // found in the LICENSE file. | 3 // found in the LICENSE file. |
| 4 | 4 |
| 5 #import "ios/chrome/browser/passwords/password_controller.h" | 5 #import "ios/chrome/browser/passwords/password_controller.h" |
| 6 | 6 |
| 7 #import <Foundation/Foundation.h> | |
| 8 | |
| 7 #include <memory> | 9 #include <memory> |
| 8 #include <vector> | 10 #include <utility> |
| 9 | 11 |
| 12 #include "base/json/json_reader.h" | |
| 13 #include "base/mac/bind_objc_block.h" | |
| 10 #include "base/mac/scoped_nsobject.h" | 14 #include "base/mac/scoped_nsobject.h" |
| 11 #include "base/memory/ptr_util.h" | 15 #include "base/memory/ptr_util.h" |
| 16 #include "base/memory/ref_counted.h" | |
| 17 #include "base/strings/sys_string_conversions.h" | |
| 18 #include "base/strings/utf_string_conversions.h" | |
| 19 #import "base/test/ios/wait_util.h" | |
| 20 #include "base/values.h" | |
| 21 #include "components/autofill/core/common/password_form_fill_data.h" | |
| 12 #include "components/password_manager/core/browser/log_manager.h" | 22 #include "components/password_manager/core/browser/log_manager.h" |
| 13 #include "components/password_manager/core/browser/password_form_manager.h" | 23 #include "components/password_manager/core/browser/mock_password_store.h" |
| 14 #include "components/password_manager/core/browser/stub_password_manager_client. h" | 24 #include "components/password_manager/core/browser/stub_password_manager_client. h" |
| 15 #include "components/password_manager/core/common/password_manager_pref_names.h" | 25 #include "components/password_manager/core/common/password_manager_pref_names.h" |
| 16 #include "components/syncable_prefs/testing_pref_service_syncable.h" | 26 #include "components/prefs/pref_registry_simple.h" |
| 27 #include "components/prefs/testing_pref_service.h" | |
| 28 #import "ios/chrome/browser/autofill/form_input_accessory_view_controller.h" | |
| 29 #import "ios/chrome/browser/autofill/form_suggestion_controller.h" | |
| 17 #include "ios/chrome/browser/browser_state/test_chrome_browser_state.h" | 30 #include "ios/chrome/browser/browser_state/test_chrome_browser_state.h" |
| 31 #import "ios/chrome/browser/passwords/js_password_manager.h" | |
| 32 #import "ios/web/public/web_state/web_state.h" | |
| 33 #import "ios/web/public/test/web_test_with_web_state.h" | |
| 18 #import "ios/web/public/test/test_web_state.h" | 34 #import "ios/web/public/test/test_web_state.h" |
| 19 #include "testing/gmock/include/gmock/gmock.h" | 35 #include "testing/gmock/include/gmock/gmock.h" |
| 20 #include "testing/gtest/include/gtest/gtest.h" | 36 #include "testing/gtest/include/gtest/gtest.h" |
| 37 #include "testing/gtest_mac.h" | |
| 38 #import "third_party/ocmock/OCMock/OCMock.h" | |
| 39 #import "third_party/ocmock/OCMock/OCPartialMockObject.h" | |
| 21 #include "url/gurl.h" | 40 #include "url/gurl.h" |
| 22 | 41 |
| 23 using testing::_; | 42 using autofill::PasswordForm; |
| 43 using autofill::PasswordFormFillData; | |
| 24 using testing::Return; | 44 using testing::Return; |
| 25 | 45 |
| 46 namespace { | |
| 47 | |
| 26 class MockWebState : public web::TestWebState { | 48 class MockWebState : public web::TestWebState { |
| 27 public: | 49 public: |
| 28 MOCK_CONST_METHOD0(GetBrowserState, web::BrowserState*(void)); | 50 MOCK_CONST_METHOD0(GetBrowserState, web::BrowserState*(void)); |
| 29 }; | 51 }; |
| 30 | 52 |
| 31 class MockPasswordManagerClient | 53 class MockPasswordManagerClient |
| 32 : public password_manager::StubPasswordManagerClient { | 54 : public password_manager::StubPasswordManagerClient { |
| 33 public: | 55 public: |
| 34 // |form_manager| stays owned by the mock. | 56 explicit MockPasswordManagerClient(password_manager::PasswordStore* store) |
| 35 MOCK_METHOD3(PromptUserToSaveOrUpdatePasswordPtr, | 57 : store_(store) {} |
| 36 void(password_manager::PasswordFormManager* form_manager, | 58 |
| 37 password_manager::CredentialSourceType type, | 59 ~MockPasswordManagerClient() override = default; |
| 38 bool update_password)); | 60 |
| 39 MOCK_CONST_METHOD0(GetLogManager, password_manager::LogManager*(void)); | 61 MOCK_CONST_METHOD0(GetLogManager, password_manager::LogManager*(void)); |
| 40 | 62 |
| 41 // Workaround for std::unique_ptr<> lacking a copy constructor. | 63 PrefService* GetPrefs() override { return &prefs_; } |
| 42 bool PromptUserToSaveOrUpdatePassword( | 64 |
| 43 std::unique_ptr<password_manager::PasswordFormManager> manager, | 65 password_manager::PasswordStore* GetPasswordStore() const override { |
| 44 password_manager::CredentialSourceType type, | 66 return store_; |
| 45 bool update_password) override { | |
| 46 PromptUserToSaveOrUpdatePasswordPtr(manager.get(), type, update_password); | |
| 47 return false; | |
| 48 } | 67 } |
| 68 | |
| 69 private: | |
| 70 TestingPrefServiceSimple prefs_; | |
| 71 password_manager::PasswordStore* const store_; | |
| 49 }; | 72 }; |
| 50 | 73 |
| 51 class MockLogManager : public password_manager::LogManager { | 74 class MockLogManager : public password_manager::LogManager { |
| 52 public: | 75 public: |
| 53 MOCK_CONST_METHOD1(LogSavePasswordProgress, void(const std::string& text)); | 76 MOCK_CONST_METHOD1(LogSavePasswordProgress, void(const std::string& text)); |
| 54 MOCK_CONST_METHOD0(IsLoggingActive, bool(void)); | 77 MOCK_CONST_METHOD0(IsLoggingActive, bool(void)); |
| 55 | 78 |
| 56 // Methods not important for testing. | 79 // Methods not important for testing. |
| 57 void OnLogRouterAvailabilityChanged(bool router_can_be_used) override {} | 80 void OnLogRouterAvailabilityChanged(bool router_can_be_used) override {} |
| 58 void SetSuspended(bool suspended) override {} | 81 void SetSuspended(bool suspended) override {} |
| 59 }; | 82 }; |
| 60 | 83 |
| 61 TEST(PasswordControllerTest, SaveOnNonHTMLLandingPage) { | 84 // Creates PasswordController with the given |web_state| and a mock client |
| 62 // Create the PasswordController with a MockPasswordManagerClient. | 85 // using the given |store|. If not null, |weak_client| is filled with a |
| 86 // non-owning pointer to the created client. The created controller is | |
| 87 // returned. | |
| 88 base::scoped_nsobject<PasswordController> CreatePasswordController( | |
| 89 web::WebState* web_state, | |
| 90 password_manager::PasswordStore* store, | |
| 91 MockPasswordManagerClient** weak_client) { | |
| 92 auto client = base::WrapUnique(new MockPasswordManagerClient(store)); | |
| 93 if (weak_client) | |
| 94 *weak_client = client.get(); | |
| 95 return base::scoped_nsobject<PasswordController>([[PasswordController alloc] | |
| 96 initWithWebState:web_state | |
| 97 passwordsUiDelegate:nil | |
| 98 client:std::move(client)]); | |
| 99 } | |
| 100 | |
| 101 } // namespace | |
| 102 | |
| 103 @interface PasswordController ( | |
| 104 Testing)<CRWWebStateObserver, FormSuggestionProvider> | |
| 105 | |
| 106 - (void)findPasswordFormsWithCompletionHandler: | |
| 107 (void (^)(const std::vector<PasswordForm>&))completionHandler; | |
| 108 | |
| 109 - (void)extractSubmittedPasswordForm:(const std::string&)formName | |
| 110 completionHandler: | |
| 111 (void (^)(BOOL found, | |
| 112 const PasswordForm& form))completionHandler; | |
| 113 | |
| 114 - (void)fillPasswordForm:(const PasswordFormFillData&)formData | |
| 115 completionHandler:(void (^)(BOOL))completionHandler; | |
| 116 | |
| 117 - (BOOL)getPasswordForm:(PasswordForm*)form | |
| 118 fromDictionary:(const base::DictionaryValue*)dictionary | |
| 119 pageURL:(const GURL&)pageLocation; | |
| 120 | |
| 121 // Provides access to JavaScript Manager for testing with mocks. | |
| 122 @property(readonly) JsPasswordManager* passwordJsManager; | |
| 123 | |
| 124 @end | |
| 125 | |
| 126 // Real FormSuggestionController is wrapped to register the addition of | |
| 127 // suggestions. | |
| 128 @interface PasswordsTestSuggestionController : FormSuggestionController | |
| 129 | |
| 130 @property(nonatomic, copy) NSArray* suggestions; | |
| 131 | |
| 132 - (void)dealloc; | |
| 133 | |
| 134 @end | |
| 135 | |
| 136 @implementation PasswordsTestSuggestionController | |
| 137 | |
| 138 @synthesize suggestions = _suggestions; | |
| 139 | |
| 140 - (void)updateKeyboardWithSuggestions:(NSArray*)suggestions { | |
| 141 self.suggestions = suggestions; | |
| 142 } | |
| 143 | |
| 144 - (void)dealloc { | |
| 145 [_suggestions release]; | |
| 146 [super dealloc]; | |
| 147 } | |
| 148 | |
| 149 @end | |
| 150 | |
| 151 class PasswordControllerTest : public web::WebTestWithWebState { | |
| 152 public: | |
| 153 PasswordControllerTest() | |
| 154 : store_(new testing::NiceMock<password_manager::MockPasswordStore>()) {} | |
| 155 | |
| 156 ~PasswordControllerTest() override { store_->ShutdownOnUIThread(); } | |
| 157 | |
| 158 void SetUp() override { | |
| 159 web::WebTestWithWebState::SetUp(); | |
| 160 passwordController_ = | |
| 161 CreatePasswordController(web_state(), store_.get(), nullptr); | |
| 162 @autoreleasepool { | |
| 163 // Make sure the temporary array is released after SetUp finishes, | |
| 164 // otherwise [passwordController_ suggestionProvider] will be retained | |
| 165 // until PlatformTest teardown, at which point all Chrome objects are | |
| 166 // already gone and teardown may access invalid memory. | |
| 167 suggestionController_.reset([[PasswordsTestSuggestionController alloc] | |
| 168 initWithWebState:web_state() | |
| 169 providers:@[ [passwordController_ suggestionProvider] ]]); | |
| 170 accessoryViewController_.reset([[FormInputAccessoryViewController alloc] | |
| 171 initWithWebState:web_state() | |
| 172 providers:@[ [suggestionController_ accessoryViewProvider] ]]); | |
| 173 } | |
| 174 } | |
| 175 | |
| 176 protected: | |
| 177 // Helper method for PasswordControllerTest.DontFillReadonly. Tries to load | |
| 178 // |html| and find and fill there a form with hard-coded form data. Returns | |
| 179 // YES on success, NO otherwise. | |
| 180 BOOL BasicFormFill(NSString* html); | |
| 181 | |
| 182 // Retrieve the current suggestions from suggestionController_ sorted in | |
| 183 // alphabetical order according to their value properties. | |
| 184 NSArray* GetSortedSuggestionValues() { | |
| 185 NSMutableArray* suggestion_values = [NSMutableArray array]; | |
| 186 for (FormSuggestion* suggestion in [suggestionController_ suggestions]) | |
| 187 [suggestion_values addObject:suggestion.value]; | |
| 188 return [suggestion_values | |
| 189 sortedArrayUsingSelector:@selector(localizedCaseInsensitiveCompare:)]; | |
| 190 } | |
| 191 | |
| 192 // Returns an identifier for the |form_number|th form in the page. | |
| 193 std::string FormName(int form_number) { | |
| 194 NSString* kFormNamingScript = | |
| 195 @"__gCrWeb.common.getFormIdentifier(" | |
| 196 " document.querySelectorAll('form')[%d]);"; | |
| 197 return base::SysNSStringToUTF8(EvaluateJavaScriptAsString( | |
| 198 [NSString stringWithFormat:kFormNamingScript, form_number])); | |
| 199 } | |
| 200 | |
| 201 // Sets up a partial mock that intercepts calls to the selector | |
| 202 // -fillPasswordForm:withUsername:password:completionHandler: to the | |
| 203 // PasswordController's JavaScript manager. For the first | |
| 204 // |target_failure_count| calls, skips the invocation of the real JavaScript | |
| 205 // manager, giving the effect that password form fill failed. As soon as | |
| 206 // |failure_count| reaches |target_failure_count|, stop the partial mock | |
| 207 // and let the original JavaScript manager execute. | |
| 208 void SetFillPasswordFormFailureCount(int target_failure_count) { | |
| 209 id original_manager = [passwordController_ passwordJsManager]; | |
| 210 OCPartialMockObject* failing_manager = | |
| 211 [OCMockObject partialMockForObject:original_manager]; | |
| 212 __block int failure_count = 0; | |
| 213 void (^fail_invocation)(NSInvocation*) = ^(NSInvocation* invocation) { | |
| 214 if (failure_count >= target_failure_count) { | |
| 215 [failing_manager stop]; | |
| 216 [invocation invokeWithTarget:original_manager]; | |
| 217 } else { | |
| 218 ++failure_count; | |
| 219 // Fetches the completion handler from |invocation| and calls it with | |
| 220 // failure status. | |
| 221 void (^completionHandler)(BOOL); | |
| 222 const NSInteger kArgOffset = 1; | |
| 223 const NSInteger kCompletionHandlerArgIndex = 4; | |
| 224 [invocation getArgument:&completionHandler | |
| 225 atIndex:(kCompletionHandlerArgIndex + kArgOffset)]; | |
| 226 ASSERT_TRUE(completionHandler); | |
| 227 completionHandler(NO); | |
| 228 } | |
| 229 }; | |
| 230 [[[failing_manager stub] andDo:fail_invocation] | |
| 231 fillPasswordForm:[OCMArg any] | |
| 232 withUsername:[OCMArg any] | |
| 233 password:[OCMArg any] | |
| 234 completionHandler:[OCMArg any]]; | |
| 235 } | |
| 236 | |
| 237 // SuggestionController for testing. | |
| 238 base::scoped_nsobject<PasswordsTestSuggestionController> | |
| 239 suggestionController_; | |
| 240 | |
| 241 // FormInputAccessoryViewController for testing. | |
| 242 base::scoped_nsobject<FormInputAccessoryViewController> | |
| 243 accessoryViewController_; | |
| 244 | |
| 245 // PasswordController for testing. | |
| 246 base::scoped_nsobject<PasswordController> passwordController_; | |
| 247 | |
| 248 scoped_refptr<password_manager::PasswordStore> store_; | |
| 249 }; | |
| 250 | |
| 251 struct PasswordFormTestData { | |
| 252 const char* const page_location; | |
| 253 const char* const json_string; | |
| 254 const char* const expected_origin; | |
| 255 const char* const expected_action; | |
| 256 const char* const expected_username_element; | |
| 257 const char* const expected_username_value; | |
| 258 const char* const expected_new_password_element; | |
| 259 const char* const expected_new_password_value; | |
| 260 const char* const expected_old_password_element; | |
| 261 const char* const expected_old_password_value; | |
| 262 }; | |
| 263 | |
| 264 TEST_F(PasswordControllerTest, PopulatePasswordFormWithDictionary) { | |
|
Eugene But (OOO till 7-30)
2016/07/13 22:30:31
If you understand the purpose if these tests, coul
vabr (Chromium)
2016/07/14 07:36:31
Done.
| |
| 265 // clang-format off | |
| 266 PasswordFormTestData test_data[] = { | |
| 267 // One username element, one password element. URLs contain extra | |
| 268 // parts: username/password, query, reference, which are all expected | |
| 269 // to be stripped off. The password is recognized as an old password. | |
| 270 { | |
| 271 "http://john:doe@fakedomain.com/foo/bar?baz=quz#foobar", | |
| 272 "{ \"action\": \"some/action?to=be&or=not#tobe\"," | |
| 273 "\"usernameElement\": \"account\"," | |
| 274 "\"usernameValue\": \"fakeaccount\"," | |
| 275 "\"name\": \"signup\"," | |
| 276 "\"origin\": \"http://john:doe@fakedomain.com/foo/bar\"," | |
| 277 "\"passwords\": [" | |
| 278 "{ \"element\": \"secret\"," "\"value\": \"fakesecret\" }," | |
| 279 "]}", | |
| 280 "http://fakedomain.com/foo/bar", | |
| 281 "http://fakedomain.com/foo/some/action", | |
| 282 "account", | |
| 283 "fakeaccount", | |
| 284 "", | |
| 285 "", | |
| 286 "secret", | |
| 287 "fakesecret", | |
| 288 }, | |
| 289 // One username element, one password element. Population should fail | |
| 290 // due to an origin mismatch. | |
| 291 { | |
| 292 "http://john:doe@fakedomain.com/foo/bar?baz=quz#foobar", | |
| 293 "{ \"action\": \"some/action?to=be&or=not#tobe\"," | |
| 294 "\"usernameElement\": \"account\"," | |
| 295 "\"usernameValue\": \"fakeaccount\"," | |
| 296 "\"name\": \"signup\"," | |
| 297 "\"origin\": \"http://john:doe@realdomainipromise.com/foo/bar\"," | |
| 298 "\"passwords\": [" | |
| 299 "{ \"element\": \"secret\"," "\"value\": \"fakesecret\" }," | |
| 300 "]}", | |
| 301 "", | |
| 302 "", | |
| 303 "", | |
| 304 "", | |
| 305 "", | |
| 306 "", | |
| 307 "", | |
| 308 "", | |
| 309 }, | |
| 310 // One username element, two password elements. Since both password | |
| 311 // values are the same, we are assuming that the webpage asked the user | |
| 312 // to enter the password twice for confirmation. | |
| 313 { | |
| 314 "http://fakedomain.com/foo", | |
| 315 "{ \"action\": \"http://anotherdomain.com/some_action\"," | |
| 316 "\"usernameElement\": \"account\"," | |
| 317 "\"usernameValue\": \"fakeaccount\"," | |
| 318 "\"name\": \"signup\"," | |
| 319 "\"origin\": \"http://fakedomain.com/foo\"," | |
| 320 "\"passwords\": [" | |
| 321 "{ \"element\": \"secret\"," "\"value\": \"fakesecret\" }," | |
| 322 "{ \"element\": \"confirm\"," "\"value\": \"fakesecret\" }," | |
| 323 "]}", | |
| 324 "http://fakedomain.com/foo", | |
| 325 "http://anotherdomain.com/some_action", | |
| 326 "account", | |
| 327 "fakeaccount", | |
| 328 "secret", | |
| 329 "fakesecret", | |
| 330 "", | |
| 331 "", | |
| 332 }, | |
| 333 // One username element, two password elements. The password | |
| 334 // values are different, so we are assuming that the webpage asked the user | |
| 335 // to enter the old password and new password. | |
| 336 { | |
| 337 "http://fakedomain.com/foo", | |
| 338 "{ \"action\": \"\"," | |
| 339 "\"usernameElement\": \"account\"," | |
| 340 "\"usernameValue\": \"fakeaccount\"," | |
| 341 "\"name\": \"signup\"," | |
| 342 "\"origin\": \"http://fakedomain.com/foo\"," | |
| 343 "\"passwords\": [" | |
| 344 "{ \"element\": \"old\"," "\"value\": \"oldsecret\" }," | |
| 345 "{ \"element\": \"new\"," "\"value\": \"newsecret\" }," | |
| 346 "]}", | |
| 347 "http://fakedomain.com/foo", | |
| 348 "http://fakedomain.com/foo", | |
| 349 "account", | |
| 350 "fakeaccount", | |
| 351 "new", | |
| 352 "newsecret", | |
| 353 "old", | |
| 354 "oldsecret", | |
| 355 }, | |
| 356 // One username element, three password elements. All passwords | |
| 357 // are the same. Password population should fail because this configuration | |
| 358 // does not make sense. | |
| 359 { | |
| 360 "http://fakedomain.com", | |
| 361 "{ \"action\": \"\"," | |
| 362 "\"usernameElement\": \"account\"," | |
| 363 "\"usernameValue\": \"fakeaccount\"," | |
| 364 "\"name\": \"signup\"," | |
| 365 "\"origin\": \"http://fakedomain.com/foo\"," | |
| 366 "\"passwords\": [" | |
| 367 "{ \"element\": \"pass1\"," "\"value\": \"word\" }," | |
| 368 "{ \"element\": \"pass2\"," "\"value\": \"word\" }," | |
| 369 "{ \"element\": \"pass3\"," "\"value\": \"word\" }," | |
| 370 "]}", | |
| 371 "http://fakedomain.com/", | |
| 372 "http://fakedomain.com/", | |
| 373 "account", | |
| 374 "fakeaccount", | |
| 375 "", | |
| 376 "", | |
| 377 "", | |
| 378 "", | |
| 379 }, | |
| 380 // One username element, three password elements. Two passwords are | |
| 381 // the same followed by a different one. Assuming that the duplicated | |
| 382 // password is the old one. | |
| 383 { | |
| 384 "http://fakedomain.com", | |
| 385 "{ \"action\": \"\"," | |
| 386 "\"usernameElement\": \"account\"," | |
| 387 "\"usernameValue\": \"fakeaccount\"," | |
| 388 "\"name\": \"signup\"," | |
| 389 "\"origin\": \"http://fakedomain.com/foo\"," | |
| 390 "\"passwords\": [" | |
| 391 "{ \"element\": \"pass1\"," "\"value\": \"word1\" }," | |
| 392 "{ \"element\": \"pass2\"," "\"value\": \"word1\" }," | |
| 393 "{ \"element\": \"pass3\"," "\"value\": \"word3\" }," | |
| 394 "]}", | |
| 395 "http://fakedomain.com/", | |
| 396 "http://fakedomain.com/", | |
| 397 "account", | |
| 398 "fakeaccount", | |
| 399 "pass3", | |
| 400 "word3", | |
| 401 "pass1", | |
| 402 "word1", | |
| 403 }, | |
| 404 // One username element, three password elements. A password is | |
| 405 // follwed by two duplicate ones. Assuming that the duplicated | |
| 406 // password is the new one. | |
| 407 { | |
| 408 "http://fakedomain.com", | |
| 409 "{ \"action\": \"\"," | |
| 410 "\"usernameElement\": \"account\"," | |
| 411 "\"usernameValue\": \"fakeaccount\"," | |
| 412 "\"name\": \"signup\"," | |
| 413 "\"origin\": \"http://fakedomain.com/foo\"," | |
| 414 "\"passwords\": [" | |
| 415 "{ \"element\": \"pass1\"," "\"value\": \"word1\" }," | |
| 416 "{ \"element\": \"pass2\"," "\"value\": \"word2\" }," | |
| 417 "{ \"element\": \"pass3\"," "\"value\": \"word2\" }," | |
| 418 "]}", | |
| 419 "http://fakedomain.com/", | |
| 420 "http://fakedomain.com/", | |
| 421 "account", | |
| 422 "fakeaccount", | |
| 423 "pass2", | |
| 424 "word2", | |
| 425 "pass1", | |
| 426 "word1", | |
| 427 }, | |
| 428 }; | |
| 429 // clang-format on | |
| 430 | |
| 431 for (const PasswordFormTestData& data : test_data) { | |
| 432 SCOPED_TRACE(testing::Message() | |
| 433 << "for page_location=" << data.page_location | |
| 434 << " and json_string=" << data.json_string); | |
| 435 std::unique_ptr<base::Value> json_data( | |
| 436 base::JSONReader::Read(data.json_string, true)); | |
| 437 const base::DictionaryValue* json_dict = nullptr; | |
| 438 ASSERT_TRUE(json_data->GetAsDictionary(&json_dict)); | |
| 439 PasswordForm form; | |
| 440 [passwordController_ getPasswordForm:&form | |
| 441 fromDictionary:json_dict | |
| 442 pageURL:GURL(data.page_location)]; | |
| 443 EXPECT_STREQ(data.expected_origin, form.origin.spec().c_str()); | |
| 444 EXPECT_STREQ(data.expected_action, form.action.spec().c_str()); | |
| 445 EXPECT_EQ(base::ASCIIToUTF16(data.expected_username_element), | |
| 446 form.username_element); | |
| 447 EXPECT_EQ(base::ASCIIToUTF16(data.expected_username_value), | |
| 448 form.username_value); | |
| 449 EXPECT_EQ(base::ASCIIToUTF16(data.expected_new_password_element), | |
| 450 form.new_password_element); | |
| 451 EXPECT_EQ(base::ASCIIToUTF16(data.expected_new_password_value), | |
| 452 form.new_password_value); | |
| 453 EXPECT_EQ(base::ASCIIToUTF16(data.expected_old_password_element), | |
| 454 form.password_element); | |
| 455 EXPECT_EQ(base::ASCIIToUTF16(data.expected_old_password_value), | |
| 456 form.password_value); | |
| 457 } | |
| 458 }; | |
| 459 | |
| 460 struct FindPasswordFormTestData { | |
| 461 NSString* html_string; | |
| 462 const bool expected_form_found; | |
| 463 const char* const expected_username_element; | |
| 464 const char* const expected_password_element; | |
| 465 }; | |
| 466 | |
| 467 // TODO(crbug.com/403705) This test is flaky. | |
| 468 TEST_F(PasswordControllerTest, FLAKY_FindPasswordFormsInView) { | |
| 469 // clang-format off | |
| 470 FindPasswordFormTestData test_data[] = { | |
| 471 // Normal form: a username and a password element. | |
| 472 { | |
| 473 @"<form>" | |
| 474 "<input type='text' name='user0'>" | |
| 475 "<input type='password' name='pass0'>" | |
| 476 "</form>", | |
| 477 true, "user0", "pass0" | |
| 478 }, | |
| 479 // User name is captured as an email address (HTML5). | |
| 480 { | |
| 481 @"<form>" | |
| 482 "<input type='email' name='email1'>" | |
| 483 "<input type='password' name='pass1'>" | |
| 484 "</form>", | |
| 485 true, "email1", "pass1" | |
| 486 }, | |
| 487 // No username element. | |
| 488 { | |
| 489 @"<form>" | |
| 490 "<input type='password' name='user2'>" | |
| 491 "<input type='password' name='pass2'>" | |
| 492 "</form>", | |
| 493 true, "", "user2" | |
| 494 }, | |
| 495 // No username element before password. | |
| 496 { | |
| 497 @"<form>" | |
| 498 "<input type='password' name='pass3'>" | |
| 499 "<input type='text' name='user3'>" | |
| 500 "</form>", | |
| 501 true, "", "pass3" | |
| 502 }, | |
| 503 // Disabled username element. | |
| 504 { | |
| 505 @"<form>" | |
| 506 "<input type='text' name='user4' disabled='disabled'>" | |
| 507 "<input type='password' name='pass4'>" | |
| 508 "</form>", | |
| 509 true, "", "pass4" | |
| 510 }, | |
| 511 // Username element has autocomplete='off'. | |
| 512 { | |
| 513 @"<form>" | |
| 514 "<input type='text' name='user5' AUTOCOMPLETE='off'>" | |
| 515 "<input type='password' name='pass5'>" | |
| 516 "</form>", | |
| 517 true, "user5", "pass5" | |
| 518 }, | |
| 519 // No password element. | |
| 520 { | |
| 521 @"<form>" | |
| 522 "<input type='text' name='user6'>" | |
| 523 "<input type='text' name='pass6'>" | |
| 524 "</form>", | |
| 525 false, nullptr, nullptr | |
| 526 }, | |
| 527 // Disabled password element. | |
| 528 { | |
| 529 @"<form>" | |
| 530 "<input type='text' name='user7'>" | |
| 531 "<input type='password' name='pass7' disabled='disabled'>" | |
| 532 "</form>", | |
| 533 false, nullptr, nullptr | |
| 534 }, | |
| 535 // Password element has autocomplete='off'. | |
| 536 { | |
| 537 @"<form>" | |
| 538 "<input type='text' name='user8'>" | |
| 539 "<input type='password' name='pass8' AUTOCOMPLETE='OFF'>" | |
| 540 "</form>", | |
| 541 true, "user8", "pass8" | |
| 542 }, | |
| 543 // Form element has autocomplete='off'. | |
| 544 { | |
| 545 @"<form autocomplete='off'>" | |
| 546 "<input type='text' name='user9'>" | |
| 547 "<input type='password' name='pass9'>" | |
| 548 "</form>", | |
| 549 true, "user9", "pass9" | |
| 550 }, | |
| 551 }; | |
| 552 // clang-format on | |
| 553 | |
| 554 for (const FindPasswordFormTestData& data : test_data) { | |
| 555 SCOPED_TRACE(testing::Message() << "for html_string=" << data.html_string); | |
| 556 LoadHtml(data.html_string); | |
| 557 __block std::vector<PasswordForm> forms; | |
| 558 __block BOOL block_was_called = NO; | |
| 559 [passwordController_ findPasswordFormsWithCompletionHandler:^( | |
| 560 const std::vector<PasswordForm>& result) { | |
| 561 block_was_called = YES; | |
| 562 forms = result; | |
| 563 }]; | |
| 564 base::test::ios::WaitUntilCondition(^bool() { | |
| 565 return block_was_called; | |
| 566 }); | |
| 567 if (data.expected_form_found) { | |
| 568 ASSERT_EQ(1U, forms.size()); | |
| 569 EXPECT_EQ(base::ASCIIToUTF16(data.expected_username_element), | |
| 570 forms[0].username_element); | |
| 571 EXPECT_EQ(base::ASCIIToUTF16(data.expected_password_element), | |
| 572 forms[0].password_element); | |
| 573 } else { | |
| 574 ASSERT_TRUE(forms.empty()); | |
| 575 } | |
| 576 } | |
| 577 } | |
| 578 | |
| 579 struct GetSubmittedPasswordFormTestData { | |
| 580 NSString* html_string; | |
| 581 NSString* java_script; | |
| 582 const int number_of_forms_to_submit; | |
| 583 const bool expected_form_found; | |
| 584 const char* expected_username_element; | |
| 585 }; | |
| 586 | |
| 587 // TODO(crbug.com/403705) This test is flaky. | |
| 588 TEST_F(PasswordControllerTest, FLAKY_GetSubmittedPasswordForm) { | |
| 589 // clang-format off | |
| 590 GetSubmittedPasswordFormTestData test_data[] = { | |
| 591 // Two forms with no explicit names. | |
| 592 { | |
| 593 @"<form action='javascript:;'>" | |
| 594 "<input type='text' name='user1'>" | |
| 595 "<input type='password' name='pass1'>" | |
| 596 "</form>" | |
| 597 "<form action='javascript:;'>" | |
| 598 "<input type='text' name='user2'>" | |
| 599 "<input type='password' name='pass2'>" | |
| 600 "<input type='submit' id='s2'>" | |
| 601 "</form>", | |
| 602 @"document.getElementById('s2').click()", | |
| 603 1, true, "user2" | |
| 604 }, | |
| 605 // Two forms with explicit names. | |
| 606 { | |
| 607 @"<form name='test2a' action='javascript:;'>" | |
| 608 "<input type='text' name='user1'>" | |
| 609 "<input type='password' name='pass1'>" | |
| 610 "<input type='submit' id='s1'>" | |
| 611 "</form>" | |
| 612 "<form name='test2b' action='javascript:;'>" | |
| 613 "<input type='text' name='user2'>" | |
| 614 "<input type='password' name='pass2'>" | |
| 615 "</form>", | |
| 616 @"document.getElementById('s1').click()", | |
| 617 0, true, "user1" | |
| 618 }, | |
| 619 // No password forms. | |
| 620 { | |
| 621 @"<form action='javascript:;'>" | |
| 622 "<input type='text' name='user1'>" | |
| 623 "<input type='text' name='pass1'>" | |
| 624 "<input type='submit' id='s1'>" | |
| 625 "</form>", | |
| 626 @"document.getElementById('s1').click()", | |
| 627 0, false, nullptr | |
| 628 }, | |
| 629 // Form with quotes in the form and field names. | |
| 630 { | |
| 631 @"<form name=\"foo'\" action='javascript:;'>" | |
| 632 "<input type='text' name=\"user1'\">" | |
| 633 "<input type='password' id='s1' name=\"pass1'\">" | |
| 634 "</form>", | |
| 635 @"document.getElementById('s1').click()", | |
| 636 0, true, "user1'" | |
| 637 }, | |
| 638 }; | |
| 639 // clang-format on | |
| 640 | |
| 641 for (const GetSubmittedPasswordFormTestData& data : test_data) { | |
| 642 SCOPED_TRACE(testing::Message() << "for html_string=" << data.html_string | |
| 643 << " and java_script=" << data.java_script | |
| 644 << " and number_of_forms_to_submit=" | |
| 645 << data.number_of_forms_to_submit); | |
| 646 LoadHtml(data.html_string); | |
| 647 EvaluateJavaScriptAsString(data.java_script); | |
| 648 __block BOOL block_was_called = NO; | |
| 649 id completion_handler = ^(BOOL found, const PasswordForm& form) { | |
| 650 block_was_called = YES; | |
| 651 ASSERT_EQ(data.expected_form_found, found); | |
| 652 if (data.expected_form_found) { | |
| 653 EXPECT_EQ(base::ASCIIToUTF16(data.expected_username_element), | |
| 654 form.username_element); | |
| 655 } | |
| 656 }; | |
| 657 [passwordController_ | |
| 658 extractSubmittedPasswordForm:FormName(data.number_of_forms_to_submit) | |
| 659 completionHandler:completion_handler]; | |
| 660 base::test::ios::WaitUntilCondition(^bool() { | |
| 661 return block_was_called; | |
| 662 }); | |
| 663 } | |
| 664 } | |
| 665 | |
| 666 // Populates |form_data| with test values. | |
| 667 void SetPasswordFormFillData(PasswordFormFillData& form_data, | |
| 668 const std::string& origin, | |
| 669 const std::string& action, | |
| 670 const char* username_field, | |
| 671 const char* username_value, | |
| 672 const char* password_field, | |
| 673 const char* password_value, | |
| 674 const char* additional_username, | |
| 675 const char* additional_password, | |
| 676 bool wait_for_username) { | |
| 677 form_data.origin = GURL(origin); | |
| 678 form_data.action = GURL(action); | |
| 679 autofill::FormFieldData username; | |
| 680 username.name = base::UTF8ToUTF16(username_field); | |
| 681 username.value = base::UTF8ToUTF16(username_value); | |
| 682 form_data.username_field = username; | |
| 683 autofill::FormFieldData password; | |
| 684 password.name = base::UTF8ToUTF16(password_field); | |
| 685 password.value = base::UTF8ToUTF16(password_value); | |
| 686 form_data.password_field = password; | |
| 687 if (additional_username) { | |
| 688 autofill::PasswordAndRealm additional_password_data; | |
| 689 additional_password_data.password = base::UTF8ToUTF16(additional_password); | |
| 690 additional_password_data.realm.clear(); | |
| 691 form_data.additional_logins.insert( | |
| 692 std::pair<base::string16, autofill::PasswordAndRealm>( | |
| 693 base::UTF8ToUTF16(additional_username), additional_password_data)); | |
| 694 } | |
| 695 form_data.wait_for_username = wait_for_username; | |
| 696 } | |
| 697 | |
| 698 // Test HTML page. It contains several password forms. Tests autofill | |
| 699 // them and verify that the right ones are autofilled. | |
| 700 static NSString* kHtmlWithMultiplePasswordForms = | |
| 701 @"<form>" | |
| 702 "<input id='un0' type='text' name='u0'>" | |
| 703 "<input id='pw0' type='password' name='p0'>" | |
| 704 "</form>" | |
| 705 "<form action='action?query=yes#reference'>" | |
| 706 "<input id='un1' type='text' name='u1'>" | |
| 707 "<input id='pw1' type='password' name='p1'>" | |
| 708 "</form>" | |
| 709 "<form action='http://some_other_action'>" | |
| 710 "<input id='un2' type='text' name='u2'>" | |
| 711 "<input id='pw2' type='password' name='p2'>" | |
| 712 "</form>" | |
| 713 "<form>" | |
| 714 "<input id='un3' type='text' name='u3'>" | |
| 715 "<input id='pw3' type='password' name='p3'>" | |
| 716 "<input id='pw3' type='password' name='p3'>" | |
| 717 "</form>" | |
| 718 "<form>" | |
| 719 "<input id='un4' type='text' name='u4'>" | |
| 720 "<input id='pw4' type='password' name='p4'>" | |
| 721 "</form>" | |
| 722 "<form>" | |
| 723 "<input id='un5' type='text' name='u4'>" | |
| 724 "<input id='pw5' type='password' name='p4'>" | |
| 725 "</form>" | |
| 726 "<form name=\"f6'\">" | |
| 727 "<input id=\"un6'\" type='text' name=\"u6'\">" | |
| 728 "<input id=\"pw6'\" type='password' name=\"p6'\">" | |
| 729 "</form>"; | |
| 730 | |
| 731 // A script that resets all text fields. | |
| 732 static NSString* kClearInputFieldsScript = | |
| 733 @"var inputs = document.getElementsByTagName('input');" | |
| 734 "for(var i = 0; i < inputs.length; i++){" | |
| 735 " inputs[i].value = '';" | |
| 736 "}"; | |
| 737 | |
| 738 // A script that we run after autofilling forms. It returns | |
| 739 // ids and values of all non-empty fields. | |
| 740 static NSString* kInputFieldValueVerificationScript = | |
| 741 @"var result='';" | |
| 742 "var inputs = document.getElementsByTagName('input');" | |
| 743 "for(var i = 0; i < inputs.length; i++){" | |
| 744 " var input = inputs[i];" | |
| 745 " if (input.value) {" | |
| 746 " result += input.id + '=' + input.value +';';" | |
| 747 " }" | |
| 748 "}; result"; | |
| 749 | |
| 750 struct FillPasswordFormTestData { | |
| 751 const std::string origin; | |
| 752 const std::string action; | |
| 753 const char* username_field; | |
| 754 const char* username_value; | |
| 755 const char* password_field; | |
| 756 const char* password_value; | |
| 757 const BOOL should_succeed; | |
| 758 NSString* expected_result; | |
| 759 }; | |
| 760 | |
| 761 TEST_F(PasswordControllerTest, FillPasswordForm) { | |
| 762 LoadHtml(kHtmlWithMultiplePasswordForms); | |
| 763 | |
| 764 EXPECT_NSEQ(@"true", | |
| 765 EvaluateJavaScriptAsString(@"__gCrWeb.hasPasswordField()")); | |
| 766 | |
| 767 const std::string base_url = BaseUrl(); | |
| 768 // clang-format off | |
| 769 FillPasswordFormTestData test_data[] = { | |
| 770 // Basic test: one-to-one match on the first password form. | |
| 771 { | |
| 772 base_url, | |
| 773 base_url, | |
| 774 "u0", | |
| 775 "test_user", | |
| 776 "p0", | |
| 777 "test_password", | |
| 778 YES, | |
| 779 @"un0=test_user;pw0=test_password;" | |
| 780 }, | |
| 781 // Multiple forms match: they should all be autofilled. | |
| 782 { | |
| 783 base_url, | |
| 784 base_url, | |
| 785 "u4", | |
| 786 "test_user", | |
| 787 "p4", | |
| 788 "test_password", | |
| 789 YES, | |
| 790 @"un4=test_user;pw4=test_password;un5=test_user;pw5=test_password;" | |
| 791 }, | |
| 792 // The form matches despite a different action: the only difference | |
| 793 // is a query and reference. | |
| 794 { | |
| 795 base_url, | |
| 796 base_url, | |
| 797 "u1", | |
| 798 "test_user", | |
| 799 "p1", | |
| 800 "test_password", | |
| 801 YES, | |
| 802 @"un1=test_user;pw1=test_password;" | |
| 803 }, | |
| 804 // No match because of a different origin. | |
| 805 { | |
| 806 "http://someotherfakedomain.com", | |
| 807 base_url, | |
| 808 "u0", | |
| 809 "test_user", | |
| 810 "p0", | |
| 811 "test_password", | |
| 812 NO, | |
| 813 @"" | |
| 814 }, | |
| 815 // No match because of a different action. | |
| 816 { | |
| 817 base_url, | |
| 818 "http://someotherfakedomain.com", | |
| 819 "u0", | |
| 820 "test_user", | |
| 821 "p0", | |
| 822 "test_password", | |
| 823 NO, | |
| 824 @"" | |
| 825 }, | |
| 826 // No match because some inputs are not in the form. | |
| 827 { | |
| 828 base_url, | |
| 829 base_url, | |
| 830 "u0", | |
| 831 "test_user", | |
| 832 "p1", | |
| 833 "test_password", | |
| 834 NO, | |
| 835 @"" | |
| 836 }, | |
| 837 // No match because there are duplicate inputs in the form. | |
| 838 { | |
| 839 base_url, | |
| 840 base_url, | |
| 841 "u3", | |
| 842 "test_user", | |
| 843 "p3", | |
| 844 "test_password", | |
| 845 NO, | |
| 846 @"" | |
| 847 }, | |
| 848 // Basic test, but with quotes in the names and IDs. | |
| 849 { | |
| 850 base_url, | |
| 851 base_url, | |
| 852 "u6'", | |
| 853 "test_user", | |
| 854 "p6'", | |
| 855 "test_password", | |
| 856 YES, | |
| 857 @"un6'=test_user;pw6'=test_password;" | |
| 858 }, | |
| 859 }; | |
| 860 // clang-format on | |
| 861 | |
| 862 for (const FillPasswordFormTestData& data : test_data) { | |
| 863 EvaluateJavaScriptAsString(kClearInputFieldsScript); | |
| 864 | |
| 865 PasswordFormFillData form_data; | |
| 866 SetPasswordFormFillData(form_data, data.origin, data.action, | |
| 867 data.username_field, data.username_value, | |
| 868 data.password_field, data.password_value, nullptr, | |
| 869 nullptr, false); | |
| 870 | |
| 871 __block BOOL block_was_called = NO; | |
| 872 [passwordController_ fillPasswordForm:form_data | |
| 873 completionHandler:^(BOOL success) { | |
| 874 block_was_called = YES; | |
| 875 EXPECT_EQ(data.should_succeed, success); | |
| 876 }]; | |
| 877 base::test::ios::WaitUntilCondition(^bool() { | |
| 878 return block_was_called; | |
| 879 }); | |
| 880 | |
| 881 NSString* result = | |
| 882 EvaluateJavaScriptAsString(kInputFieldValueVerificationScript); | |
| 883 EXPECT_NSEQ(data.expected_result, result); | |
| 884 } | |
| 885 } | |
| 886 | |
| 887 // Tests that a form is found and the found form is filled in with the given | |
| 888 // username and password. | |
| 889 TEST_F(PasswordControllerTest, FindAndFillOnePasswordForm) { | |
| 890 LoadHtml(@"<form><input id='un' type='text' name='u'>" | |
| 891 "<input id='pw' type='password' name='p'></form>"); | |
| 892 __block int call_counter = 0; | |
| 893 __block int success_counter = 0; | |
| 894 [passwordController_ findAndFillPasswordForms:@"john.doe@gmail.com" | |
| 895 password:@"super!secret" | |
| 896 completionHandler:^(BOOL complete) { | |
| 897 ++call_counter; | |
| 898 if (complete) | |
| 899 ++success_counter; | |
| 900 }]; | |
| 901 base::test::ios::WaitUntilCondition(^{ | |
| 902 return call_counter == 1; | |
| 903 }); | |
| 904 EXPECT_EQ(1, success_counter); | |
| 905 NSString* result = | |
| 906 EvaluateJavaScriptAsString(kInputFieldValueVerificationScript); | |
| 907 EXPECT_NSEQ(@"un=john.doe@gmail.com;pw=super!secret;", result); | |
| 908 } | |
| 909 | |
| 910 // Tests that multiple forms on the same page are found and filled. | |
| 911 // This test includes an mock injected failure on form filling to verify | |
| 912 // that completion handler is called with the proper values. | |
| 913 TEST_F(PasswordControllerTest, FindAndFillMultiplePasswordForms) { | |
| 914 // Fails the first call to fill password form. | |
| 915 SetFillPasswordFormFailureCount(1); | |
| 916 LoadHtml(@"<form><input id='u1' type='text' name='un1'>" | |
| 917 "<input id='p1' type='password' name='pw1'></form>" | |
| 918 "<form><input id='u2' type='text' name='un2'>" | |
| 919 "<input id='p2' type='password' name='pw2'></form>" | |
| 920 "<form><input id='u3' type='text' name='un3'>" | |
| 921 "<input id='p3' type='password' name='pw3'></form>"); | |
| 922 __block int call_counter = 0; | |
| 923 __block int success_counter = 0; | |
| 924 [passwordController_ findAndFillPasswordForms:@"john.doe@gmail.com" | |
| 925 password:@"super!secret" | |
| 926 completionHandler:^(BOOL complete) { | |
| 927 ++call_counter; | |
| 928 if (complete) | |
| 929 ++success_counter; | |
| 930 LOG(INFO) << "HANDLER call " << call_counter | |
| 931 << " success " << success_counter; | |
| 932 }]; | |
| 933 // There should be 3 password forms and only 2 successfully filled forms. | |
| 934 base::test::ios::WaitUntilCondition(^{ | |
| 935 return call_counter == 3; | |
| 936 }); | |
| 937 EXPECT_EQ(2, success_counter); | |
| 938 NSString* result = | |
| 939 EvaluateJavaScriptAsString(kInputFieldValueVerificationScript); | |
| 940 EXPECT_NSEQ(@"u2=john.doe@gmail.com;p2=super!secret;" | |
| 941 "u3=john.doe@gmail.com;p3=super!secret;", | |
| 942 result); | |
| 943 } | |
| 944 | |
| 945 BOOL PasswordControllerTest::BasicFormFill(NSString* html) { | |
| 946 LoadHtml(html); | |
| 947 EXPECT_NSEQ(@"true", | |
| 948 EvaluateJavaScriptAsString(@"__gCrWeb.hasPasswordField()")); | |
| 949 const std::string base_url = BaseUrl(); | |
| 950 PasswordFormFillData form_data; | |
| 951 SetPasswordFormFillData(form_data, base_url, base_url, "u0", "test_user", | |
| 952 "p0", "test_password", nullptr, nullptr, false); | |
| 953 __block BOOL block_was_called = NO; | |
| 954 __block BOOL return_value = NO; | |
| 955 [passwordController_ fillPasswordForm:form_data | |
| 956 completionHandler:^(BOOL success) { | |
| 957 block_was_called = YES; | |
| 958 return_value = success; | |
| 959 }]; | |
| 960 base::test::ios::WaitUntilCondition(^bool() { | |
| 961 return block_was_called; | |
| 962 }); | |
| 963 return return_value; | |
| 964 } | |
| 965 | |
| 966 // Check that |fillPasswordForm| is not filled if 'readonly' attribute is set | |
| 967 // on either username or password fields. | |
| 968 // TODO(crbug.com/503050): Test is flaky. | |
| 969 TEST_F(PasswordControllerTest, FLAKY_DontFillReadOnly) { | |
| 970 // Control check that the fill operation will succceed with well-formed form. | |
| 971 EXPECT_TRUE(BasicFormFill(@"<form>" | |
| 972 "<input id='un0' type='text' name='u0'>" | |
| 973 "<input id='pw0' type='password' name='p0'>" | |
| 974 "</form>")); | |
| 975 // Form fill should fail with 'readonly' attribute on username. | |
| 976 EXPECT_FALSE(BasicFormFill( | |
| 977 @"<form>" | |
| 978 "<input id='un0' type='text' name='u0' readonly='readonly'>" | |
| 979 "<input id='pw0' type='password' name='p0'>" | |
| 980 "</form>")); | |
| 981 // Form fill should fail with 'readonly' attribute on password. | |
| 982 EXPECT_FALSE(BasicFormFill( | |
| 983 @"<form>" | |
| 984 "<input id='un0' type='text' name='u0'>" | |
| 985 "<input id='pw0' type='password' name='p0' readonly='readonly'>" | |
| 986 "</form>")); | |
| 987 } | |
| 988 | |
| 989 // An HTML page containing one password form. The username input field | |
| 990 // also has custom event handlers. We need to verify that those event | |
| 991 // handlers are still triggered even though we override them with our own. | |
| 992 static NSString* kHtmlWithPasswordForm = | |
| 993 @"<form>" | |
| 994 "<input id='un' type='text' name=\"u'\"" | |
| 995 " onkeyup='window.onKeyUpCalled_=true'" | |
| 996 " onchange='window.onChangeCalled_=true'>" | |
| 997 "<input id='pw' type='password' name=\"p'\">" | |
| 998 "</form>"; | |
| 999 | |
| 1000 // A script that resets indicators used to verify that custom event | |
| 1001 // handlers are triggered. It also finds and the username and | |
| 1002 // password fields and caches them for future verification. | |
| 1003 static NSString* kUsernameAndPasswordTestPreparationScript = | |
| 1004 @"onKeyUpCalled_ = false;" | |
| 1005 "onChangeCalled_ = false;" | |
| 1006 "username_ = document.getElementById('un');" | |
| 1007 "username_.__gCrWebAutofilled = 'false';" | |
| 1008 "password_ = document.getElementById('pw');" | |
| 1009 "password_.__gCrWebAutofilled = 'false';"; | |
| 1010 | |
| 1011 // A script that we run after autofilling forms. It returns | |
| 1012 // all values for verification as a single concatenated string. | |
| 1013 static NSString* kUsernamePasswordVerificationScript = | |
| 1014 @"var value = username_.value;" | |
| 1015 "var from = username_.selectionStart;" | |
| 1016 "var to = username_.selectionEnd;" | |
| 1017 "value.substr(0, from) + '[' + value.substr(from, to) + ']'" | |
| 1018 " + value.substr(to, value.length) + '=' + password_.value" | |
| 1019 " + ', onkeyup=' + onKeyUpCalled_" | |
| 1020 " + ', onchange=' + onChangeCalled_;"; | |
| 1021 | |
| 1022 struct SuggestionTestData { | |
| 1023 std::string description; | |
| 1024 NSArray* eval_scripts; | |
| 1025 NSArray* expected_suggestions; | |
| 1026 NSString* expected_result; | |
| 1027 }; | |
| 1028 | |
| 1029 // Tests that form activity correctly sends suggestions to the suggestion | |
| 1030 // controller. | |
| 1031 TEST_F(PasswordControllerTest, SuggestionUpdateTests) { | |
| 1032 LoadHtml(kHtmlWithPasswordForm); | |
| 1033 const std::string base_url = BaseUrl(); | |
| 1034 EvaluateJavaScriptAsString(kUsernameAndPasswordTestPreparationScript); | |
| 1035 | |
| 1036 // Initialize |form_data| with test data and an indicator that autofill | |
| 1037 // should not be performed while the user is entering the username so that | |
| 1038 // we can test with an initially-empty username field. Testing with a | |
| 1039 // username field that contains input is performed by a specific test below. | |
| 1040 PasswordFormFillData form_data; | |
| 1041 SetPasswordFormFillData(form_data, base_url, base_url, "u'", "user0", "p'", | |
| 1042 "password0", "abc", "def", true); | |
| 1043 form_data.name = base::ASCIIToUTF16(FormName(0)); | |
| 1044 | |
| 1045 __block BOOL block_was_called = NO; | |
| 1046 [passwordController_ fillPasswordForm:form_data | |
| 1047 completionHandler:^(BOOL success) { | |
| 1048 block_was_called = YES; | |
| 1049 // Verify that the fill reports failed. | |
| 1050 EXPECT_FALSE(success); | |
| 1051 }]; | |
| 1052 base::test::ios::WaitUntilCondition(^bool() { | |
| 1053 return block_was_called; | |
| 1054 }); | |
| 1055 | |
| 1056 // Verify that the form has not been autofilled. | |
| 1057 EXPECT_NSEQ(@"[]=, onkeyup=false, onchange=false", | |
| 1058 EvaluateJavaScriptAsString(kUsernamePasswordVerificationScript)); | |
| 1059 | |
| 1060 // clang-format off | |
| 1061 SuggestionTestData test_data[] = { | |
| 1062 { | |
| 1063 "Should show all suggestions when focusing empty username field", | |
| 1064 @[(@"var evt = document.createEvent('Events');" | |
| 1065 "evt.initEvent('focus', true, true, window, 1);" | |
| 1066 "username_.dispatchEvent(evt);"), | |
| 1067 @""], | |
| 1068 @[@"abc", @"user0"], | |
| 1069 @"[]=, onkeyup=false, onchange=false" | |
| 1070 }, | |
| 1071 { | |
| 1072 "Should not show suggestions when focusing password field", | |
| 1073 @[(@"var evt = document.createEvent('Events');" | |
| 1074 "evt.initEvent('focus', true, true, window, 1);" | |
| 1075 "password_.dispatchEvent(evt);"), | |
| 1076 @""], | |
| 1077 @[], | |
| 1078 @"[]=, onkeyup=false, onchange=false" | |
| 1079 }, | |
| 1080 { | |
| 1081 "Should filter suggestions when focusing username field with input", | |
| 1082 @[(@"username_.value='ab';" | |
| 1083 "var evt = document.createEvent('Events');" | |
| 1084 "evt.initEvent('focus', true, true, window, 1);" | |
| 1085 "username_.dispatchEvent(evt);"), | |
| 1086 @""], | |
| 1087 @[@"abc"], | |
| 1088 @"ab[]=, onkeyup=false, onchange=false" | |
| 1089 }, | |
| 1090 { | |
| 1091 "Should filter suggestions while typing", | |
| 1092 @[(@"var evt = document.createEvent('Events');" | |
| 1093 "evt.initEvent('focus', true, true, window, 1);" | |
| 1094 "username_.dispatchEvent(evt);"), | |
| 1095 (@"username_.value='ab';" | |
| 1096 "evt = document.createEvent('Events');" | |
| 1097 "evt.initEvent('keyup', true, true, window, 1);" | |
| 1098 "evt.keyCode = 98;" | |
| 1099 "username_.dispatchEvent(evt);"), | |
| 1100 @""], | |
| 1101 @[@"abc"], | |
| 1102 @"ab[]=, onkeyup=true, onchange=false" | |
| 1103 }, | |
| 1104 { | |
| 1105 "Should unfilter suggestions after backspacing", | |
| 1106 @[(@"var evt = document.createEvent('Events');" | |
| 1107 "evt.initEvent('focus', true, true, window, 1);" | |
| 1108 "username_.dispatchEvent(evt);"), | |
| 1109 (@"username_.value='ab';" | |
| 1110 "evt = document.createEvent('Events');" | |
| 1111 "evt.initEvent('keyup', true, true, window, 1);" | |
| 1112 "evt.keyCode = 98;" | |
| 1113 "username_.dispatchEvent(evt);"), | |
| 1114 (@"username_.value='';" | |
| 1115 "evt = document.createEvent('Events');" | |
| 1116 "evt.initEvent('keyup', true, true, window, 1);" | |
| 1117 "evt.keyCode = 8;" | |
| 1118 "username_.dispatchEvent(evt);"), | |
| 1119 @""], | |
| 1120 @[@"abc", @"user0"], | |
| 1121 @"[]=, onkeyup=true, onchange=false" | |
| 1122 }, | |
| 1123 }; | |
| 1124 // clang-format on | |
| 1125 | |
| 1126 for (const SuggestionTestData& data : test_data) { | |
| 1127 SCOPED_TRACE(testing::Message() | |
| 1128 << "for description=" << data.description | |
| 1129 << " and eval_scripts=" << data.eval_scripts); | |
| 1130 // Prepare the test. | |
| 1131 EvaluateJavaScriptAsString(kUsernameAndPasswordTestPreparationScript); | |
| 1132 | |
| 1133 for (NSString* script in data.eval_scripts) { | |
| 1134 // Trigger events. | |
| 1135 EvaluateJavaScriptAsString(script); | |
| 1136 | |
| 1137 // Pump the run loop so that the host can respond. | |
| 1138 WaitForBackgroundTasks(); | |
| 1139 } | |
| 1140 | |
| 1141 EXPECT_NSEQ(data.expected_suggestions, GetSortedSuggestionValues()); | |
| 1142 EXPECT_NSEQ(data.expected_result, EvaluateJavaScriptAsString( | |
| 1143 kUsernamePasswordVerificationScript)); | |
| 1144 // Clear all suggestions. | |
| 1145 [suggestionController_ setSuggestions:nil]; | |
| 1146 } | |
| 1147 } | |
| 1148 | |
| 1149 // Tests that selecting a suggestion will fill the corresponding form and field. | |
| 1150 TEST_F(PasswordControllerTest, SelectingSuggestionShouldFillPasswordForm) { | |
| 1151 LoadHtml(kHtmlWithPasswordForm); | |
| 1152 const std::string base_url = BaseUrl(); | |
| 1153 EvaluateJavaScriptAsString(kUsernameAndPasswordTestPreparationScript); | |
| 1154 | |
| 1155 // Initialize |form_data| with test data and an indicator that autofill | |
| 1156 // should not be performed while the user is entering the username so that | |
| 1157 // we can test with an initially-empty username field. | |
| 1158 PasswordFormFillData form_data; | |
| 1159 SetPasswordFormFillData(form_data, base_url, base_url, "u'", "user0", "p'", | |
| 1160 "password0", "abc", "def", true); | |
| 1161 form_data.name = base::ASCIIToUTF16(FormName(0)); | |
| 1162 | |
| 1163 __block BOOL block_was_called = NO; | |
| 1164 [passwordController_ fillPasswordForm:form_data | |
| 1165 completionHandler:^(BOOL success) { | |
| 1166 block_was_called = YES; | |
| 1167 // Verify that the fill reports failed. | |
| 1168 EXPECT_FALSE(success); | |
| 1169 }]; | |
| 1170 base::test::ios::WaitUntilCondition(^bool() { | |
| 1171 return block_was_called; | |
| 1172 }); | |
| 1173 | |
| 1174 // Verify that the form has not been autofilled. | |
| 1175 EXPECT_NSEQ(@"[]=, onkeyup=false, onchange=false", | |
| 1176 EvaluateJavaScriptAsString(kUsernamePasswordVerificationScript)); | |
| 1177 | |
| 1178 // Tell PasswordController that a suggestion was selected. It should fill | |
| 1179 // out the password form with the corresponding credentials. | |
| 1180 FormSuggestion* suggestion = [FormSuggestion suggestionWithValue:@"abc" | |
| 1181 displayDescription:nil | |
| 1182 icon:nil | |
| 1183 identifier:0]; | |
| 1184 | |
| 1185 block_was_called = NO; | |
| 1186 SuggestionHandledCompletion completion = ^{ | |
| 1187 block_was_called = YES; | |
| 1188 EXPECT_NSEQ(@"abc[]=def, onkeyup=false, onchange=false", | |
| 1189 ExecuteJavaScript(kUsernamePasswordVerificationScript)); | |
| 1190 }; | |
| 1191 [passwordController_ didSelectSuggestion:suggestion | |
| 1192 forField:@"u" | |
| 1193 form:base::SysUTF8ToNSString(FormName(0)) | |
| 1194 completionHandler:completion]; | |
| 1195 base::test::ios::WaitUntilCondition(^bool() { | |
| 1196 return block_was_called; | |
| 1197 }); | |
| 1198 } | |
| 1199 | |
| 1200 // Tests with invalid inputs. | |
| 1201 TEST_F(PasswordControllerTest, CheckIncorrectData) { | |
| 1202 // clang-format off | |
| 1203 std::string invalid_data[] = { | |
| 1204 "{}", | |
| 1205 | |
| 1206 "{ \"usernameValue\": \"fakeaccount\"," | |
| 1207 "\"passwords\": [" | |
| 1208 "{ \"element\": \"secret\"," "\"value\": \"fakesecret\" }," | |
| 1209 "]}", | |
| 1210 | |
| 1211 "{ \"usernameElement\": \"account\"," | |
| 1212 "\"passwords\": [" | |
| 1213 "{ \"element\": \"secret\"," "\"value\": \"fakesecret\" }," | |
| 1214 "]}", | |
| 1215 | |
| 1216 "{ \"usernameElement\": \"account\"," | |
| 1217 "\"usernameValue\": \"fakeaccount\"," | |
| 1218 "}", | |
| 1219 | |
| 1220 "{ \"usernameElement\": \"account\"," | |
| 1221 "\"usernameValue\": \"fakeaccount\"," | |
| 1222 "\"passwords\": {}," | |
| 1223 "}", | |
| 1224 | |
| 1225 "{ \"usernameElement\": \"account\"," | |
| 1226 "\"usernameValue\": \"fakeaccount\"," | |
| 1227 "\"passwords\": [" | |
| 1228 "]}", | |
| 1229 | |
| 1230 "{ \"usernameElement\": \"account\"," | |
| 1231 "\"usernameValue\": \"fakeaccount\"," | |
| 1232 "\"passwords\": [" | |
| 1233 "{ \"value\": \"fakesecret\" }," | |
| 1234 "]}", | |
| 1235 | |
| 1236 "{ \"usernameElement\": \"account\"," | |
| 1237 "\"usernameValue\": \"fakeaccount\"," | |
| 1238 "\"passwords\": [" | |
| 1239 "{ \"element\": \"secret\" }," | |
| 1240 "]}", | |
| 1241 }; | |
| 1242 // clang-format on | |
| 1243 | |
| 1244 for (const std::string& data : invalid_data) { | |
| 1245 SCOPED_TRACE(testing::Message() << "for data=" << data); | |
| 1246 std::unique_ptr<base::Value> json_data(base::JSONReader::Read(data, true)); | |
| 1247 const base::DictionaryValue* json_dict = nullptr; | |
| 1248 ASSERT_TRUE(json_data->GetAsDictionary(&json_dict)); | |
| 1249 PasswordForm form; | |
| 1250 BOOL res = | |
| 1251 [passwordController_ getPasswordForm:&form | |
| 1252 fromDictionary:json_dict | |
| 1253 pageURL:GURL("https://www.foo.com/")]; | |
| 1254 EXPECT_FALSE(res); | |
| 1255 } | |
| 1256 } | |
| 1257 | |
| 1258 // The test case below does not need the heavy fixture from above, but it | |
| 1259 // needs to use MockWebState. | |
| 1260 TEST(PasswordControllerTestSimple, SaveOnNonHTMLLandingPage) { | |
| 63 TestChromeBrowserState::Builder builder; | 1261 TestChromeBrowserState::Builder builder; |
| 64 auto pref_service = | |
| 65 base::WrapUnique(new syncable_prefs::TestingPrefServiceSyncable); | |
| 66 pref_service->registry()->RegisterBooleanPref( | |
| 67 password_manager::prefs::kPasswordManagerSavingEnabled, true); | |
| 68 builder.SetPrefService(std::move(pref_service)); | |
| 69 std::unique_ptr<TestChromeBrowserState> browser_state(builder.Build()); | 1262 std::unique_ptr<TestChromeBrowserState> browser_state(builder.Build()); |
| 70 MockWebState web_state; | 1263 MockWebState web_state; |
| 71 ON_CALL(web_state, GetBrowserState()) | 1264 ON_CALL(web_state, GetBrowserState()) |
| 72 .WillByDefault(testing::Return(browser_state.get())); | 1265 .WillByDefault(testing::Return(browser_state.get())); |
| 73 auto client = base::WrapUnique(new MockPasswordManagerClient); | 1266 |
| 74 MockPasswordManagerClient* weak_client = client.get(); | 1267 MockPasswordManagerClient* weak_client = nullptr; |
| 75 base::scoped_nsobject<PasswordController> passwordController( | 1268 base::scoped_nsobject<PasswordController> passwordController = |
| 76 [[PasswordController alloc] initWithWebState:&web_state | 1269 CreatePasswordController(&web_state, nullptr, &weak_client); |
| 77 passwordsUiDelegate:nil | 1270 static_cast<TestingPrefServiceSimple*>(weak_client->GetPrefs()) |
| 78 client:std::move(client)]); | 1271 ->registry() |
| 1272 ->RegisterBooleanPref( | |
| 1273 password_manager::prefs::kPasswordManagerSavingEnabled, true); | |
| 79 | 1274 |
| 80 // Use a mock LogManager to detect that OnPasswordFormsRendered has been | 1275 // Use a mock LogManager to detect that OnPasswordFormsRendered has been |
| 81 // called. TODO(crbug.com/598672): this is a hack, we should modularize the | 1276 // called. TODO(crbug.com/598672): this is a hack, we should modularize the |
| 82 // code better to allow proper unit-testing. | 1277 // code better to allow proper unit-testing. |
| 83 MockLogManager log_manager; | 1278 MockLogManager log_manager; |
| 84 EXPECT_CALL(log_manager, IsLoggingActive()).WillRepeatedly(Return(true)); | 1279 EXPECT_CALL(log_manager, IsLoggingActive()).WillRepeatedly(Return(true)); |
| 85 EXPECT_CALL(log_manager, | 1280 EXPECT_CALL(log_manager, |
| 86 LogSavePasswordProgress( | 1281 LogSavePasswordProgress( |
| 87 "Message: \"PasswordManager::OnPasswordFormsRendered\"\n")); | 1282 "Message: \"PasswordManager::OnPasswordFormsRendered\"\n")); |
| 88 EXPECT_CALL(log_manager, | 1283 EXPECT_CALL(log_manager, |
| 89 LogSavePasswordProgress(testing::Ne( | 1284 LogSavePasswordProgress(testing::Ne( |
| 90 "Message: \"PasswordManager::OnPasswordFormsRendered\"\n"))) | 1285 "Message: \"PasswordManager::OnPasswordFormsRendered\"\n"))) |
| 91 .Times(testing::AnyNumber()); | 1286 .Times(testing::AnyNumber()); |
| 92 EXPECT_CALL(*weak_client, GetLogManager()) | 1287 EXPECT_CALL(*weak_client, GetLogManager()) |
| 93 .WillRepeatedly(Return(&log_manager)); | 1288 .WillRepeatedly(Return(&log_manager)); |
| 94 | 1289 |
| 95 web_state.SetContentIsHTML(false); | 1290 web_state.SetContentIsHTML(false); |
| 96 web_state.SetCurrentURL(GURL("https://example.com")); | 1291 web_state.SetCurrentURL(GURL("https://example.com")); |
| 97 [passwordController webStateDidLoadPage:&web_state]; | 1292 [passwordController webStateDidLoadPage:&web_state]; |
| 98 } | 1293 } |
| OLD | NEW |