Index: ios/chrome/browser/passwords/password_generation_agent_unittest.mm |
diff --git a/ios/chrome/browser/passwords/password_generation_agent_unittest.mm b/ios/chrome/browser/passwords/password_generation_agent_unittest.mm |
new file mode 100644 |
index 0000000000000000000000000000000000000000..a3b5889e9b1581c1afe563eb207ff29f98ef8d16 |
--- /dev/null |
+++ b/ios/chrome/browser/passwords/password_generation_agent_unittest.mm |
@@ -0,0 +1,521 @@ |
+// Copyright 2014 The Chromium Authors. All rights reserved. |
+// Use of this source code is governed by a BSD-style license that can be |
+// found in the LICENSE file. |
+ |
+#import "ios/chrome/browser/passwords/password_generation_agent.h" |
+ |
+#include <algorithm> |
+#include <memory> |
+ |
+#include "base/logging.h" |
+#include "base/mac/foundation_util.h" |
+#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.
|
+#include "base/mac/scoped_objc_class_swizzler.h" |
+#include "base/macros.h" |
+#include "base/strings/string16.h" |
+#include "base/strings/sys_string_conversions.h" |
+#include "base/strings/utf_string_conversions.h" |
+#include "components/autofill/core/common/form_data.h" |
+#include "components/autofill/core/common/form_field_data.h" |
+#include "components/autofill/core/common/password_form.h" |
+#import "components/autofill/ios/browser/js_suggestion_manager.h" |
+#include "google_apis/gaia/gaia_urls.h" |
+#import "ios/chrome/browser/autofill/form_input_accessory_view_controller.h" |
+#import "ios/chrome/browser/passwords/js_password_manager.h" |
+#import "ios/chrome/browser/passwords/password_generation_offer_view.h" |
+#import "ios/chrome/browser/passwords/passwords_ui_delegate.h" |
+#import "ios/chrome/browser/ui/commands/generic_chrome_command.h" |
+#include "ios/chrome/browser/ui/commands/ios_command_ids.h" |
+#import "ios/testing/ocmock_complex_type_helper.h" |
+#include "ios/web/public/test/test_web_state.h" |
+#include "ios/web/public/web_state/url_verification_constants.h" |
+#import "ios/web/public/test/web_test_with_web_state.h" |
+#include "testing/gtest_mac.h" |
+#include "third_party/ocmock/OCMock/OCMock.h" |
+#include "third_party/ocmock/gtest_support.h" |
+#include "url/gurl.h" |
+ |
+namespace { |
+ |
+NSString* const kAccountCreationFormName = @"create-foo-account"; |
+NSString* const kAccountCreationFieldName = @"password"; |
+NSString* const kAccountCreationOrigin = @"http://foo.com/login"; |
+NSString* const kEmailFieldName = @"email"; |
+ |
+// Static storage to access arguments passed to swizzled method of UIWindow. |
+static id g_chrome_execute_command_sender = nil; |
+ |
+} // namespace |
+ |
+@interface MockPasswordsUiDelegate : NSObject<PasswordsUiDelegate> |
+ |
+- (instancetype)init; |
+ |
+@property(nonatomic, readonly) BOOL UIShown; |
+ |
+@end |
+ |
+@implementation MockPasswordsUiDelegate { |
+ // YES if showGenerationAlertWithPassword was called more recently than |
+ // hideGenerationAlert, NO otherwise. |
+ BOOL _UIShown; |
+} |
+ |
+- (instancetype)init { |
+ self = [super init]; |
+ if (self) { |
+ _UIShown = NO; |
+ } |
+ return self; |
+} |
+ |
+@synthesize UIShown = _UIShown; |
+ |
+- (void)showGenerationAlertWithPassword:(NSString*)password |
+ andPromptDelegate: |
+ (id<PasswordGenerationPromptDelegate>)delegate { |
+ _UIShown = YES; |
+} |
+ |
+- (void)hideGenerationAlert { |
+ _UIShown = NO; |
+} |
+ |
+@end |
+ |
+// A donor class that provides a chromeExecuteCommand method that can be |
+// swapped with UIWindow. |
+@interface DonorWindow : NSObject |
+ |
+- (void)chromeExecuteCommand:(id)sender; |
+ |
+@end |
+ |
+@implementation DonorWindow |
+ |
+- (void)chromeExecuteCommand:(id)sender { |
+ g_chrome_execute_command_sender = [sender retain]; |
+} |
+ |
+@end |
+ |
+namespace { |
+ |
+// A helper to swizzle chromeExecuteCommand method on UIWindow. |
+class ScopedWindowSwizzler { |
+ public: |
+ ScopedWindowSwizzler() |
+ : class_swizzler_([UIWindow class], |
+ [DonorWindow class], |
+ @selector(chromeExecuteCommand:)) { |
+ DCHECK(!g_chrome_execute_command_sender); |
+ } |
+ |
+ ~ScopedWindowSwizzler() { |
+ [g_chrome_execute_command_sender release]; |
+ g_chrome_execute_command_sender = nil; |
+ } |
+ |
+ private: |
+ base::mac::ScopedObjCClassSwizzler class_swizzler_; |
+ |
+ DISALLOW_COPY_AND_ASSIGN(ScopedWindowSwizzler); |
+}; |
+ |
+// Returns a form that should be marked as an account creation form by local |
+// heuristics. |
+autofill::PasswordForm GetAccountCreationForm() { |
+ autofill::FormFieldData name_field; |
+ name_field.name = base::ASCIIToUTF16("name"); |
+ name_field.form_control_type = "text"; |
+ |
+ autofill::FormFieldData email_field; |
+ email_field.name = base::ASCIIToUTF16("email"); |
+ email_field.form_control_type = "email"; |
+ |
+ autofill::FormFieldData password_field; |
+ password_field.name = base::SysNSStringToUTF16(kAccountCreationFieldName); |
+ password_field.form_control_type = "password"; |
+ |
+ autofill::FormFieldData confirmPasswordField; |
+ confirmPasswordField.name = base::ASCIIToUTF16("confirm"); |
+ confirmPasswordField.form_control_type = "password"; |
+ |
+ autofill::FormData form; |
+ form.name = base::SysNSStringToUTF16(kAccountCreationFormName); |
+ form.origin = GURL(base::SysNSStringToUTF8(kAccountCreationOrigin)); |
+ form.action = GURL(base::SysNSStringToUTF8(kAccountCreationOrigin)); |
+ |
+ form.fields.push_back(name_field); |
+ form.fields.push_back(email_field); |
+ form.fields.push_back(password_field); |
+ form.fields.push_back(confirmPasswordField); |
+ |
+ autofill::PasswordForm password_form; |
+ password_form.origin = form.origin; |
+ password_form.username_element = email_field.name; |
+ password_form.password_element = password_field.name; |
+ |
+ password_form.form_data = form; |
+ |
+ return password_form; |
+} |
+ |
+// Executes each block in |blocks|, where each block must have the type |
+// void^(void). |
+void ExecuteBlocks(NSArray* blocks) { |
+ for (void (^block)(void) in blocks) |
+ block(); |
+} |
+ |
+// Returns a form that has the same origin as GAIA. |
+autofill::PasswordForm GetGAIAForm() { |
+ autofill::PasswordForm form(GetAccountCreationForm()); |
+ form.origin = GaiaUrls::GetInstance()->gaia_login_form_realm(); |
+ form.signon_realm = form.origin.GetOrigin().spec(); |
+ return form; |
+} |
+ |
+// Returns a form with no text fields. |
+autofill::PasswordForm GetFormWithNoTextFields() { |
+ autofill::PasswordForm form(GetAccountCreationForm()); |
+ form.form_data.fields.clear(); |
+ return form; |
+} |
+ |
+// Returns true if |field| has type "password" and false otherwise. |
+bool IsPasswordField(const autofill::FormFieldData& field) { |
+ return field.form_control_type == "password"; |
+} |
+ |
+// Returns all password fields in |form|. |
+std::vector<autofill::FormFieldData> GetPasswordFields( |
+ const autofill::PasswordForm& form) { |
+ std::vector<autofill::FormFieldData> fields; |
+ fields.reserve(form.form_data.fields.size()); |
+ for (const auto& field : form.form_data.fields) { |
+ if (IsPasswordField(field)) |
+ fields.push_back(field); |
+ } |
+ return fields; |
+} |
+ |
+// Returns a form with no password fields. |
+autofill::PasswordForm GetFormWithNoPasswordFields() { |
+ autofill::PasswordForm form(GetAccountCreationForm()); |
+ form.form_data.fields.erase( |
+ std::remove_if(form.form_data.fields.begin(), form.form_data.fields.end(), |
+ &IsPasswordField), |
+ form.form_data.fields.end()); |
+ return form; |
+} |
+ |
+// Test fixture for testing PasswordGenerationAgent. |
+class PasswordGenerationAgentTest : public web::WebTestWithWebState { |
+ public: |
+ void SetUp() override { |
+ web::WebTestWithWebState::SetUp(); |
+ mock_js_suggestion_manager_.reset( |
+ [[OCMockObject niceMockForClass:[JsSuggestionManager class]] retain]); |
+ mock_js_password_manager_.reset( |
+ [[OCMockObject niceMockForClass:[JsPasswordManager class]] retain]); |
+ mock_ui_delegate_.reset([[MockPasswordsUiDelegate alloc] init]); |
+ test_web_state_.reset(new web::TestWebState); |
+ agent_.reset([[PasswordGenerationAgent alloc] |
+ initWithWebState:test_web_state_.get() |
+ passwordManager:nullptr |
+ passwordManagerDriver:nullptr |
+ JSPasswordManager:mock_js_password_manager_ |
+ JSSuggestionManager:mock_js_suggestion_manager_ |
+ passwordsUiDelegate:mock_ui_delegate_]); |
+ @autoreleasepool { |
+ accessory_view_controller_.reset([[FormInputAccessoryViewController alloc] |
+ initWithWebState:test_web_state_.get() |
+ providers:@[ agent_ ]]); |
+ } |
+ } |
+ |
+ // Sends form data, autofill data, and password manager data to the |
+ // generation agent so that it can find an account creation form and password |
+ // field. |
+ void LoadAccountCreationForm() { |
+ autofill::PasswordForm password_form(GetAccountCreationForm()); |
+ [agent() allowPasswordGenerationForForm:password_form]; |
+ std::vector<autofill::PasswordForm> password_forms; |
+ password_forms.push_back(password_form); |
+ [agent() processParsedPasswordForms:password_forms]; |
+ SetCurrentURLAndTrustLevel( |
+ GURL(base::SysNSStringToUTF8(kAccountCreationOrigin)), |
+ web::URLVerificationTrustLevel::kAbsolute); |
+ SetContentIsHTML(YES); |
+ } |
+ |
+ // Sets up the web controller mock to use the specified URL and trust level. |
+ void SetCurrentURLAndTrustLevel( |
+ GURL url, |
+ web::URLVerificationTrustLevel url_trust_level) { |
+ test_web_state_->SetCurrentURL(url); |
+ test_web_state_->SetTrustLevel(url_trust_level); |
+ } |
+ |
+ // Swizzles the current web controller to set whether the content is HTML. |
+ void SetContentIsHTML(BOOL content_is_html) { |
+ test_web_state_->SetContentIsHTML(content_is_html); |
+ } |
+ |
+ // Simulates an event on the specified form/field. |
+ void SimulateFormActivity(NSString* form_name, |
+ NSString* field_name, |
+ NSString* type) { |
+ [accessory_view_controller_ webState:test_web_state_.get() |
+ didRegisterFormActivityWithFormNamed:base::SysNSStringToUTF8(form_name) |
+ fieldName:base::SysNSStringToUTF8(field_name) |
+ type:base::SysNSStringToUTF8(type) |
+ value:"" |
+ keyCode:web::WebStateObserver:: |
+ kInvalidFormKeyCode |
+ inputMissing:false]; |
+ } |
+ |
+ // Returns a mock of JsSuggestionManager. |
+ id mock_js_suggestion_manager() { return mock_js_suggestion_manager_; } |
+ |
+ // Returns a mock of JsPasswordManager. |
+ id mock_js_password_manager() { return mock_js_password_manager_; } |
+ |
+ MockPasswordsUiDelegate* mock_ui_delegate() { return mock_ui_delegate_; } |
+ |
+ protected: |
+ // Returns the current generation agent. |
+ PasswordGenerationAgent* agent() { return agent_.get(); } |
+ |
+ // Returns the current accessory view controller. |
+ FormInputAccessoryViewController* accessory_controller() { |
+ return accessory_view_controller_.get(); |
+ } |
+ |
+ private: |
+ // Test WebState. |
+ std::unique_ptr<web::TestWebState> test_web_state_; |
+ |
+ // Mock for JsSuggestionManager; |
+ base::scoped_nsobject<id> mock_js_suggestion_manager_; |
+ |
+ // Mock for JsPasswordManager. |
+ base::scoped_nsobject<id> mock_js_password_manager_; |
+ |
+ // Mock for the UI delegate. |
+ base::scoped_nsobject<MockPasswordsUiDelegate> mock_ui_delegate_; |
+ |
+ // Controller that shows custom input accessory views. |
+ base::scoped_nsobject<FormInputAccessoryViewController> |
+ accessory_view_controller_; |
+ |
+ // The current generation agent. |
+ base::scoped_nsobject<PasswordGenerationAgent> agent_; |
+}; |
+ |
+// Tests that local heuristics skip forms with GAIA realm. |
+TEST_F(PasswordGenerationAgentTest, |
+ OnParsedForms_ShouldIgnoreFormsWithGaiaRealm) { |
+ // Send only a form with GAIA origin to the agent. |
+ std::vector<autofill::PasswordForm> forms; |
+ forms.push_back(GetGAIAForm()); |
+ [agent() processParsedPasswordForms:forms]; |
+ |
+ // No account creation form should have been found. |
+ EXPECT_FALSE(agent().possibleAccountCreationForm); |
+ EXPECT_TRUE(agent().passwordFields.empty()); |
+} |
+ |
+// Tests that local heuristics skip forms with no text fields. |
+TEST_F(PasswordGenerationAgentTest, |
+ OnParsedForms_ShouldIgnoreFormsWithNotEnoughTextFields) { |
+ // Send only a form with GAIA origin to the agent. |
+ std::vector<autofill::PasswordForm> forms; |
+ forms.push_back(GetFormWithNoTextFields()); |
+ [agent() processParsedPasswordForms:forms]; |
+ |
+ // No account creation form should have been found. |
+ EXPECT_FALSE(agent().possibleAccountCreationForm); |
+ EXPECT_TRUE(agent().passwordFields.empty()); |
+} |
+ |
+// Tests that local heuristics skip forms with no password fields. |
+TEST_F(PasswordGenerationAgentTest, |
+ OnParsedForms_ShouldIgnoreFormsWithNoPasswordFields) { |
+ // Send only a form with GAIA origin to the agent. |
+ std::vector<autofill::PasswordForm> forms; |
+ forms.push_back(GetFormWithNoPasswordFields()); |
+ [agent() processParsedPasswordForms:forms]; |
+ |
+ // No account creation form should have been found. |
+ EXPECT_FALSE(agent().possibleAccountCreationForm); |
+ EXPECT_TRUE(agent().passwordFields.empty()); |
+} |
+ |
+// Tests that local heuristics extract an account creation form from the page |
+// when one exists, along with its password fields. |
+TEST_F(PasswordGenerationAgentTest, OnParsedForms) { |
+ // Send several forms. One should be selected. |
+ std::vector<autofill::PasswordForm> forms; |
+ forms.push_back(GetGAIAForm()); |
+ forms.push_back(GetFormWithNoTextFields()); |
+ forms.push_back(GetFormWithNoPasswordFields()); |
+ forms.push_back(GetAccountCreationForm()); |
+ [agent() processParsedPasswordForms:forms]; |
+ |
+ // Should have found an account creation form and extracted its password |
+ // fields. |
+ EXPECT_EQ(forms[3], *agent().possibleAccountCreationForm); |
+ std::vector<autofill::FormFieldData> expectedPasswordFields( |
+ GetPasswordFields(forms[3])); |
+ EXPECT_EQ(expectedPasswordFields.size(), agent().passwordFields.size()); |
+ for (size_t i = 0; i < expectedPasswordFields.size(); ++i) { |
+ EXPECT_FORM_FIELD_DATA_EQUALS(expectedPasswordFields[i], |
+ agent().passwordFields[i]); |
+ } |
+} |
+ |
+// Tests that password generation field identification waits until it has |
+// approval from autofill and the password manager and an account creation |
+// form has been identified with local heuristics.. |
+TEST_F(PasswordGenerationAgentTest, DeterminePasswordGenerationField) { |
+ std::vector<autofill::PasswordForm> forms; |
+ forms.push_back(GetAccountCreationForm()); |
+ |
+ autofill::PasswordForm form(GetAccountCreationForm()); |
+ std::vector<autofill::FormFieldData> passwordFields(GetPasswordFields(form)); |
+ |
+ // The signals can be received in any order, so test them accordingly by |
+ // breaking the steps into blocks and executing them in different orders. |
+ id sendForms = ^{ |
+ std::vector<autofill::PasswordForm> forms; |
+ forms.push_back(form); |
+ [agent() processParsedPasswordForms:forms]; |
+ }; |
+ id sendPasswordManagerWhitelist = ^{ |
+ [agent() allowPasswordGenerationForForm:form]; |
+ }; |
+ id expectFieldNotFound = ^{ |
+ EXPECT_FALSE(agent().passwordGenerationField); |
+ }; |
+ id expectFieldFound = ^{ |
+ // When there are multiple password fields in the account creation form, |
+ // the first one is used as the generation field. |
+ EXPECT_FORM_FIELD_DATA_EQUALS(passwordFields[0], |
+ (*agent().passwordGenerationField)); |
+ }; |
+ |
+ // For each permutation of steps, the field should only be set after the third |
+ // signal is received. |
+ @autoreleasepool { |
+ ExecuteBlocks(@[ |
+ sendForms, expectFieldNotFound, sendPasswordManagerWhitelist, |
+ expectFieldFound |
+ ]); |
+ [agent() clearState]; |
+ |
+ ExecuteBlocks(@[ |
+ sendPasswordManagerWhitelist, expectFieldNotFound, sendForms, |
+ expectFieldFound |
+ ]); |
+ [agent() clearState]; |
+ } |
+} |
+ |
+// Tests that the password generation UI is shown when the user focuses the |
+// password field in the account creation form. |
+TEST_F(PasswordGenerationAgentTest, |
+ ShouldStartGenerationWhenPasswordFieldFocused) { |
+ LoadAccountCreationForm(); |
+ id mock = [OCMockObject partialMockForObject:accessory_controller()]; |
+ [[mock expect] showCustomInputAccessoryView:[OCMArg any]]; |
+ SimulateFormActivity(kAccountCreationFormName, kAccountCreationFieldName, |
+ @"focus"); |
+ |
+ EXPECT_OCMOCK_VERIFY(mock); |
+ [mock stop]; |
+} |
+ |
+// Tests that requesting password generation shows the alert UI. |
+TEST_F(PasswordGenerationAgentTest, ShouldShowAlertWhenGenerationRequested) { |
+ LoadAccountCreationForm(); |
+ id mock = [OCMockObject partialMockForObject:accessory_controller()]; |
+ [[mock expect] showCustomInputAccessoryView:[OCMArg any]]; |
+ SimulateFormActivity(kAccountCreationFormName, kAccountCreationFieldName, |
+ @"focus"); |
+ EXPECT_EQ(NO, mock_ui_delegate().UIShown); |
+ |
+ [agent() generatePassword]; |
+ EXPECT_EQ(YES, mock_ui_delegate().UIShown); |
+ |
+ EXPECT_OCMOCK_VERIFY(mock); |
+ [mock stop]; |
+} |
+ |
+// Tests that the password generation UI is hidden when the user changes focus |
+// from the password field. |
+TEST_F(PasswordGenerationAgentTest, |
+ ShouldStopGenerationWhenDifferentFieldFocused) { |
+ LoadAccountCreationForm(); |
+ id mock = [OCMockObject partialMockForObject:accessory_controller()]; |
+ [[mock expect] showCustomInputAccessoryView:[OCMArg any]]; |
+ SimulateFormActivity(kAccountCreationFormName, kAccountCreationFieldName, |
+ @"focus"); |
+ |
+ [[mock expect] restoreDefaultInputAccessoryView]; |
+ SimulateFormActivity(kAccountCreationFormName, kEmailFieldName, @"focus"); |
+ |
+ EXPECT_OCMOCK_VERIFY(mock); |
+ [mock stop]; |
+} |
+ |
+// Tests that the password field is filled when the user accepts a generated |
+// password. |
+TEST_F(PasswordGenerationAgentTest, |
+ ShouldFillPasswordFieldAndDismissAlertWhenUserAcceptsGeneratedPassword) { |
+ LoadAccountCreationForm(); |
+ // Focus the password field to start generation. |
+ SimulateFormActivity(kAccountCreationFormName, kAccountCreationFieldName, |
+ @"focus"); |
+ NSString* password = @"abc"; |
+ |
+ [[[mock_js_password_manager() stub] andDo:^(NSInvocation* invocation) { |
+ void (^completion_handler)(BOOL); |
+ [invocation getArgument:&completion_handler atIndex:4]; |
+ completion_handler(YES); |
+ }] fillPasswordForm:kAccountCreationFormName |
+ withGeneratedPassword:password |
+ completionHandler:[OCMArg any]]; |
+ |
+ [agent() generatePassword]; |
+ EXPECT_EQ(YES, mock_ui_delegate().UIShown); |
+ |
+ [agent() acceptPasswordGeneration:nil]; |
+ EXPECT_EQ(NO, mock_ui_delegate().UIShown); |
+ EXPECT_OCMOCK_VERIFY(mock_js_password_manager()); |
+} |
+ |
+// Tests that the Save Passwords setting screen is shown when the user taps |
+// "show saved passwords". |
+TEST_F(PasswordGenerationAgentTest, |
+ ShouldShowPasswordsAndDismissAlertWhenUserTapsShow) { |
+ ScopedWindowSwizzler swizzler; |
+ LoadAccountCreationForm(); |
+ // Focus the password field to start generation. |
+ SimulateFormActivity(kAccountCreationFormName, kAccountCreationFieldName, |
+ @"focus"); |
+ [agent() generatePassword]; |
+ EXPECT_EQ(YES, mock_ui_delegate().UIShown); |
+ |
+ [agent() showSavedPasswords:nil]; |
+ EXPECT_EQ(NO, mock_ui_delegate().UIShown); |
+ |
+ GenericChromeCommand* command = base::mac::ObjCCast<GenericChromeCommand>( |
+ g_chrome_execute_command_sender); |
+ EXPECT_TRUE(command); |
+ EXPECT_EQ(IDC_SHOW_SAVE_PASSWORDS_SETTINGS, command.tag); |
+} |
+ |
+} // namespace |