OLD | NEW |
---|---|
(Empty) | |
1 // Copyright 2014 The Chromium Authors. All rights reserved. | |
2 // Use of this source code is governed by a BSD-style license that can be | |
3 // found in the LICENSE file. | |
4 | |
5 #import "ios/chrome/browser/passwords/password_generation_agent.h" | |
6 | |
7 #include <algorithm> | |
8 #include <memory> | |
9 | |
10 #include "base/logging.h" | |
11 #include "base/mac/foundation_util.h" | |
12 #include "base/mac/scoped_nsobject.h" | |
Eugene But (OOO till 7-30)
2016/07/13 22:30:31
s/include/import
vabr (Chromium)
2016/07/14 07:36:31
Done.
| |
13 #include "base/mac/scoped_objc_class_swizzler.h" | |
14 #include "base/macros.h" | |
15 #include "base/strings/string16.h" | |
16 #include "base/strings/sys_string_conversions.h" | |
17 #include "base/strings/utf_string_conversions.h" | |
18 #include "components/autofill/core/common/form_data.h" | |
19 #include "components/autofill/core/common/form_field_data.h" | |
20 #include "components/autofill/core/common/password_form.h" | |
21 #import "components/autofill/ios/browser/js_suggestion_manager.h" | |
22 #include "google_apis/gaia/gaia_urls.h" | |
23 #import "ios/chrome/browser/autofill/form_input_accessory_view_controller.h" | |
24 #import "ios/chrome/browser/passwords/js_password_manager.h" | |
25 #import "ios/chrome/browser/passwords/password_generation_offer_view.h" | |
26 #import "ios/chrome/browser/passwords/passwords_ui_delegate.h" | |
27 #import "ios/chrome/browser/ui/commands/generic_chrome_command.h" | |
28 #include "ios/chrome/browser/ui/commands/ios_command_ids.h" | |
29 #import "ios/testing/ocmock_complex_type_helper.h" | |
30 #include "ios/web/public/test/test_web_state.h" | |
31 #include "ios/web/public/web_state/url_verification_constants.h" | |
32 #import "ios/web/public/test/web_test_with_web_state.h" | |
33 #include "testing/gtest_mac.h" | |
34 #include "third_party/ocmock/OCMock/OCMock.h" | |
35 #include "third_party/ocmock/gtest_support.h" | |
36 #include "url/gurl.h" | |
37 | |
38 namespace { | |
39 | |
40 NSString* const kAccountCreationFormName = @"create-foo-account"; | |
41 NSString* const kAccountCreationFieldName = @"password"; | |
42 NSString* const kAccountCreationOrigin = @"http://foo.com/login"; | |
43 NSString* const kEmailFieldName = @"email"; | |
44 | |
45 // Static storage to access arguments passed to swizzled method of UIWindow. | |
46 static id g_chrome_execute_command_sender = nil; | |
47 | |
48 } // namespace | |
49 | |
50 @interface MockPasswordsUiDelegate : NSObject<PasswordsUiDelegate> | |
51 | |
52 - (instancetype)init; | |
53 | |
54 @property(nonatomic, readonly) BOOL UIShown; | |
55 | |
56 @end | |
57 | |
58 @implementation MockPasswordsUiDelegate { | |
59 // YES if showGenerationAlertWithPassword was called more recently than | |
60 // hideGenerationAlert, NO otherwise. | |
61 BOOL _UIShown; | |
62 } | |
63 | |
64 - (instancetype)init { | |
65 self = [super init]; | |
66 if (self) { | |
67 _UIShown = NO; | |
68 } | |
69 return self; | |
70 } | |
71 | |
72 @synthesize UIShown = _UIShown; | |
73 | |
74 - (void)showGenerationAlertWithPassword:(NSString*)password | |
75 andPromptDelegate: | |
76 (id<PasswordGenerationPromptDelegate>)delegate { | |
77 _UIShown = YES; | |
78 } | |
79 | |
80 - (void)hideGenerationAlert { | |
81 _UIShown = NO; | |
82 } | |
83 | |
84 @end | |
85 | |
86 // A donor class that provides a chromeExecuteCommand method that can be | |
87 // swapped with UIWindow. | |
88 @interface DonorWindow : NSObject | |
89 | |
90 - (void)chromeExecuteCommand:(id)sender; | |
91 | |
92 @end | |
93 | |
94 @implementation DonorWindow | |
95 | |
96 - (void)chromeExecuteCommand:(id)sender { | |
97 g_chrome_execute_command_sender = [sender retain]; | |
98 } | |
99 | |
100 @end | |
101 | |
102 namespace { | |
103 | |
104 // A helper to swizzle chromeExecuteCommand method on UIWindow. | |
105 class ScopedWindowSwizzler { | |
106 public: | |
107 ScopedWindowSwizzler() | |
108 : class_swizzler_([UIWindow class], | |
109 [DonorWindow class], | |
110 @selector(chromeExecuteCommand:)) { | |
111 DCHECK(!g_chrome_execute_command_sender); | |
112 } | |
113 | |
114 ~ScopedWindowSwizzler() { | |
115 [g_chrome_execute_command_sender release]; | |
116 g_chrome_execute_command_sender = nil; | |
117 } | |
118 | |
119 private: | |
120 base::mac::ScopedObjCClassSwizzler class_swizzler_; | |
121 | |
122 DISALLOW_COPY_AND_ASSIGN(ScopedWindowSwizzler); | |
123 }; | |
124 | |
125 // Returns a form that should be marked as an account creation form by local | |
126 // heuristics. | |
127 autofill::PasswordForm GetAccountCreationForm() { | |
128 autofill::FormFieldData name_field; | |
129 name_field.name = base::ASCIIToUTF16("name"); | |
130 name_field.form_control_type = "text"; | |
131 | |
132 autofill::FormFieldData email_field; | |
133 email_field.name = base::ASCIIToUTF16("email"); | |
134 email_field.form_control_type = "email"; | |
135 | |
136 autofill::FormFieldData password_field; | |
137 password_field.name = base::SysNSStringToUTF16(kAccountCreationFieldName); | |
138 password_field.form_control_type = "password"; | |
139 | |
140 autofill::FormFieldData confirmPasswordField; | |
141 confirmPasswordField.name = base::ASCIIToUTF16("confirm"); | |
142 confirmPasswordField.form_control_type = "password"; | |
143 | |
144 autofill::FormData form; | |
145 form.name = base::SysNSStringToUTF16(kAccountCreationFormName); | |
146 form.origin = GURL(base::SysNSStringToUTF8(kAccountCreationOrigin)); | |
147 form.action = GURL(base::SysNSStringToUTF8(kAccountCreationOrigin)); | |
148 | |
149 form.fields.push_back(name_field); | |
150 form.fields.push_back(email_field); | |
151 form.fields.push_back(password_field); | |
152 form.fields.push_back(confirmPasswordField); | |
153 | |
154 autofill::PasswordForm password_form; | |
155 password_form.origin = form.origin; | |
156 password_form.username_element = email_field.name; | |
157 password_form.password_element = password_field.name; | |
158 | |
159 password_form.form_data = form; | |
160 | |
161 return password_form; | |
162 } | |
163 | |
164 // Executes each block in |blocks|, where each block must have the type | |
165 // void^(void). | |
166 void ExecuteBlocks(NSArray* blocks) { | |
167 for (void (^block)(void) in blocks) | |
168 block(); | |
169 } | |
170 | |
171 // Returns a form that has the same origin as GAIA. | |
172 autofill::PasswordForm GetGAIAForm() { | |
173 autofill::PasswordForm form(GetAccountCreationForm()); | |
174 form.origin = GaiaUrls::GetInstance()->gaia_login_form_realm(); | |
175 form.signon_realm = form.origin.GetOrigin().spec(); | |
176 return form; | |
177 } | |
178 | |
179 // Returns a form with no text fields. | |
180 autofill::PasswordForm GetFormWithNoTextFields() { | |
181 autofill::PasswordForm form(GetAccountCreationForm()); | |
182 form.form_data.fields.clear(); | |
183 return form; | |
184 } | |
185 | |
186 // Returns true if |field| has type "password" and false otherwise. | |
187 bool IsPasswordField(const autofill::FormFieldData& field) { | |
188 return field.form_control_type == "password"; | |
189 } | |
190 | |
191 // Returns all password fields in |form|. | |
192 std::vector<autofill::FormFieldData> GetPasswordFields( | |
193 const autofill::PasswordForm& form) { | |
194 std::vector<autofill::FormFieldData> fields; | |
195 fields.reserve(form.form_data.fields.size()); | |
196 for (const auto& field : form.form_data.fields) { | |
197 if (IsPasswordField(field)) | |
198 fields.push_back(field); | |
199 } | |
200 return fields; | |
201 } | |
202 | |
203 // Returns a form with no password fields. | |
204 autofill::PasswordForm GetFormWithNoPasswordFields() { | |
205 autofill::PasswordForm form(GetAccountCreationForm()); | |
206 form.form_data.fields.erase( | |
207 std::remove_if(form.form_data.fields.begin(), form.form_data.fields.end(), | |
208 &IsPasswordField), | |
209 form.form_data.fields.end()); | |
210 return form; | |
211 } | |
212 | |
213 // Test fixture for testing PasswordGenerationAgent. | |
214 class PasswordGenerationAgentTest : public web::WebTestWithWebState { | |
215 public: | |
216 void SetUp() override { | |
217 web::WebTestWithWebState::SetUp(); | |
218 mock_js_suggestion_manager_.reset( | |
219 [[OCMockObject niceMockForClass:[JsSuggestionManager class]] retain]); | |
220 mock_js_password_manager_.reset( | |
221 [[OCMockObject niceMockForClass:[JsPasswordManager class]] retain]); | |
222 mock_ui_delegate_.reset([[MockPasswordsUiDelegate alloc] init]); | |
223 test_web_state_.reset(new web::TestWebState); | |
224 agent_.reset([[PasswordGenerationAgent alloc] | |
225 initWithWebState:test_web_state_.get() | |
226 passwordManager:nullptr | |
227 passwordManagerDriver:nullptr | |
228 JSPasswordManager:mock_js_password_manager_ | |
229 JSSuggestionManager:mock_js_suggestion_manager_ | |
230 passwordsUiDelegate:mock_ui_delegate_]); | |
231 @autoreleasepool { | |
232 accessory_view_controller_.reset([[FormInputAccessoryViewController alloc] | |
233 initWithWebState:test_web_state_.get() | |
234 providers:@[ agent_ ]]); | |
235 } | |
236 } | |
237 | |
238 // Sends form data, autofill data, and password manager data to the | |
239 // generation agent so that it can find an account creation form and password | |
240 // field. | |
241 void LoadAccountCreationForm() { | |
242 autofill::PasswordForm password_form(GetAccountCreationForm()); | |
243 [agent() allowPasswordGenerationForForm:password_form]; | |
244 std::vector<autofill::PasswordForm> password_forms; | |
245 password_forms.push_back(password_form); | |
246 [agent() processParsedPasswordForms:password_forms]; | |
247 SetCurrentURLAndTrustLevel( | |
248 GURL(base::SysNSStringToUTF8(kAccountCreationOrigin)), | |
249 web::URLVerificationTrustLevel::kAbsolute); | |
250 SetContentIsHTML(YES); | |
251 } | |
252 | |
253 // Sets up the web controller mock to use the specified URL and trust level. | |
254 void SetCurrentURLAndTrustLevel( | |
255 GURL url, | |
256 web::URLVerificationTrustLevel url_trust_level) { | |
257 test_web_state_->SetCurrentURL(url); | |
258 test_web_state_->SetTrustLevel(url_trust_level); | |
259 } | |
260 | |
261 // Swizzles the current web controller to set whether the content is HTML. | |
262 void SetContentIsHTML(BOOL content_is_html) { | |
263 test_web_state_->SetContentIsHTML(content_is_html); | |
264 } | |
265 | |
266 // Simulates an event on the specified form/field. | |
267 void SimulateFormActivity(NSString* form_name, | |
268 NSString* field_name, | |
269 NSString* type) { | |
270 [accessory_view_controller_ webState:test_web_state_.get() | |
271 didRegisterFormActivityWithFormNamed:base::SysNSStringToUTF8(form_name) | |
272 fieldName:base::SysNSStringToUTF8(field_name) | |
273 type:base::SysNSStringToUTF8(type) | |
274 value:"" | |
275 keyCode:web::WebStateObserver:: | |
276 kInvalidFormKeyCode | |
277 inputMissing:false]; | |
278 } | |
279 | |
280 // Returns a mock of JsSuggestionManager. | |
281 id mock_js_suggestion_manager() { return mock_js_suggestion_manager_; } | |
282 | |
283 // Returns a mock of JsPasswordManager. | |
284 id mock_js_password_manager() { return mock_js_password_manager_; } | |
285 | |
286 MockPasswordsUiDelegate* mock_ui_delegate() { return mock_ui_delegate_; } | |
287 | |
288 protected: | |
289 // Returns the current generation agent. | |
290 PasswordGenerationAgent* agent() { return agent_.get(); } | |
291 | |
292 // Returns the current accessory view controller. | |
293 FormInputAccessoryViewController* accessory_controller() { | |
294 return accessory_view_controller_.get(); | |
295 } | |
296 | |
297 private: | |
298 // Test WebState. | |
299 std::unique_ptr<web::TestWebState> test_web_state_; | |
300 | |
301 // Mock for JsSuggestionManager; | |
302 base::scoped_nsobject<id> mock_js_suggestion_manager_; | |
303 | |
304 // Mock for JsPasswordManager. | |
305 base::scoped_nsobject<id> mock_js_password_manager_; | |
306 | |
307 // Mock for the UI delegate. | |
308 base::scoped_nsobject<MockPasswordsUiDelegate> mock_ui_delegate_; | |
309 | |
310 // Controller that shows custom input accessory views. | |
311 base::scoped_nsobject<FormInputAccessoryViewController> | |
312 accessory_view_controller_; | |
313 | |
314 // The current generation agent. | |
315 base::scoped_nsobject<PasswordGenerationAgent> agent_; | |
316 }; | |
317 | |
318 // Tests that local heuristics skip forms with GAIA realm. | |
319 TEST_F(PasswordGenerationAgentTest, | |
320 OnParsedForms_ShouldIgnoreFormsWithGaiaRealm) { | |
321 // Send only a form with GAIA origin to the agent. | |
322 std::vector<autofill::PasswordForm> forms; | |
323 forms.push_back(GetGAIAForm()); | |
324 [agent() processParsedPasswordForms:forms]; | |
325 | |
326 // No account creation form should have been found. | |
327 EXPECT_FALSE(agent().possibleAccountCreationForm); | |
328 EXPECT_TRUE(agent().passwordFields.empty()); | |
329 } | |
330 | |
331 // Tests that local heuristics skip forms with no text fields. | |
332 TEST_F(PasswordGenerationAgentTest, | |
333 OnParsedForms_ShouldIgnoreFormsWithNotEnoughTextFields) { | |
334 // Send only a form with GAIA origin to the agent. | |
335 std::vector<autofill::PasswordForm> forms; | |
336 forms.push_back(GetFormWithNoTextFields()); | |
337 [agent() processParsedPasswordForms:forms]; | |
338 | |
339 // No account creation form should have been found. | |
340 EXPECT_FALSE(agent().possibleAccountCreationForm); | |
341 EXPECT_TRUE(agent().passwordFields.empty()); | |
342 } | |
343 | |
344 // Tests that local heuristics skip forms with no password fields. | |
345 TEST_F(PasswordGenerationAgentTest, | |
346 OnParsedForms_ShouldIgnoreFormsWithNoPasswordFields) { | |
347 // Send only a form with GAIA origin to the agent. | |
348 std::vector<autofill::PasswordForm> forms; | |
349 forms.push_back(GetFormWithNoPasswordFields()); | |
350 [agent() processParsedPasswordForms:forms]; | |
351 | |
352 // No account creation form should have been found. | |
353 EXPECT_FALSE(agent().possibleAccountCreationForm); | |
354 EXPECT_TRUE(agent().passwordFields.empty()); | |
355 } | |
356 | |
357 // Tests that local heuristics extract an account creation form from the page | |
358 // when one exists, along with its password fields. | |
359 TEST_F(PasswordGenerationAgentTest, OnParsedForms) { | |
360 // Send several forms. One should be selected. | |
361 std::vector<autofill::PasswordForm> forms; | |
362 forms.push_back(GetGAIAForm()); | |
363 forms.push_back(GetFormWithNoTextFields()); | |
364 forms.push_back(GetFormWithNoPasswordFields()); | |
365 forms.push_back(GetAccountCreationForm()); | |
366 [agent() processParsedPasswordForms:forms]; | |
367 | |
368 // Should have found an account creation form and extracted its password | |
369 // fields. | |
370 EXPECT_EQ(forms[3], *agent().possibleAccountCreationForm); | |
371 std::vector<autofill::FormFieldData> expectedPasswordFields( | |
372 GetPasswordFields(forms[3])); | |
373 EXPECT_EQ(expectedPasswordFields.size(), agent().passwordFields.size()); | |
374 for (size_t i = 0; i < expectedPasswordFields.size(); ++i) { | |
375 EXPECT_FORM_FIELD_DATA_EQUALS(expectedPasswordFields[i], | |
376 agent().passwordFields[i]); | |
377 } | |
378 } | |
379 | |
380 // Tests that password generation field identification waits until it has | |
381 // approval from autofill and the password manager and an account creation | |
382 // form has been identified with local heuristics.. | |
383 TEST_F(PasswordGenerationAgentTest, DeterminePasswordGenerationField) { | |
384 std::vector<autofill::PasswordForm> forms; | |
385 forms.push_back(GetAccountCreationForm()); | |
386 | |
387 autofill::PasswordForm form(GetAccountCreationForm()); | |
388 std::vector<autofill::FormFieldData> passwordFields(GetPasswordFields(form)); | |
389 | |
390 // The signals can be received in any order, so test them accordingly by | |
391 // breaking the steps into blocks and executing them in different orders. | |
392 id sendForms = ^{ | |
393 std::vector<autofill::PasswordForm> forms; | |
394 forms.push_back(form); | |
395 [agent() processParsedPasswordForms:forms]; | |
396 }; | |
397 id sendPasswordManagerWhitelist = ^{ | |
398 [agent() allowPasswordGenerationForForm:form]; | |
399 }; | |
400 id expectFieldNotFound = ^{ | |
401 EXPECT_FALSE(agent().passwordGenerationField); | |
402 }; | |
403 id expectFieldFound = ^{ | |
404 // When there are multiple password fields in the account creation form, | |
405 // the first one is used as the generation field. | |
406 EXPECT_FORM_FIELD_DATA_EQUALS(passwordFields[0], | |
407 (*agent().passwordGenerationField)); | |
408 }; | |
409 | |
410 // For each permutation of steps, the field should only be set after the third | |
411 // signal is received. | |
412 @autoreleasepool { | |
413 ExecuteBlocks(@[ | |
414 sendForms, expectFieldNotFound, sendPasswordManagerWhitelist, | |
415 expectFieldFound | |
416 ]); | |
417 [agent() clearState]; | |
418 | |
419 ExecuteBlocks(@[ | |
420 sendPasswordManagerWhitelist, expectFieldNotFound, sendForms, | |
421 expectFieldFound | |
422 ]); | |
423 [agent() clearState]; | |
424 } | |
425 } | |
426 | |
427 // Tests that the password generation UI is shown when the user focuses the | |
428 // password field in the account creation form. | |
429 TEST_F(PasswordGenerationAgentTest, | |
430 ShouldStartGenerationWhenPasswordFieldFocused) { | |
431 LoadAccountCreationForm(); | |
432 id mock = [OCMockObject partialMockForObject:accessory_controller()]; | |
433 [[mock expect] showCustomInputAccessoryView:[OCMArg any]]; | |
434 SimulateFormActivity(kAccountCreationFormName, kAccountCreationFieldName, | |
435 @"focus"); | |
436 | |
437 EXPECT_OCMOCK_VERIFY(mock); | |
438 [mock stop]; | |
439 } | |
440 | |
441 // Tests that requesting password generation shows the alert UI. | |
442 TEST_F(PasswordGenerationAgentTest, ShouldShowAlertWhenGenerationRequested) { | |
443 LoadAccountCreationForm(); | |
444 id mock = [OCMockObject partialMockForObject:accessory_controller()]; | |
445 [[mock expect] showCustomInputAccessoryView:[OCMArg any]]; | |
446 SimulateFormActivity(kAccountCreationFormName, kAccountCreationFieldName, | |
447 @"focus"); | |
448 EXPECT_EQ(NO, mock_ui_delegate().UIShown); | |
449 | |
450 [agent() generatePassword]; | |
451 EXPECT_EQ(YES, mock_ui_delegate().UIShown); | |
452 | |
453 EXPECT_OCMOCK_VERIFY(mock); | |
454 [mock stop]; | |
455 } | |
456 | |
457 // Tests that the password generation UI is hidden when the user changes focus | |
458 // from the password field. | |
459 TEST_F(PasswordGenerationAgentTest, | |
460 ShouldStopGenerationWhenDifferentFieldFocused) { | |
461 LoadAccountCreationForm(); | |
462 id mock = [OCMockObject partialMockForObject:accessory_controller()]; | |
463 [[mock expect] showCustomInputAccessoryView:[OCMArg any]]; | |
464 SimulateFormActivity(kAccountCreationFormName, kAccountCreationFieldName, | |
465 @"focus"); | |
466 | |
467 [[mock expect] restoreDefaultInputAccessoryView]; | |
468 SimulateFormActivity(kAccountCreationFormName, kEmailFieldName, @"focus"); | |
469 | |
470 EXPECT_OCMOCK_VERIFY(mock); | |
471 [mock stop]; | |
472 } | |
473 | |
474 // Tests that the password field is filled when the user accepts a generated | |
475 // password. | |
476 TEST_F(PasswordGenerationAgentTest, | |
477 ShouldFillPasswordFieldAndDismissAlertWhenUserAcceptsGeneratedPassword) { | |
478 LoadAccountCreationForm(); | |
479 // Focus the password field to start generation. | |
480 SimulateFormActivity(kAccountCreationFormName, kAccountCreationFieldName, | |
481 @"focus"); | |
482 NSString* password = @"abc"; | |
483 | |
484 [[[mock_js_password_manager() stub] andDo:^(NSInvocation* invocation) { | |
485 void (^completion_handler)(BOOL); | |
486 [invocation getArgument:&completion_handler atIndex:4]; | |
487 completion_handler(YES); | |
488 }] fillPasswordForm:kAccountCreationFormName | |
489 withGeneratedPassword:password | |
490 completionHandler:[OCMArg any]]; | |
491 | |
492 [agent() generatePassword]; | |
493 EXPECT_EQ(YES, mock_ui_delegate().UIShown); | |
494 | |
495 [agent() acceptPasswordGeneration:nil]; | |
496 EXPECT_EQ(NO, mock_ui_delegate().UIShown); | |
497 EXPECT_OCMOCK_VERIFY(mock_js_password_manager()); | |
498 } | |
499 | |
500 // Tests that the Save Passwords setting screen is shown when the user taps | |
501 // "show saved passwords". | |
502 TEST_F(PasswordGenerationAgentTest, | |
503 ShouldShowPasswordsAndDismissAlertWhenUserTapsShow) { | |
504 ScopedWindowSwizzler swizzler; | |
505 LoadAccountCreationForm(); | |
506 // Focus the password field to start generation. | |
507 SimulateFormActivity(kAccountCreationFormName, kAccountCreationFieldName, | |
508 @"focus"); | |
509 [agent() generatePassword]; | |
510 EXPECT_EQ(YES, mock_ui_delegate().UIShown); | |
511 | |
512 [agent() showSavedPasswords:nil]; | |
513 EXPECT_EQ(NO, mock_ui_delegate().UIShown); | |
514 | |
515 GenericChromeCommand* command = base::mac::ObjCCast<GenericChromeCommand>( | |
516 g_chrome_execute_command_sender); | |
517 EXPECT_TRUE(command); | |
518 EXPECT_EQ(IDC_SHOW_SAVE_PASSWORDS_SETTINGS, command.tag); | |
519 } | |
520 | |
521 } // namespace | |
OLD | NEW |