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 |
10 #include "base/mac/scoped_nsobject.h" | 12 #include "base/json/json_reader.h" |
| 13 #include "base/mac/bind_objc_block.h" |
| 14 #import "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 // Check that given a serialization of a PasswordForm, the controller is able |
| 265 // to create the corresponding PasswordForm object. |
| 266 TEST_F(PasswordControllerTest, PopulatePasswordFormWithDictionary) { |
| 267 // clang-format off |
| 268 PasswordFormTestData test_data[] = { |
| 269 // One username element, one password element. URLs contain extra |
| 270 // parts: username/password, query, reference, which are all expected |
| 271 // to be stripped off. The password is recognized as an old password. |
| 272 { |
| 273 "http://john:doe@fakedomain.com/foo/bar?baz=quz#foobar", |
| 274 "{ \"action\": \"some/action?to=be&or=not#tobe\"," |
| 275 "\"usernameElement\": \"account\"," |
| 276 "\"usernameValue\": \"fakeaccount\"," |
| 277 "\"name\": \"signup\"," |
| 278 "\"origin\": \"http://john:doe@fakedomain.com/foo/bar\"," |
| 279 "\"passwords\": [" |
| 280 "{ \"element\": \"secret\"," "\"value\": \"fakesecret\" }," |
| 281 "]}", |
| 282 "http://fakedomain.com/foo/bar", |
| 283 "http://fakedomain.com/foo/some/action", |
| 284 "account", |
| 285 "fakeaccount", |
| 286 "", |
| 287 "", |
| 288 "secret", |
| 289 "fakesecret", |
| 290 }, |
| 291 // One username element, one password element. Population should fail |
| 292 // due to an origin mismatch. |
| 293 { |
| 294 "http://john:doe@fakedomain.com/foo/bar?baz=quz#foobar", |
| 295 "{ \"action\": \"some/action?to=be&or=not#tobe\"," |
| 296 "\"usernameElement\": \"account\"," |
| 297 "\"usernameValue\": \"fakeaccount\"," |
| 298 "\"name\": \"signup\"," |
| 299 "\"origin\": \"http://john:doe@realdomainipromise.com/foo/bar\"," |
| 300 "\"passwords\": [" |
| 301 "{ \"element\": \"secret\"," "\"value\": \"fakesecret\" }," |
| 302 "]}", |
| 303 "", |
| 304 "", |
| 305 "", |
| 306 "", |
| 307 "", |
| 308 "", |
| 309 "", |
| 310 "", |
| 311 }, |
| 312 // One username element, two password elements. Since both password |
| 313 // values are the same, we are assuming that the webpage asked the user |
| 314 // to enter the password twice for confirmation. |
| 315 { |
| 316 "http://fakedomain.com/foo", |
| 317 "{ \"action\": \"http://anotherdomain.com/some_action\"," |
| 318 "\"usernameElement\": \"account\"," |
| 319 "\"usernameValue\": \"fakeaccount\"," |
| 320 "\"name\": \"signup\"," |
| 321 "\"origin\": \"http://fakedomain.com/foo\"," |
| 322 "\"passwords\": [" |
| 323 "{ \"element\": \"secret\"," "\"value\": \"fakesecret\" }," |
| 324 "{ \"element\": \"confirm\"," "\"value\": \"fakesecret\" }," |
| 325 "]}", |
| 326 "http://fakedomain.com/foo", |
| 327 "http://anotherdomain.com/some_action", |
| 328 "account", |
| 329 "fakeaccount", |
| 330 "secret", |
| 331 "fakesecret", |
| 332 "", |
| 333 "", |
| 334 }, |
| 335 // One username element, two password elements. The password |
| 336 // values are different, so we are assuming that the webpage asked the user |
| 337 // to enter the old password and new password. |
| 338 { |
| 339 "http://fakedomain.com/foo", |
| 340 "{ \"action\": \"\"," |
| 341 "\"usernameElement\": \"account\"," |
| 342 "\"usernameValue\": \"fakeaccount\"," |
| 343 "\"name\": \"signup\"," |
| 344 "\"origin\": \"http://fakedomain.com/foo\"," |
| 345 "\"passwords\": [" |
| 346 "{ \"element\": \"old\"," "\"value\": \"oldsecret\" }," |
| 347 "{ \"element\": \"new\"," "\"value\": \"newsecret\" }," |
| 348 "]}", |
| 349 "http://fakedomain.com/foo", |
| 350 "http://fakedomain.com/foo", |
| 351 "account", |
| 352 "fakeaccount", |
| 353 "new", |
| 354 "newsecret", |
| 355 "old", |
| 356 "oldsecret", |
| 357 }, |
| 358 // One username element, three password elements. All passwords |
| 359 // are the same. Password population should fail because this configuration |
| 360 // does not make sense. |
| 361 { |
| 362 "http://fakedomain.com", |
| 363 "{ \"action\": \"\"," |
| 364 "\"usernameElement\": \"account\"," |
| 365 "\"usernameValue\": \"fakeaccount\"," |
| 366 "\"name\": \"signup\"," |
| 367 "\"origin\": \"http://fakedomain.com/foo\"," |
| 368 "\"passwords\": [" |
| 369 "{ \"element\": \"pass1\"," "\"value\": \"word\" }," |
| 370 "{ \"element\": \"pass2\"," "\"value\": \"word\" }," |
| 371 "{ \"element\": \"pass3\"," "\"value\": \"word\" }," |
| 372 "]}", |
| 373 "http://fakedomain.com/", |
| 374 "http://fakedomain.com/", |
| 375 "account", |
| 376 "fakeaccount", |
| 377 "", |
| 378 "", |
| 379 "", |
| 380 "", |
| 381 }, |
| 382 // One username element, three password elements. Two passwords are |
| 383 // the same followed by a different one. Assuming that the duplicated |
| 384 // password is the old one. |
| 385 { |
| 386 "http://fakedomain.com", |
| 387 "{ \"action\": \"\"," |
| 388 "\"usernameElement\": \"account\"," |
| 389 "\"usernameValue\": \"fakeaccount\"," |
| 390 "\"name\": \"signup\"," |
| 391 "\"origin\": \"http://fakedomain.com/foo\"," |
| 392 "\"passwords\": [" |
| 393 "{ \"element\": \"pass1\"," "\"value\": \"word1\" }," |
| 394 "{ \"element\": \"pass2\"," "\"value\": \"word1\" }," |
| 395 "{ \"element\": \"pass3\"," "\"value\": \"word3\" }," |
| 396 "]}", |
| 397 "http://fakedomain.com/", |
| 398 "http://fakedomain.com/", |
| 399 "account", |
| 400 "fakeaccount", |
| 401 "pass3", |
| 402 "word3", |
| 403 "pass1", |
| 404 "word1", |
| 405 }, |
| 406 // One username element, three password elements. A password is |
| 407 // follwed by two duplicate ones. Assuming that the duplicated |
| 408 // password is the new one. |
| 409 { |
| 410 "http://fakedomain.com", |
| 411 "{ \"action\": \"\"," |
| 412 "\"usernameElement\": \"account\"," |
| 413 "\"usernameValue\": \"fakeaccount\"," |
| 414 "\"name\": \"signup\"," |
| 415 "\"origin\": \"http://fakedomain.com/foo\"," |
| 416 "\"passwords\": [" |
| 417 "{ \"element\": \"pass1\"," "\"value\": \"word1\" }," |
| 418 "{ \"element\": \"pass2\"," "\"value\": \"word2\" }," |
| 419 "{ \"element\": \"pass3\"," "\"value\": \"word2\" }," |
| 420 "]}", |
| 421 "http://fakedomain.com/", |
| 422 "http://fakedomain.com/", |
| 423 "account", |
| 424 "fakeaccount", |
| 425 "pass2", |
| 426 "word2", |
| 427 "pass1", |
| 428 "word1", |
| 429 }, |
| 430 }; |
| 431 // clang-format on |
| 432 |
| 433 for (const PasswordFormTestData& data : test_data) { |
| 434 SCOPED_TRACE(testing::Message() |
| 435 << "for page_location=" << data.page_location |
| 436 << " and json_string=" << data.json_string); |
| 437 std::unique_ptr<base::Value> json_data( |
| 438 base::JSONReader::Read(data.json_string, true)); |
| 439 const base::DictionaryValue* json_dict = nullptr; |
| 440 ASSERT_TRUE(json_data->GetAsDictionary(&json_dict)); |
| 441 PasswordForm form; |
| 442 [passwordController_ getPasswordForm:&form |
| 443 fromDictionary:json_dict |
| 444 pageURL:GURL(data.page_location)]; |
| 445 EXPECT_STREQ(data.expected_origin, form.origin.spec().c_str()); |
| 446 EXPECT_STREQ(data.expected_action, form.action.spec().c_str()); |
| 447 EXPECT_EQ(base::ASCIIToUTF16(data.expected_username_element), |
| 448 form.username_element); |
| 449 EXPECT_EQ(base::ASCIIToUTF16(data.expected_username_value), |
| 450 form.username_value); |
| 451 EXPECT_EQ(base::ASCIIToUTF16(data.expected_new_password_element), |
| 452 form.new_password_element); |
| 453 EXPECT_EQ(base::ASCIIToUTF16(data.expected_new_password_value), |
| 454 form.new_password_value); |
| 455 EXPECT_EQ(base::ASCIIToUTF16(data.expected_old_password_element), |
| 456 form.password_element); |
| 457 EXPECT_EQ(base::ASCIIToUTF16(data.expected_old_password_value), |
| 458 form.password_value); |
| 459 } |
| 460 }; |
| 461 |
| 462 struct FindPasswordFormTestData { |
| 463 NSString* html_string; |
| 464 const bool expected_form_found; |
| 465 const char* const expected_username_element; |
| 466 const char* const expected_password_element; |
| 467 }; |
| 468 |
| 469 // TODO(crbug.com/403705) This test is flaky. |
| 470 // Check that HTML forms are converted correctly into PasswordForms. |
| 471 TEST_F(PasswordControllerTest, FLAKY_FindPasswordFormsInView) { |
| 472 // clang-format off |
| 473 FindPasswordFormTestData test_data[] = { |
| 474 // Normal form: a username and a password element. |
| 475 { |
| 476 @"<form>" |
| 477 "<input type='text' name='user0'>" |
| 478 "<input type='password' name='pass0'>" |
| 479 "</form>", |
| 480 true, "user0", "pass0" |
| 481 }, |
| 482 // User name is captured as an email address (HTML5). |
| 483 { |
| 484 @"<form>" |
| 485 "<input type='email' name='email1'>" |
| 486 "<input type='password' name='pass1'>" |
| 487 "</form>", |
| 488 true, "email1", "pass1" |
| 489 }, |
| 490 // No username element. |
| 491 { |
| 492 @"<form>" |
| 493 "<input type='password' name='user2'>" |
| 494 "<input type='password' name='pass2'>" |
| 495 "</form>", |
| 496 true, "", "user2" |
| 497 }, |
| 498 // No username element before password. |
| 499 { |
| 500 @"<form>" |
| 501 "<input type='password' name='pass3'>" |
| 502 "<input type='text' name='user3'>" |
| 503 "</form>", |
| 504 true, "", "pass3" |
| 505 }, |
| 506 // Disabled username element. |
| 507 { |
| 508 @"<form>" |
| 509 "<input type='text' name='user4' disabled='disabled'>" |
| 510 "<input type='password' name='pass4'>" |
| 511 "</form>", |
| 512 true, "", "pass4" |
| 513 }, |
| 514 // Username element has autocomplete='off'. |
| 515 { |
| 516 @"<form>" |
| 517 "<input type='text' name='user5' AUTOCOMPLETE='off'>" |
| 518 "<input type='password' name='pass5'>" |
| 519 "</form>", |
| 520 true, "user5", "pass5" |
| 521 }, |
| 522 // No password element. |
| 523 { |
| 524 @"<form>" |
| 525 "<input type='text' name='user6'>" |
| 526 "<input type='text' name='pass6'>" |
| 527 "</form>", |
| 528 false, nullptr, nullptr |
| 529 }, |
| 530 // Disabled password element. |
| 531 { |
| 532 @"<form>" |
| 533 "<input type='text' name='user7'>" |
| 534 "<input type='password' name='pass7' disabled='disabled'>" |
| 535 "</form>", |
| 536 false, nullptr, nullptr |
| 537 }, |
| 538 // Password element has autocomplete='off'. |
| 539 { |
| 540 @"<form>" |
| 541 "<input type='text' name='user8'>" |
| 542 "<input type='password' name='pass8' AUTOCOMPLETE='OFF'>" |
| 543 "</form>", |
| 544 true, "user8", "pass8" |
| 545 }, |
| 546 // Form element has autocomplete='off'. |
| 547 { |
| 548 @"<form autocomplete='off'>" |
| 549 "<input type='text' name='user9'>" |
| 550 "<input type='password' name='pass9'>" |
| 551 "</form>", |
| 552 true, "user9", "pass9" |
| 553 }, |
| 554 }; |
| 555 // clang-format on |
| 556 |
| 557 for (const FindPasswordFormTestData& data : test_data) { |
| 558 SCOPED_TRACE(testing::Message() << "for html_string=" << data.html_string); |
| 559 LoadHtml(data.html_string); |
| 560 __block std::vector<PasswordForm> forms; |
| 561 __block BOOL block_was_called = NO; |
| 562 [passwordController_ findPasswordFormsWithCompletionHandler:^( |
| 563 const std::vector<PasswordForm>& result) { |
| 564 block_was_called = YES; |
| 565 forms = result; |
| 566 }]; |
| 567 base::test::ios::WaitUntilCondition(^bool() { |
| 568 return block_was_called; |
| 569 }); |
| 570 if (data.expected_form_found) { |
| 571 ASSERT_EQ(1U, forms.size()); |
| 572 EXPECT_EQ(base::ASCIIToUTF16(data.expected_username_element), |
| 573 forms[0].username_element); |
| 574 EXPECT_EQ(base::ASCIIToUTF16(data.expected_password_element), |
| 575 forms[0].password_element); |
| 576 } else { |
| 577 ASSERT_TRUE(forms.empty()); |
| 578 } |
| 579 } |
| 580 } |
| 581 |
| 582 struct GetSubmittedPasswordFormTestData { |
| 583 NSString* html_string; |
| 584 NSString* java_script; |
| 585 const int number_of_forms_to_submit; |
| 586 const bool expected_form_found; |
| 587 const char* expected_username_element; |
| 588 }; |
| 589 |
| 590 // TODO(crbug.com/403705) This test is flaky. |
| 591 // Check that HTML forms are captured and converted correctly into |
| 592 // PasswordForms on submission. |
| 593 TEST_F(PasswordControllerTest, FLAKY_GetSubmittedPasswordForm) { |
| 594 // clang-format off |
| 595 GetSubmittedPasswordFormTestData test_data[] = { |
| 596 // Two forms with no explicit names. |
| 597 { |
| 598 @"<form action='javascript:;'>" |
| 599 "<input type='text' name='user1'>" |
| 600 "<input type='password' name='pass1'>" |
| 601 "</form>" |
| 602 "<form action='javascript:;'>" |
| 603 "<input type='text' name='user2'>" |
| 604 "<input type='password' name='pass2'>" |
| 605 "<input type='submit' id='s2'>" |
| 606 "</form>", |
| 607 @"document.getElementById('s2').click()", |
| 608 1, true, "user2" |
| 609 }, |
| 610 // Two forms with explicit names. |
| 611 { |
| 612 @"<form name='test2a' action='javascript:;'>" |
| 613 "<input type='text' name='user1'>" |
| 614 "<input type='password' name='pass1'>" |
| 615 "<input type='submit' id='s1'>" |
| 616 "</form>" |
| 617 "<form name='test2b' action='javascript:;'>" |
| 618 "<input type='text' name='user2'>" |
| 619 "<input type='password' name='pass2'>" |
| 620 "</form>", |
| 621 @"document.getElementById('s1').click()", |
| 622 0, true, "user1" |
| 623 }, |
| 624 // No password forms. |
| 625 { |
| 626 @"<form action='javascript:;'>" |
| 627 "<input type='text' name='user1'>" |
| 628 "<input type='text' name='pass1'>" |
| 629 "<input type='submit' id='s1'>" |
| 630 "</form>", |
| 631 @"document.getElementById('s1').click()", |
| 632 0, false, nullptr |
| 633 }, |
| 634 // Form with quotes in the form and field names. |
| 635 { |
| 636 @"<form name=\"foo'\" action='javascript:;'>" |
| 637 "<input type='text' name=\"user1'\">" |
| 638 "<input type='password' id='s1' name=\"pass1'\">" |
| 639 "</form>", |
| 640 @"document.getElementById('s1').click()", |
| 641 0, true, "user1'" |
| 642 }, |
| 643 }; |
| 644 // clang-format on |
| 645 |
| 646 for (const GetSubmittedPasswordFormTestData& data : test_data) { |
| 647 SCOPED_TRACE(testing::Message() << "for html_string=" << data.html_string |
| 648 << " and java_script=" << data.java_script |
| 649 << " and number_of_forms_to_submit=" |
| 650 << data.number_of_forms_to_submit); |
| 651 LoadHtml(data.html_string); |
| 652 EvaluateJavaScriptAsString(data.java_script); |
| 653 __block BOOL block_was_called = NO; |
| 654 id completion_handler = ^(BOOL found, const PasswordForm& form) { |
| 655 block_was_called = YES; |
| 656 ASSERT_EQ(data.expected_form_found, found); |
| 657 if (data.expected_form_found) { |
| 658 EXPECT_EQ(base::ASCIIToUTF16(data.expected_username_element), |
| 659 form.username_element); |
| 660 } |
| 661 }; |
| 662 [passwordController_ |
| 663 extractSubmittedPasswordForm:FormName(data.number_of_forms_to_submit) |
| 664 completionHandler:completion_handler]; |
| 665 base::test::ios::WaitUntilCondition(^bool() { |
| 666 return block_was_called; |
| 667 }); |
| 668 } |
| 669 } |
| 670 |
| 671 // Populates |form_data| with test values. |
| 672 void SetPasswordFormFillData(PasswordFormFillData& form_data, |
| 673 const std::string& origin, |
| 674 const std::string& action, |
| 675 const char* username_field, |
| 676 const char* username_value, |
| 677 const char* password_field, |
| 678 const char* password_value, |
| 679 const char* additional_username, |
| 680 const char* additional_password, |
| 681 bool wait_for_username) { |
| 682 form_data.origin = GURL(origin); |
| 683 form_data.action = GURL(action); |
| 684 autofill::FormFieldData username; |
| 685 username.name = base::UTF8ToUTF16(username_field); |
| 686 username.value = base::UTF8ToUTF16(username_value); |
| 687 form_data.username_field = username; |
| 688 autofill::FormFieldData password; |
| 689 password.name = base::UTF8ToUTF16(password_field); |
| 690 password.value = base::UTF8ToUTF16(password_value); |
| 691 form_data.password_field = password; |
| 692 if (additional_username) { |
| 693 autofill::PasswordAndRealm additional_password_data; |
| 694 additional_password_data.password = base::UTF8ToUTF16(additional_password); |
| 695 additional_password_data.realm.clear(); |
| 696 form_data.additional_logins.insert( |
| 697 std::pair<base::string16, autofill::PasswordAndRealm>( |
| 698 base::UTF8ToUTF16(additional_username), additional_password_data)); |
| 699 } |
| 700 form_data.wait_for_username = wait_for_username; |
| 701 } |
| 702 |
| 703 // Test HTML page. It contains several password forms. Tests autofill |
| 704 // them and verify that the right ones are autofilled. |
| 705 static NSString* kHtmlWithMultiplePasswordForms = |
| 706 @"<form>" |
| 707 "<input id='un0' type='text' name='u0'>" |
| 708 "<input id='pw0' type='password' name='p0'>" |
| 709 "</form>" |
| 710 "<form action='action?query=yes#reference'>" |
| 711 "<input id='un1' type='text' name='u1'>" |
| 712 "<input id='pw1' type='password' name='p1'>" |
| 713 "</form>" |
| 714 "<form action='http://some_other_action'>" |
| 715 "<input id='un2' type='text' name='u2'>" |
| 716 "<input id='pw2' type='password' name='p2'>" |
| 717 "</form>" |
| 718 "<form>" |
| 719 "<input id='un3' type='text' name='u3'>" |
| 720 "<input id='pw3' type='password' name='p3'>" |
| 721 "<input id='pw3' type='password' name='p3'>" |
| 722 "</form>" |
| 723 "<form>" |
| 724 "<input id='un4' type='text' name='u4'>" |
| 725 "<input id='pw4' type='password' name='p4'>" |
| 726 "</form>" |
| 727 "<form>" |
| 728 "<input id='un5' type='text' name='u4'>" |
| 729 "<input id='pw5' type='password' name='p4'>" |
| 730 "</form>" |
| 731 "<form name=\"f6'\">" |
| 732 "<input id=\"un6'\" type='text' name=\"u6'\">" |
| 733 "<input id=\"pw6'\" type='password' name=\"p6'\">" |
| 734 "</form>"; |
| 735 |
| 736 // A script that resets all text fields. |
| 737 static NSString* kClearInputFieldsScript = |
| 738 @"var inputs = document.getElementsByTagName('input');" |
| 739 "for(var i = 0; i < inputs.length; i++){" |
| 740 " inputs[i].value = '';" |
| 741 "}"; |
| 742 |
| 743 // A script that we run after autofilling forms. It returns |
| 744 // ids and values of all non-empty fields. |
| 745 static NSString* kInputFieldValueVerificationScript = |
| 746 @"var result='';" |
| 747 "var inputs = document.getElementsByTagName('input');" |
| 748 "for(var i = 0; i < inputs.length; i++){" |
| 749 " var input = inputs[i];" |
| 750 " if (input.value) {" |
| 751 " result += input.id + '=' + input.value +';';" |
| 752 " }" |
| 753 "}; result"; |
| 754 |
| 755 struct FillPasswordFormTestData { |
| 756 const std::string origin; |
| 757 const std::string action; |
| 758 const char* username_field; |
| 759 const char* username_value; |
| 760 const char* password_field; |
| 761 const char* password_value; |
| 762 const BOOL should_succeed; |
| 763 NSString* expected_result; |
| 764 }; |
| 765 |
| 766 // Test that filling password forms works correctly. |
| 767 TEST_F(PasswordControllerTest, FillPasswordForm) { |
| 768 LoadHtml(kHtmlWithMultiplePasswordForms); |
| 769 |
| 770 EXPECT_NSEQ(@"true", |
| 771 EvaluateJavaScriptAsString(@"__gCrWeb.hasPasswordField()")); |
| 772 |
| 773 const std::string base_url = BaseUrl(); |
| 774 // clang-format off |
| 775 FillPasswordFormTestData test_data[] = { |
| 776 // Basic test: one-to-one match on the first password form. |
| 777 { |
| 778 base_url, |
| 779 base_url, |
| 780 "u0", |
| 781 "test_user", |
| 782 "p0", |
| 783 "test_password", |
| 784 YES, |
| 785 @"un0=test_user;pw0=test_password;" |
| 786 }, |
| 787 // Multiple forms match: they should all be autofilled. |
| 788 { |
| 789 base_url, |
| 790 base_url, |
| 791 "u4", |
| 792 "test_user", |
| 793 "p4", |
| 794 "test_password", |
| 795 YES, |
| 796 @"un4=test_user;pw4=test_password;un5=test_user;pw5=test_password;" |
| 797 }, |
| 798 // The form matches despite a different action: the only difference |
| 799 // is a query and reference. |
| 800 { |
| 801 base_url, |
| 802 base_url, |
| 803 "u1", |
| 804 "test_user", |
| 805 "p1", |
| 806 "test_password", |
| 807 YES, |
| 808 @"un1=test_user;pw1=test_password;" |
| 809 }, |
| 810 // No match because of a different origin. |
| 811 { |
| 812 "http://someotherfakedomain.com", |
| 813 base_url, |
| 814 "u0", |
| 815 "test_user", |
| 816 "p0", |
| 817 "test_password", |
| 818 NO, |
| 819 @"" |
| 820 }, |
| 821 // No match because of a different action. |
| 822 { |
| 823 base_url, |
| 824 "http://someotherfakedomain.com", |
| 825 "u0", |
| 826 "test_user", |
| 827 "p0", |
| 828 "test_password", |
| 829 NO, |
| 830 @"" |
| 831 }, |
| 832 // No match because some inputs are not in the form. |
| 833 { |
| 834 base_url, |
| 835 base_url, |
| 836 "u0", |
| 837 "test_user", |
| 838 "p1", |
| 839 "test_password", |
| 840 NO, |
| 841 @"" |
| 842 }, |
| 843 // No match because there are duplicate inputs in the form. |
| 844 { |
| 845 base_url, |
| 846 base_url, |
| 847 "u3", |
| 848 "test_user", |
| 849 "p3", |
| 850 "test_password", |
| 851 NO, |
| 852 @"" |
| 853 }, |
| 854 // Basic test, but with quotes in the names and IDs. |
| 855 { |
| 856 base_url, |
| 857 base_url, |
| 858 "u6'", |
| 859 "test_user", |
| 860 "p6'", |
| 861 "test_password", |
| 862 YES, |
| 863 @"un6'=test_user;pw6'=test_password;" |
| 864 }, |
| 865 }; |
| 866 // clang-format on |
| 867 |
| 868 for (const FillPasswordFormTestData& data : test_data) { |
| 869 EvaluateJavaScriptAsString(kClearInputFieldsScript); |
| 870 |
| 871 PasswordFormFillData form_data; |
| 872 SetPasswordFormFillData(form_data, data.origin, data.action, |
| 873 data.username_field, data.username_value, |
| 874 data.password_field, data.password_value, nullptr, |
| 875 nullptr, false); |
| 876 |
| 877 __block BOOL block_was_called = NO; |
| 878 [passwordController_ fillPasswordForm:form_data |
| 879 completionHandler:^(BOOL success) { |
| 880 block_was_called = YES; |
| 881 EXPECT_EQ(data.should_succeed, success); |
| 882 }]; |
| 883 base::test::ios::WaitUntilCondition(^bool() { |
| 884 return block_was_called; |
| 885 }); |
| 886 |
| 887 NSString* result = |
| 888 EvaluateJavaScriptAsString(kInputFieldValueVerificationScript); |
| 889 EXPECT_NSEQ(data.expected_result, result); |
| 890 } |
| 891 } |
| 892 |
| 893 // Tests that a form is found and the found form is filled in with the given |
| 894 // username and password. |
| 895 TEST_F(PasswordControllerTest, FindAndFillOnePasswordForm) { |
| 896 LoadHtml(@"<form><input id='un' type='text' name='u'>" |
| 897 "<input id='pw' type='password' name='p'></form>"); |
| 898 __block int call_counter = 0; |
| 899 __block int success_counter = 0; |
| 900 [passwordController_ findAndFillPasswordForms:@"john.doe@gmail.com" |
| 901 password:@"super!secret" |
| 902 completionHandler:^(BOOL complete) { |
| 903 ++call_counter; |
| 904 if (complete) |
| 905 ++success_counter; |
| 906 }]; |
| 907 base::test::ios::WaitUntilCondition(^{ |
| 908 return call_counter == 1; |
| 909 }); |
| 910 EXPECT_EQ(1, success_counter); |
| 911 NSString* result = |
| 912 EvaluateJavaScriptAsString(kInputFieldValueVerificationScript); |
| 913 EXPECT_NSEQ(@"un=john.doe@gmail.com;pw=super!secret;", result); |
| 914 } |
| 915 |
| 916 // Tests that multiple forms on the same page are found and filled. |
| 917 // This test includes an mock injected failure on form filling to verify |
| 918 // that completion handler is called with the proper values. |
| 919 TEST_F(PasswordControllerTest, FindAndFillMultiplePasswordForms) { |
| 920 // Fails the first call to fill password form. |
| 921 SetFillPasswordFormFailureCount(1); |
| 922 LoadHtml(@"<form><input id='u1' type='text' name='un1'>" |
| 923 "<input id='p1' type='password' name='pw1'></form>" |
| 924 "<form><input id='u2' type='text' name='un2'>" |
| 925 "<input id='p2' type='password' name='pw2'></form>" |
| 926 "<form><input id='u3' type='text' name='un3'>" |
| 927 "<input id='p3' type='password' name='pw3'></form>"); |
| 928 __block int call_counter = 0; |
| 929 __block int success_counter = 0; |
| 930 [passwordController_ findAndFillPasswordForms:@"john.doe@gmail.com" |
| 931 password:@"super!secret" |
| 932 completionHandler:^(BOOL complete) { |
| 933 ++call_counter; |
| 934 if (complete) |
| 935 ++success_counter; |
| 936 LOG(INFO) << "HANDLER call " << call_counter |
| 937 << " success " << success_counter; |
| 938 }]; |
| 939 // There should be 3 password forms and only 2 successfully filled forms. |
| 940 base::test::ios::WaitUntilCondition(^{ |
| 941 return call_counter == 3; |
| 942 }); |
| 943 EXPECT_EQ(2, success_counter); |
| 944 NSString* result = |
| 945 EvaluateJavaScriptAsString(kInputFieldValueVerificationScript); |
| 946 EXPECT_NSEQ(@"u2=john.doe@gmail.com;p2=super!secret;" |
| 947 "u3=john.doe@gmail.com;p3=super!secret;", |
| 948 result); |
| 949 } |
| 950 |
| 951 BOOL PasswordControllerTest::BasicFormFill(NSString* html) { |
| 952 LoadHtml(html); |
| 953 EXPECT_NSEQ(@"true", |
| 954 EvaluateJavaScriptAsString(@"__gCrWeb.hasPasswordField()")); |
| 955 const std::string base_url = BaseUrl(); |
| 956 PasswordFormFillData form_data; |
| 957 SetPasswordFormFillData(form_data, base_url, base_url, "u0", "test_user", |
| 958 "p0", "test_password", nullptr, nullptr, false); |
| 959 __block BOOL block_was_called = NO; |
| 960 __block BOOL return_value = NO; |
| 961 [passwordController_ fillPasswordForm:form_data |
| 962 completionHandler:^(BOOL success) { |
| 963 block_was_called = YES; |
| 964 return_value = success; |
| 965 }]; |
| 966 base::test::ios::WaitUntilCondition(^bool() { |
| 967 return block_was_called; |
| 968 }); |
| 969 return return_value; |
| 970 } |
| 971 |
| 972 // Check that |fillPasswordForm| is not filled if 'readonly' attribute is set |
| 973 // on either username or password fields. |
| 974 // TODO(crbug.com/503050): Test is flaky. |
| 975 TEST_F(PasswordControllerTest, FLAKY_DontFillReadOnly) { |
| 976 // Control check that the fill operation will succceed with well-formed form. |
| 977 EXPECT_TRUE(BasicFormFill(@"<form>" |
| 978 "<input id='un0' type='text' name='u0'>" |
| 979 "<input id='pw0' type='password' name='p0'>" |
| 980 "</form>")); |
| 981 // Form fill should fail with 'readonly' attribute on username. |
| 982 EXPECT_FALSE(BasicFormFill( |
| 983 @"<form>" |
| 984 "<input id='un0' type='text' name='u0' readonly='readonly'>" |
| 985 "<input id='pw0' type='password' name='p0'>" |
| 986 "</form>")); |
| 987 // Form fill should fail with 'readonly' attribute on password. |
| 988 EXPECT_FALSE(BasicFormFill( |
| 989 @"<form>" |
| 990 "<input id='un0' type='text' name='u0'>" |
| 991 "<input id='pw0' type='password' name='p0' readonly='readonly'>" |
| 992 "</form>")); |
| 993 } |
| 994 |
| 995 // An HTML page containing one password form. The username input field |
| 996 // also has custom event handlers. We need to verify that those event |
| 997 // handlers are still triggered even though we override them with our own. |
| 998 static NSString* kHtmlWithPasswordForm = |
| 999 @"<form>" |
| 1000 "<input id='un' type='text' name=\"u'\"" |
| 1001 " onkeyup='window.onKeyUpCalled_=true'" |
| 1002 " onchange='window.onChangeCalled_=true'>" |
| 1003 "<input id='pw' type='password' name=\"p'\">" |
| 1004 "</form>"; |
| 1005 |
| 1006 // A script that resets indicators used to verify that custom event |
| 1007 // handlers are triggered. It also finds and the username and |
| 1008 // password fields and caches them for future verification. |
| 1009 static NSString* kUsernameAndPasswordTestPreparationScript = |
| 1010 @"onKeyUpCalled_ = false;" |
| 1011 "onChangeCalled_ = false;" |
| 1012 "username_ = document.getElementById('un');" |
| 1013 "username_.__gCrWebAutofilled = 'false';" |
| 1014 "password_ = document.getElementById('pw');" |
| 1015 "password_.__gCrWebAutofilled = 'false';"; |
| 1016 |
| 1017 // A script that we run after autofilling forms. It returns |
| 1018 // all values for verification as a single concatenated string. |
| 1019 static NSString* kUsernamePasswordVerificationScript = |
| 1020 @"var value = username_.value;" |
| 1021 "var from = username_.selectionStart;" |
| 1022 "var to = username_.selectionEnd;" |
| 1023 "value.substr(0, from) + '[' + value.substr(from, to) + ']'" |
| 1024 " + value.substr(to, value.length) + '=' + password_.value" |
| 1025 " + ', onkeyup=' + onKeyUpCalled_" |
| 1026 " + ', onchange=' + onChangeCalled_;"; |
| 1027 |
| 1028 struct SuggestionTestData { |
| 1029 std::string description; |
| 1030 NSArray* eval_scripts; |
| 1031 NSArray* expected_suggestions; |
| 1032 NSString* expected_result; |
| 1033 }; |
| 1034 |
| 1035 // Tests that form activity correctly sends suggestions to the suggestion |
| 1036 // controller. |
| 1037 TEST_F(PasswordControllerTest, SuggestionUpdateTests) { |
| 1038 LoadHtml(kHtmlWithPasswordForm); |
| 1039 const std::string base_url = BaseUrl(); |
| 1040 EvaluateJavaScriptAsString(kUsernameAndPasswordTestPreparationScript); |
| 1041 |
| 1042 // Initialize |form_data| with test data and an indicator that autofill |
| 1043 // should not be performed while the user is entering the username so that |
| 1044 // we can test with an initially-empty username field. Testing with a |
| 1045 // username field that contains input is performed by a specific test below. |
| 1046 PasswordFormFillData form_data; |
| 1047 SetPasswordFormFillData(form_data, base_url, base_url, "u'", "user0", "p'", |
| 1048 "password0", "abc", "def", true); |
| 1049 form_data.name = base::ASCIIToUTF16(FormName(0)); |
| 1050 |
| 1051 __block BOOL block_was_called = NO; |
| 1052 [passwordController_ fillPasswordForm:form_data |
| 1053 completionHandler:^(BOOL success) { |
| 1054 block_was_called = YES; |
| 1055 // Verify that the fill reports failed. |
| 1056 EXPECT_FALSE(success); |
| 1057 }]; |
| 1058 base::test::ios::WaitUntilCondition(^bool() { |
| 1059 return block_was_called; |
| 1060 }); |
| 1061 |
| 1062 // Verify that the form has not been autofilled. |
| 1063 EXPECT_NSEQ(@"[]=, onkeyup=false, onchange=false", |
| 1064 EvaluateJavaScriptAsString(kUsernamePasswordVerificationScript)); |
| 1065 |
| 1066 // clang-format off |
| 1067 SuggestionTestData test_data[] = { |
| 1068 { |
| 1069 "Should show all suggestions when focusing empty username field", |
| 1070 @[(@"var evt = document.createEvent('Events');" |
| 1071 "evt.initEvent('focus', true, true, window, 1);" |
| 1072 "username_.dispatchEvent(evt);"), |
| 1073 @""], |
| 1074 @[@"abc", @"user0"], |
| 1075 @"[]=, onkeyup=false, onchange=false" |
| 1076 }, |
| 1077 { |
| 1078 "Should not show suggestions when focusing password field", |
| 1079 @[(@"var evt = document.createEvent('Events');" |
| 1080 "evt.initEvent('focus', true, true, window, 1);" |
| 1081 "password_.dispatchEvent(evt);"), |
| 1082 @""], |
| 1083 @[], |
| 1084 @"[]=, onkeyup=false, onchange=false" |
| 1085 }, |
| 1086 { |
| 1087 "Should filter suggestions when focusing username field with input", |
| 1088 @[(@"username_.value='ab';" |
| 1089 "var evt = document.createEvent('Events');" |
| 1090 "evt.initEvent('focus', true, true, window, 1);" |
| 1091 "username_.dispatchEvent(evt);"), |
| 1092 @""], |
| 1093 @[@"abc"], |
| 1094 @"ab[]=, onkeyup=false, onchange=false" |
| 1095 }, |
| 1096 { |
| 1097 "Should filter suggestions while typing", |
| 1098 @[(@"var evt = document.createEvent('Events');" |
| 1099 "evt.initEvent('focus', true, true, window, 1);" |
| 1100 "username_.dispatchEvent(evt);"), |
| 1101 (@"username_.value='ab';" |
| 1102 "evt = document.createEvent('Events');" |
| 1103 "evt.initEvent('keyup', true, true, window, 1);" |
| 1104 "evt.keyCode = 98;" |
| 1105 "username_.dispatchEvent(evt);"), |
| 1106 @""], |
| 1107 @[@"abc"], |
| 1108 @"ab[]=, onkeyup=true, onchange=false" |
| 1109 }, |
| 1110 { |
| 1111 "Should unfilter suggestions after backspacing", |
| 1112 @[(@"var evt = document.createEvent('Events');" |
| 1113 "evt.initEvent('focus', true, true, window, 1);" |
| 1114 "username_.dispatchEvent(evt);"), |
| 1115 (@"username_.value='ab';" |
| 1116 "evt = document.createEvent('Events');" |
| 1117 "evt.initEvent('keyup', true, true, window, 1);" |
| 1118 "evt.keyCode = 98;" |
| 1119 "username_.dispatchEvent(evt);"), |
| 1120 (@"username_.value='';" |
| 1121 "evt = document.createEvent('Events');" |
| 1122 "evt.initEvent('keyup', true, true, window, 1);" |
| 1123 "evt.keyCode = 8;" |
| 1124 "username_.dispatchEvent(evt);"), |
| 1125 @""], |
| 1126 @[@"abc", @"user0"], |
| 1127 @"[]=, onkeyup=true, onchange=false" |
| 1128 }, |
| 1129 }; |
| 1130 // clang-format on |
| 1131 |
| 1132 for (const SuggestionTestData& data : test_data) { |
| 1133 SCOPED_TRACE(testing::Message() |
| 1134 << "for description=" << data.description |
| 1135 << " and eval_scripts=" << data.eval_scripts); |
| 1136 // Prepare the test. |
| 1137 EvaluateJavaScriptAsString(kUsernameAndPasswordTestPreparationScript); |
| 1138 |
| 1139 for (NSString* script in data.eval_scripts) { |
| 1140 // Trigger events. |
| 1141 EvaluateJavaScriptAsString(script); |
| 1142 |
| 1143 // Pump the run loop so that the host can respond. |
| 1144 WaitForBackgroundTasks(); |
| 1145 } |
| 1146 |
| 1147 EXPECT_NSEQ(data.expected_suggestions, GetSortedSuggestionValues()); |
| 1148 EXPECT_NSEQ(data.expected_result, EvaluateJavaScriptAsString( |
| 1149 kUsernamePasswordVerificationScript)); |
| 1150 // Clear all suggestions. |
| 1151 [suggestionController_ setSuggestions:nil]; |
| 1152 } |
| 1153 } |
| 1154 |
| 1155 // Tests that selecting a suggestion will fill the corresponding form and field. |
| 1156 TEST_F(PasswordControllerTest, SelectingSuggestionShouldFillPasswordForm) { |
| 1157 LoadHtml(kHtmlWithPasswordForm); |
| 1158 const std::string base_url = BaseUrl(); |
| 1159 EvaluateJavaScriptAsString(kUsernameAndPasswordTestPreparationScript); |
| 1160 |
| 1161 // Initialize |form_data| with test data and an indicator that autofill |
| 1162 // should not be performed while the user is entering the username so that |
| 1163 // we can test with an initially-empty username field. |
| 1164 PasswordFormFillData form_data; |
| 1165 SetPasswordFormFillData(form_data, base_url, base_url, "u'", "user0", "p'", |
| 1166 "password0", "abc", "def", true); |
| 1167 form_data.name = base::ASCIIToUTF16(FormName(0)); |
| 1168 |
| 1169 __block BOOL block_was_called = NO; |
| 1170 [passwordController_ fillPasswordForm:form_data |
| 1171 completionHandler:^(BOOL success) { |
| 1172 block_was_called = YES; |
| 1173 // Verify that the fill reports failed. |
| 1174 EXPECT_FALSE(success); |
| 1175 }]; |
| 1176 base::test::ios::WaitUntilCondition(^bool() { |
| 1177 return block_was_called; |
| 1178 }); |
| 1179 |
| 1180 // Verify that the form has not been autofilled. |
| 1181 EXPECT_NSEQ(@"[]=, onkeyup=false, onchange=false", |
| 1182 EvaluateJavaScriptAsString(kUsernamePasswordVerificationScript)); |
| 1183 |
| 1184 // Tell PasswordController that a suggestion was selected. It should fill |
| 1185 // out the password form with the corresponding credentials. |
| 1186 FormSuggestion* suggestion = [FormSuggestion suggestionWithValue:@"abc" |
| 1187 displayDescription:nil |
| 1188 icon:nil |
| 1189 identifier:0]; |
| 1190 |
| 1191 block_was_called = NO; |
| 1192 SuggestionHandledCompletion completion = ^{ |
| 1193 block_was_called = YES; |
| 1194 EXPECT_NSEQ(@"abc[]=def, onkeyup=false, onchange=false", |
| 1195 ExecuteJavaScript(kUsernamePasswordVerificationScript)); |
| 1196 }; |
| 1197 [passwordController_ didSelectSuggestion:suggestion |
| 1198 forField:@"u" |
| 1199 form:base::SysUTF8ToNSString(FormName(0)) |
| 1200 completionHandler:completion]; |
| 1201 base::test::ios::WaitUntilCondition(^bool() { |
| 1202 return block_was_called; |
| 1203 }); |
| 1204 } |
| 1205 |
| 1206 // Tests with invalid inputs. |
| 1207 TEST_F(PasswordControllerTest, CheckIncorrectData) { |
| 1208 // clang-format off |
| 1209 std::string invalid_data[] = { |
| 1210 "{}", |
| 1211 |
| 1212 "{ \"usernameValue\": \"fakeaccount\"," |
| 1213 "\"passwords\": [" |
| 1214 "{ \"element\": \"secret\"," "\"value\": \"fakesecret\" }," |
| 1215 "]}", |
| 1216 |
| 1217 "{ \"usernameElement\": \"account\"," |
| 1218 "\"passwords\": [" |
| 1219 "{ \"element\": \"secret\"," "\"value\": \"fakesecret\" }," |
| 1220 "]}", |
| 1221 |
| 1222 "{ \"usernameElement\": \"account\"," |
| 1223 "\"usernameValue\": \"fakeaccount\"," |
| 1224 "}", |
| 1225 |
| 1226 "{ \"usernameElement\": \"account\"," |
| 1227 "\"usernameValue\": \"fakeaccount\"," |
| 1228 "\"passwords\": {}," |
| 1229 "}", |
| 1230 |
| 1231 "{ \"usernameElement\": \"account\"," |
| 1232 "\"usernameValue\": \"fakeaccount\"," |
| 1233 "\"passwords\": [" |
| 1234 "]}", |
| 1235 |
| 1236 "{ \"usernameElement\": \"account\"," |
| 1237 "\"usernameValue\": \"fakeaccount\"," |
| 1238 "\"passwords\": [" |
| 1239 "{ \"value\": \"fakesecret\" }," |
| 1240 "]}", |
| 1241 |
| 1242 "{ \"usernameElement\": \"account\"," |
| 1243 "\"usernameValue\": \"fakeaccount\"," |
| 1244 "\"passwords\": [" |
| 1245 "{ \"element\": \"secret\" }," |
| 1246 "]}", |
| 1247 }; |
| 1248 // clang-format on |
| 1249 |
| 1250 for (const std::string& data : invalid_data) { |
| 1251 SCOPED_TRACE(testing::Message() << "for data=" << data); |
| 1252 std::unique_ptr<base::Value> json_data(base::JSONReader::Read(data, true)); |
| 1253 const base::DictionaryValue* json_dict = nullptr; |
| 1254 ASSERT_TRUE(json_data->GetAsDictionary(&json_dict)); |
| 1255 PasswordForm form; |
| 1256 BOOL res = |
| 1257 [passwordController_ getPasswordForm:&form |
| 1258 fromDictionary:json_dict |
| 1259 pageURL:GURL("https://www.foo.com/")]; |
| 1260 EXPECT_FALSE(res); |
| 1261 } |
| 1262 } |
| 1263 |
| 1264 // The test case below does not need the heavy fixture from above, but it |
| 1265 // needs to use MockWebState. |
| 1266 TEST(PasswordControllerTestSimple, SaveOnNonHTMLLandingPage) { |
63 TestChromeBrowserState::Builder builder; | 1267 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()); | 1268 std::unique_ptr<TestChromeBrowserState> browser_state(builder.Build()); |
70 MockWebState web_state; | 1269 MockWebState web_state; |
71 ON_CALL(web_state, GetBrowserState()) | 1270 ON_CALL(web_state, GetBrowserState()) |
72 .WillByDefault(testing::Return(browser_state.get())); | 1271 .WillByDefault(testing::Return(browser_state.get())); |
73 auto client = base::WrapUnique(new MockPasswordManagerClient); | 1272 |
74 MockPasswordManagerClient* weak_client = client.get(); | 1273 MockPasswordManagerClient* weak_client = nullptr; |
75 base::scoped_nsobject<PasswordController> passwordController( | 1274 base::scoped_nsobject<PasswordController> passwordController = |
76 [[PasswordController alloc] initWithWebState:&web_state | 1275 CreatePasswordController(&web_state, nullptr, &weak_client); |
77 passwordsUiDelegate:nil | 1276 static_cast<TestingPrefServiceSimple*>(weak_client->GetPrefs()) |
78 client:std::move(client)]); | 1277 ->registry() |
| 1278 ->RegisterBooleanPref( |
| 1279 password_manager::prefs::kPasswordManagerSavingEnabled, true); |
79 | 1280 |
80 // Use a mock LogManager to detect that OnPasswordFormsRendered has been | 1281 // Use a mock LogManager to detect that OnPasswordFormsRendered has been |
81 // called. TODO(crbug.com/598672): this is a hack, we should modularize the | 1282 // called. TODO(crbug.com/598672): this is a hack, we should modularize the |
82 // code better to allow proper unit-testing. | 1283 // code better to allow proper unit-testing. |
83 MockLogManager log_manager; | 1284 MockLogManager log_manager; |
84 EXPECT_CALL(log_manager, IsLoggingActive()).WillRepeatedly(Return(true)); | 1285 EXPECT_CALL(log_manager, IsLoggingActive()).WillRepeatedly(Return(true)); |
85 EXPECT_CALL(log_manager, | 1286 EXPECT_CALL(log_manager, |
86 LogSavePasswordProgress( | 1287 LogSavePasswordProgress( |
87 "Message: \"PasswordManager::OnPasswordFormsRendered\"\n")); | 1288 "Message: \"PasswordManager::OnPasswordFormsRendered\"\n")); |
88 EXPECT_CALL(log_manager, | 1289 EXPECT_CALL(log_manager, |
89 LogSavePasswordProgress(testing::Ne( | 1290 LogSavePasswordProgress(testing::Ne( |
90 "Message: \"PasswordManager::OnPasswordFormsRendered\"\n"))) | 1291 "Message: \"PasswordManager::OnPasswordFormsRendered\"\n"))) |
91 .Times(testing::AnyNumber()); | 1292 .Times(testing::AnyNumber()); |
92 EXPECT_CALL(*weak_client, GetLogManager()) | 1293 EXPECT_CALL(*weak_client, GetLogManager()) |
93 .WillRepeatedly(Return(&log_manager)); | 1294 .WillRepeatedly(Return(&log_manager)); |
94 | 1295 |
95 web_state.SetContentIsHTML(false); | 1296 web_state.SetContentIsHTML(false); |
96 web_state.SetCurrentURL(GURL("https://example.com")); | 1297 web_state.SetCurrentURL(GURL("https://example.com")); |
97 [passwordController webStateDidLoadPage:&web_state]; | 1298 [passwordController webStateDidLoadPage:&web_state]; |
98 } | 1299 } |
OLD | NEW |