| 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..744518e38854edfdcbc0abbc1266817aafd64b07
|
| --- /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"
|
| +#import "base/mac/scoped_nsobject.h"
|
| +#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
|
|
|