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

Unified 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 side-by-side diff with in-line comments
Download patch
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

Powered by Google App Engine
This is Rietveld 408576698