Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(1061)

Side by Side Diff: ios/chrome/browser/passwords/password_generation_agent_unittest.mm

Issue 2152593002: Upstream password manager unit tests (Closed) Base URL: https://chromium.googlesource.com/chromium/src.git@master
Patch Set: Fix GN Created 4 years, 5 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
(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
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698