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 |