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

Unified Diff: ios/chrome/browser/passwords/password_controller_unittest.mm

Issue 2152593002: Upstream password manager unit tests (Closed) Base URL: https://chromium.googlesource.com/chromium/src.git@master
Patch Set: Fix GYP 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_controller_unittest.mm
diff --git a/ios/chrome/browser/passwords/password_controller_unittest.mm b/ios/chrome/browser/passwords/password_controller_unittest.mm
index 7757fe291c68abe8b21a1ed81c1a441ba1326487..56148f246f27d08b8ebd663342718484ad82c711 100644
--- a/ios/chrome/browser/passwords/password_controller_unittest.mm
+++ b/ios/chrome/browser/passwords/password_controller_unittest.mm
@@ -1,28 +1,50 @@
-// Copyright 2016 The Chromium Authors. All rights reserved.
+// Copyright 2012 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_controller.h"
+#import <Foundation/Foundation.h>
+
#include <memory>
-#include <vector>
+#include <utility>
-#include "base/mac/scoped_nsobject.h"
+#include "base/json/json_reader.h"
+#include "base/mac/bind_objc_block.h"
+#import "base/mac/scoped_nsobject.h"
#include "base/memory/ptr_util.h"
+#include "base/memory/ref_counted.h"
+#include "base/strings/sys_string_conversions.h"
+#include "base/strings/utf_string_conversions.h"
+#import "base/test/ios/wait_util.h"
+#include "base/values.h"
+#include "components/autofill/core/common/password_form_fill_data.h"
#include "components/password_manager/core/browser/log_manager.h"
-#include "components/password_manager/core/browser/password_form_manager.h"
+#include "components/password_manager/core/browser/mock_password_store.h"
#include "components/password_manager/core/browser/stub_password_manager_client.h"
#include "components/password_manager/core/common/password_manager_pref_names.h"
-#include "components/syncable_prefs/testing_pref_service_syncable.h"
+#include "components/prefs/pref_registry_simple.h"
+#include "components/prefs/testing_pref_service.h"
+#import "ios/chrome/browser/autofill/form_input_accessory_view_controller.h"
+#import "ios/chrome/browser/autofill/form_suggestion_controller.h"
#include "ios/chrome/browser/browser_state/test_chrome_browser_state.h"
+#import "ios/chrome/browser/passwords/js_password_manager.h"
+#import "ios/web/public/web_state/web_state.h"
+#import "ios/web/public/test/web_test_with_web_state.h"
#import "ios/web/public/test/test_web_state.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
+#include "testing/gtest_mac.h"
+#import "third_party/ocmock/OCMock/OCMock.h"
+#import "third_party/ocmock/OCMock/OCPartialMockObject.h"
#include "url/gurl.h"
-using testing::_;
+using autofill::PasswordForm;
+using autofill::PasswordFormFillData;
using testing::Return;
+namespace {
+
class MockWebState : public web::TestWebState {
public:
MOCK_CONST_METHOD0(GetBrowserState, web::BrowserState*(void));
@@ -31,21 +53,22 @@ class MockWebState : public web::TestWebState {
class MockPasswordManagerClient
: public password_manager::StubPasswordManagerClient {
public:
- // |form_manager| stays owned by the mock.
- MOCK_METHOD3(PromptUserToSaveOrUpdatePasswordPtr,
- void(password_manager::PasswordFormManager* form_manager,
- password_manager::CredentialSourceType type,
- bool update_password));
+ explicit MockPasswordManagerClient(password_manager::PasswordStore* store)
+ : store_(store) {}
+
+ ~MockPasswordManagerClient() override = default;
+
MOCK_CONST_METHOD0(GetLogManager, password_manager::LogManager*(void));
- // Workaround for std::unique_ptr<> lacking a copy constructor.
- bool PromptUserToSaveOrUpdatePassword(
- std::unique_ptr<password_manager::PasswordFormManager> manager,
- password_manager::CredentialSourceType type,
- bool update_password) override {
- PromptUserToSaveOrUpdatePasswordPtr(manager.get(), type, update_password);
- return false;
+ PrefService* GetPrefs() override { return &prefs_; }
+
+ password_manager::PasswordStore* GetPasswordStore() const override {
+ return store_;
}
+
+ private:
+ TestingPrefServiceSimple prefs_;
+ password_manager::PasswordStore* const store_;
};
class MockLogManager : public password_manager::LogManager {
@@ -58,24 +81,1202 @@ class MockLogManager : public password_manager::LogManager {
void SetSuspended(bool suspended) override {}
};
-TEST(PasswordControllerTest, SaveOnNonHTMLLandingPage) {
- // Create the PasswordController with a MockPasswordManagerClient.
+// Creates PasswordController with the given |web_state| and a mock client
+// using the given |store|. If not null, |weak_client| is filled with a
+// non-owning pointer to the created client. The created controller is
+// returned.
+base::scoped_nsobject<PasswordController> CreatePasswordController(
+ web::WebState* web_state,
+ password_manager::PasswordStore* store,
+ MockPasswordManagerClient** weak_client) {
+ auto client = base::WrapUnique(new MockPasswordManagerClient(store));
+ if (weak_client)
+ *weak_client = client.get();
+ return base::scoped_nsobject<PasswordController>([[PasswordController alloc]
+ initWithWebState:web_state
+ passwordsUiDelegate:nil
+ client:std::move(client)]);
+}
+
+} // namespace
+
+@interface PasswordController (
+ Testing)<CRWWebStateObserver, FormSuggestionProvider>
+
+- (void)findPasswordFormsWithCompletionHandler:
+ (void (^)(const std::vector<PasswordForm>&))completionHandler;
+
+- (void)extractSubmittedPasswordForm:(const std::string&)formName
+ completionHandler:
+ (void (^)(BOOL found,
+ const PasswordForm& form))completionHandler;
+
+- (void)fillPasswordForm:(const PasswordFormFillData&)formData
+ completionHandler:(void (^)(BOOL))completionHandler;
+
+- (BOOL)getPasswordForm:(PasswordForm*)form
+ fromDictionary:(const base::DictionaryValue*)dictionary
+ pageURL:(const GURL&)pageLocation;
+
+// Provides access to JavaScript Manager for testing with mocks.
+@property(readonly) JsPasswordManager* passwordJsManager;
+
+@end
+
+// Real FormSuggestionController is wrapped to register the addition of
+// suggestions.
+@interface PasswordsTestSuggestionController : FormSuggestionController
+
+@property(nonatomic, copy) NSArray* suggestions;
+
+- (void)dealloc;
+
+@end
+
+@implementation PasswordsTestSuggestionController
+
+@synthesize suggestions = _suggestions;
+
+- (void)updateKeyboardWithSuggestions:(NSArray*)suggestions {
+ self.suggestions = suggestions;
+}
+
+- (void)dealloc {
+ [_suggestions release];
+ [super dealloc];
+}
+
+@end
+
+class PasswordControllerTest : public web::WebTestWithWebState {
+ public:
+ PasswordControllerTest()
+ : store_(new testing::NiceMock<password_manager::MockPasswordStore>()) {}
+
+ ~PasswordControllerTest() override { store_->ShutdownOnUIThread(); }
+
+ void SetUp() override {
+ web::WebTestWithWebState::SetUp();
+ passwordController_ =
+ CreatePasswordController(web_state(), store_.get(), nullptr);
+ @autoreleasepool {
+ // Make sure the temporary array is released after SetUp finishes,
+ // otherwise [passwordController_ suggestionProvider] will be retained
+ // until PlatformTest teardown, at which point all Chrome objects are
+ // already gone and teardown may access invalid memory.
+ suggestionController_.reset([[PasswordsTestSuggestionController alloc]
+ initWithWebState:web_state()
+ providers:@[ [passwordController_ suggestionProvider] ]]);
+ accessoryViewController_.reset([[FormInputAccessoryViewController alloc]
+ initWithWebState:web_state()
+ providers:@[ [suggestionController_ accessoryViewProvider] ]]);
+ }
+ }
+
+ protected:
+ // Helper method for PasswordControllerTest.DontFillReadonly. Tries to load
+ // |html| and find and fill there a form with hard-coded form data. Returns
+ // YES on success, NO otherwise.
+ BOOL BasicFormFill(NSString* html);
+
+ // Retrieve the current suggestions from suggestionController_ sorted in
+ // alphabetical order according to their value properties.
+ NSArray* GetSortedSuggestionValues() {
+ NSMutableArray* suggestion_values = [NSMutableArray array];
+ for (FormSuggestion* suggestion in [suggestionController_ suggestions])
+ [suggestion_values addObject:suggestion.value];
+ return [suggestion_values
+ sortedArrayUsingSelector:@selector(localizedCaseInsensitiveCompare:)];
+ }
+
+ // Returns an identifier for the |form_number|th form in the page.
+ std::string FormName(int form_number) {
+ NSString* kFormNamingScript =
+ @"__gCrWeb.common.getFormIdentifier("
+ " document.querySelectorAll('form')[%d]);";
+ return base::SysNSStringToUTF8(EvaluateJavaScriptAsString(
+ [NSString stringWithFormat:kFormNamingScript, form_number]));
+ }
+
+ // Sets up a partial mock that intercepts calls to the selector
+ // -fillPasswordForm:withUsername:password:completionHandler: to the
+ // PasswordController's JavaScript manager. For the first
+ // |target_failure_count| calls, skips the invocation of the real JavaScript
+ // manager, giving the effect that password form fill failed. As soon as
+ // |failure_count| reaches |target_failure_count|, stop the partial mock
+ // and let the original JavaScript manager execute.
+ void SetFillPasswordFormFailureCount(int target_failure_count) {
+ id original_manager = [passwordController_ passwordJsManager];
+ OCPartialMockObject* failing_manager =
+ [OCMockObject partialMockForObject:original_manager];
+ __block int failure_count = 0;
+ void (^fail_invocation)(NSInvocation*) = ^(NSInvocation* invocation) {
+ if (failure_count >= target_failure_count) {
+ [failing_manager stop];
+ [invocation invokeWithTarget:original_manager];
+ } else {
+ ++failure_count;
+ // Fetches the completion handler from |invocation| and calls it with
+ // failure status.
+ void (^completionHandler)(BOOL);
+ const NSInteger kArgOffset = 1;
+ const NSInteger kCompletionHandlerArgIndex = 4;
+ [invocation getArgument:&completionHandler
+ atIndex:(kCompletionHandlerArgIndex + kArgOffset)];
+ ASSERT_TRUE(completionHandler);
+ completionHandler(NO);
+ }
+ };
+ [[[failing_manager stub] andDo:fail_invocation]
+ fillPasswordForm:[OCMArg any]
+ withUsername:[OCMArg any]
+ password:[OCMArg any]
+ completionHandler:[OCMArg any]];
+ }
+
+ // SuggestionController for testing.
+ base::scoped_nsobject<PasswordsTestSuggestionController>
+ suggestionController_;
+
+ // FormInputAccessoryViewController for testing.
+ base::scoped_nsobject<FormInputAccessoryViewController>
+ accessoryViewController_;
+
+ // PasswordController for testing.
+ base::scoped_nsobject<PasswordController> passwordController_;
+
+ scoped_refptr<password_manager::PasswordStore> store_;
+};
+
+struct PasswordFormTestData {
+ const char* const page_location;
+ const char* const json_string;
+ const char* const expected_origin;
+ const char* const expected_action;
+ const char* const expected_username_element;
+ const char* const expected_username_value;
+ const char* const expected_new_password_element;
+ const char* const expected_new_password_value;
+ const char* const expected_old_password_element;
+ const char* const expected_old_password_value;
+};
+
+// Check that given a serialization of a PasswordForm, the controller is able
+// to create the corresponding PasswordForm object.
+TEST_F(PasswordControllerTest, PopulatePasswordFormWithDictionary) {
+ // clang-format off
+ PasswordFormTestData test_data[] = {
+ // One username element, one password element. URLs contain extra
+ // parts: username/password, query, reference, which are all expected
+ // to be stripped off. The password is recognized as an old password.
+ {
+ "http://john:doe@fakedomain.com/foo/bar?baz=quz#foobar",
+ "{ \"action\": \"some/action?to=be&or=not#tobe\","
+ "\"usernameElement\": \"account\","
+ "\"usernameValue\": \"fakeaccount\","
+ "\"name\": \"signup\","
+ "\"origin\": \"http://john:doe@fakedomain.com/foo/bar\","
+ "\"passwords\": ["
+ "{ \"element\": \"secret\"," "\"value\": \"fakesecret\" },"
+ "]}",
+ "http://fakedomain.com/foo/bar",
+ "http://fakedomain.com/foo/some/action",
+ "account",
+ "fakeaccount",
+ "",
+ "",
+ "secret",
+ "fakesecret",
+ },
+ // One username element, one password element. Population should fail
+ // due to an origin mismatch.
+ {
+ "http://john:doe@fakedomain.com/foo/bar?baz=quz#foobar",
+ "{ \"action\": \"some/action?to=be&or=not#tobe\","
+ "\"usernameElement\": \"account\","
+ "\"usernameValue\": \"fakeaccount\","
+ "\"name\": \"signup\","
+ "\"origin\": \"http://john:doe@realdomainipromise.com/foo/bar\","
+ "\"passwords\": ["
+ "{ \"element\": \"secret\"," "\"value\": \"fakesecret\" },"
+ "]}",
+ "",
+ "",
+ "",
+ "",
+ "",
+ "",
+ "",
+ "",
+ },
+ // One username element, two password elements. Since both password
+ // values are the same, we are assuming that the webpage asked the user
+ // to enter the password twice for confirmation.
+ {
+ "http://fakedomain.com/foo",
+ "{ \"action\": \"http://anotherdomain.com/some_action\","
+ "\"usernameElement\": \"account\","
+ "\"usernameValue\": \"fakeaccount\","
+ "\"name\": \"signup\","
+ "\"origin\": \"http://fakedomain.com/foo\","
+ "\"passwords\": ["
+ "{ \"element\": \"secret\"," "\"value\": \"fakesecret\" },"
+ "{ \"element\": \"confirm\"," "\"value\": \"fakesecret\" },"
+ "]}",
+ "http://fakedomain.com/foo",
+ "http://anotherdomain.com/some_action",
+ "account",
+ "fakeaccount",
+ "secret",
+ "fakesecret",
+ "",
+ "",
+ },
+ // One username element, two password elements. The password
+ // values are different, so we are assuming that the webpage asked the user
+ // to enter the old password and new password.
+ {
+ "http://fakedomain.com/foo",
+ "{ \"action\": \"\","
+ "\"usernameElement\": \"account\","
+ "\"usernameValue\": \"fakeaccount\","
+ "\"name\": \"signup\","
+ "\"origin\": \"http://fakedomain.com/foo\","
+ "\"passwords\": ["
+ "{ \"element\": \"old\"," "\"value\": \"oldsecret\" },"
+ "{ \"element\": \"new\"," "\"value\": \"newsecret\" },"
+ "]}",
+ "http://fakedomain.com/foo",
+ "http://fakedomain.com/foo",
+ "account",
+ "fakeaccount",
+ "new",
+ "newsecret",
+ "old",
+ "oldsecret",
+ },
+ // One username element, three password elements. All passwords
+ // are the same. Password population should fail because this configuration
+ // does not make sense.
+ {
+ "http://fakedomain.com",
+ "{ \"action\": \"\","
+ "\"usernameElement\": \"account\","
+ "\"usernameValue\": \"fakeaccount\","
+ "\"name\": \"signup\","
+ "\"origin\": \"http://fakedomain.com/foo\","
+ "\"passwords\": ["
+ "{ \"element\": \"pass1\"," "\"value\": \"word\" },"
+ "{ \"element\": \"pass2\"," "\"value\": \"word\" },"
+ "{ \"element\": \"pass3\"," "\"value\": \"word\" },"
+ "]}",
+ "http://fakedomain.com/",
+ "http://fakedomain.com/",
+ "account",
+ "fakeaccount",
+ "",
+ "",
+ "",
+ "",
+ },
+ // One username element, three password elements. Two passwords are
+ // the same followed by a different one. Assuming that the duplicated
+ // password is the old one.
+ {
+ "http://fakedomain.com",
+ "{ \"action\": \"\","
+ "\"usernameElement\": \"account\","
+ "\"usernameValue\": \"fakeaccount\","
+ "\"name\": \"signup\","
+ "\"origin\": \"http://fakedomain.com/foo\","
+ "\"passwords\": ["
+ "{ \"element\": \"pass1\"," "\"value\": \"word1\" },"
+ "{ \"element\": \"pass2\"," "\"value\": \"word1\" },"
+ "{ \"element\": \"pass3\"," "\"value\": \"word3\" },"
+ "]}",
+ "http://fakedomain.com/",
+ "http://fakedomain.com/",
+ "account",
+ "fakeaccount",
+ "pass3",
+ "word3",
+ "pass1",
+ "word1",
+ },
+ // One username element, three password elements. A password is
+ // follwed by two duplicate ones. Assuming that the duplicated
+ // password is the new one.
+ {
+ "http://fakedomain.com",
+ "{ \"action\": \"\","
+ "\"usernameElement\": \"account\","
+ "\"usernameValue\": \"fakeaccount\","
+ "\"name\": \"signup\","
+ "\"origin\": \"http://fakedomain.com/foo\","
+ "\"passwords\": ["
+ "{ \"element\": \"pass1\"," "\"value\": \"word1\" },"
+ "{ \"element\": \"pass2\"," "\"value\": \"word2\" },"
+ "{ \"element\": \"pass3\"," "\"value\": \"word2\" },"
+ "]}",
+ "http://fakedomain.com/",
+ "http://fakedomain.com/",
+ "account",
+ "fakeaccount",
+ "pass2",
+ "word2",
+ "pass1",
+ "word1",
+ },
+ };
+ // clang-format on
+
+ for (const PasswordFormTestData& data : test_data) {
+ SCOPED_TRACE(testing::Message()
+ << "for page_location=" << data.page_location
+ << " and json_string=" << data.json_string);
+ std::unique_ptr<base::Value> json_data(
+ base::JSONReader::Read(data.json_string, true));
+ const base::DictionaryValue* json_dict = nullptr;
+ ASSERT_TRUE(json_data->GetAsDictionary(&json_dict));
+ PasswordForm form;
+ [passwordController_ getPasswordForm:&form
+ fromDictionary:json_dict
+ pageURL:GURL(data.page_location)];
+ EXPECT_STREQ(data.expected_origin, form.origin.spec().c_str());
+ EXPECT_STREQ(data.expected_action, form.action.spec().c_str());
+ EXPECT_EQ(base::ASCIIToUTF16(data.expected_username_element),
+ form.username_element);
+ EXPECT_EQ(base::ASCIIToUTF16(data.expected_username_value),
+ form.username_value);
+ EXPECT_EQ(base::ASCIIToUTF16(data.expected_new_password_element),
+ form.new_password_element);
+ EXPECT_EQ(base::ASCIIToUTF16(data.expected_new_password_value),
+ form.new_password_value);
+ EXPECT_EQ(base::ASCIIToUTF16(data.expected_old_password_element),
+ form.password_element);
+ EXPECT_EQ(base::ASCIIToUTF16(data.expected_old_password_value),
+ form.password_value);
+ }
+};
+
+struct FindPasswordFormTestData {
+ NSString* html_string;
+ const bool expected_form_found;
+ const char* const expected_username_element;
+ const char* const expected_password_element;
+};
+
+// TODO(crbug.com/403705) This test is flaky.
+// Check that HTML forms are converted correctly into PasswordForms.
+TEST_F(PasswordControllerTest, FLAKY_FindPasswordFormsInView) {
+ // clang-format off
+ FindPasswordFormTestData test_data[] = {
+ // Normal form: a username and a password element.
+ {
+ @"<form>"
+ "<input type='text' name='user0'>"
+ "<input type='password' name='pass0'>"
+ "</form>",
+ true, "user0", "pass0"
+ },
+ // User name is captured as an email address (HTML5).
+ {
+ @"<form>"
+ "<input type='email' name='email1'>"
+ "<input type='password' name='pass1'>"
+ "</form>",
+ true, "email1", "pass1"
+ },
+ // No username element.
+ {
+ @"<form>"
+ "<input type='password' name='user2'>"
+ "<input type='password' name='pass2'>"
+ "</form>",
+ true, "", "user2"
+ },
+ // No username element before password.
+ {
+ @"<form>"
+ "<input type='password' name='pass3'>"
+ "<input type='text' name='user3'>"
+ "</form>",
+ true, "", "pass3"
+ },
+ // Disabled username element.
+ {
+ @"<form>"
+ "<input type='text' name='user4' disabled='disabled'>"
+ "<input type='password' name='pass4'>"
+ "</form>",
+ true, "", "pass4"
+ },
+ // Username element has autocomplete='off'.
+ {
+ @"<form>"
+ "<input type='text' name='user5' AUTOCOMPLETE='off'>"
+ "<input type='password' name='pass5'>"
+ "</form>",
+ true, "user5", "pass5"
+ },
+ // No password element.
+ {
+ @"<form>"
+ "<input type='text' name='user6'>"
+ "<input type='text' name='pass6'>"
+ "</form>",
+ false, nullptr, nullptr
+ },
+ // Disabled password element.
+ {
+ @"<form>"
+ "<input type='text' name='user7'>"
+ "<input type='password' name='pass7' disabled='disabled'>"
+ "</form>",
+ false, nullptr, nullptr
+ },
+ // Password element has autocomplete='off'.
+ {
+ @"<form>"
+ "<input type='text' name='user8'>"
+ "<input type='password' name='pass8' AUTOCOMPLETE='OFF'>"
+ "</form>",
+ true, "user8", "pass8"
+ },
+ // Form element has autocomplete='off'.
+ {
+ @"<form autocomplete='off'>"
+ "<input type='text' name='user9'>"
+ "<input type='password' name='pass9'>"
+ "</form>",
+ true, "user9", "pass9"
+ },
+ };
+ // clang-format on
+
+ for (const FindPasswordFormTestData& data : test_data) {
+ SCOPED_TRACE(testing::Message() << "for html_string=" << data.html_string);
+ LoadHtml(data.html_string);
+ __block std::vector<PasswordForm> forms;
+ __block BOOL block_was_called = NO;
+ [passwordController_ findPasswordFormsWithCompletionHandler:^(
+ const std::vector<PasswordForm>& result) {
+ block_was_called = YES;
+ forms = result;
+ }];
+ base::test::ios::WaitUntilCondition(^bool() {
+ return block_was_called;
+ });
+ if (data.expected_form_found) {
+ ASSERT_EQ(1U, forms.size());
+ EXPECT_EQ(base::ASCIIToUTF16(data.expected_username_element),
+ forms[0].username_element);
+ EXPECT_EQ(base::ASCIIToUTF16(data.expected_password_element),
+ forms[0].password_element);
+ } else {
+ ASSERT_TRUE(forms.empty());
+ }
+ }
+}
+
+struct GetSubmittedPasswordFormTestData {
+ NSString* html_string;
+ NSString* java_script;
+ const int number_of_forms_to_submit;
+ const bool expected_form_found;
+ const char* expected_username_element;
+};
+
+// TODO(crbug.com/403705) This test is flaky.
+// Check that HTML forms are captured and converted correctly into
+// PasswordForms on submission.
+TEST_F(PasswordControllerTest, FLAKY_GetSubmittedPasswordForm) {
+ // clang-format off
+ GetSubmittedPasswordFormTestData test_data[] = {
+ // Two forms with no explicit names.
+ {
+ @"<form action='javascript:;'>"
+ "<input type='text' name='user1'>"
+ "<input type='password' name='pass1'>"
+ "</form>"
+ "<form action='javascript:;'>"
+ "<input type='text' name='user2'>"
+ "<input type='password' name='pass2'>"
+ "<input type='submit' id='s2'>"
+ "</form>",
+ @"document.getElementById('s2').click()",
+ 1, true, "user2"
+ },
+ // Two forms with explicit names.
+ {
+ @"<form name='test2a' action='javascript:;'>"
+ "<input type='text' name='user1'>"
+ "<input type='password' name='pass1'>"
+ "<input type='submit' id='s1'>"
+ "</form>"
+ "<form name='test2b' action='javascript:;'>"
+ "<input type='text' name='user2'>"
+ "<input type='password' name='pass2'>"
+ "</form>",
+ @"document.getElementById('s1').click()",
+ 0, true, "user1"
+ },
+ // No password forms.
+ {
+ @"<form action='javascript:;'>"
+ "<input type='text' name='user1'>"
+ "<input type='text' name='pass1'>"
+ "<input type='submit' id='s1'>"
+ "</form>",
+ @"document.getElementById('s1').click()",
+ 0, false, nullptr
+ },
+ // Form with quotes in the form and field names.
+ {
+ @"<form name=\"foo'\" action='javascript:;'>"
+ "<input type='text' name=\"user1'\">"
+ "<input type='password' id='s1' name=\"pass1'\">"
+ "</form>",
+ @"document.getElementById('s1').click()",
+ 0, true, "user1'"
+ },
+ };
+ // clang-format on
+
+ for (const GetSubmittedPasswordFormTestData& data : test_data) {
+ SCOPED_TRACE(testing::Message() << "for html_string=" << data.html_string
+ << " and java_script=" << data.java_script
+ << " and number_of_forms_to_submit="
+ << data.number_of_forms_to_submit);
+ LoadHtml(data.html_string);
+ EvaluateJavaScriptAsString(data.java_script);
+ __block BOOL block_was_called = NO;
+ id completion_handler = ^(BOOL found, const PasswordForm& form) {
+ block_was_called = YES;
+ ASSERT_EQ(data.expected_form_found, found);
+ if (data.expected_form_found) {
+ EXPECT_EQ(base::ASCIIToUTF16(data.expected_username_element),
+ form.username_element);
+ }
+ };
+ [passwordController_
+ extractSubmittedPasswordForm:FormName(data.number_of_forms_to_submit)
+ completionHandler:completion_handler];
+ base::test::ios::WaitUntilCondition(^bool() {
+ return block_was_called;
+ });
+ }
+}
+
+// Populates |form_data| with test values.
+void SetPasswordFormFillData(PasswordFormFillData& form_data,
+ const std::string& origin,
+ const std::string& action,
+ const char* username_field,
+ const char* username_value,
+ const char* password_field,
+ const char* password_value,
+ const char* additional_username,
+ const char* additional_password,
+ bool wait_for_username) {
+ form_data.origin = GURL(origin);
+ form_data.action = GURL(action);
+ autofill::FormFieldData username;
+ username.name = base::UTF8ToUTF16(username_field);
+ username.value = base::UTF8ToUTF16(username_value);
+ form_data.username_field = username;
+ autofill::FormFieldData password;
+ password.name = base::UTF8ToUTF16(password_field);
+ password.value = base::UTF8ToUTF16(password_value);
+ form_data.password_field = password;
+ if (additional_username) {
+ autofill::PasswordAndRealm additional_password_data;
+ additional_password_data.password = base::UTF8ToUTF16(additional_password);
+ additional_password_data.realm.clear();
+ form_data.additional_logins.insert(
+ std::pair<base::string16, autofill::PasswordAndRealm>(
+ base::UTF8ToUTF16(additional_username), additional_password_data));
+ }
+ form_data.wait_for_username = wait_for_username;
+}
+
+// Test HTML page. It contains several password forms. Tests autofill
+// them and verify that the right ones are autofilled.
+static NSString* kHtmlWithMultiplePasswordForms =
+ @"<form>"
+ "<input id='un0' type='text' name='u0'>"
+ "<input id='pw0' type='password' name='p0'>"
+ "</form>"
+ "<form action='action?query=yes#reference'>"
+ "<input id='un1' type='text' name='u1'>"
+ "<input id='pw1' type='password' name='p1'>"
+ "</form>"
+ "<form action='http://some_other_action'>"
+ "<input id='un2' type='text' name='u2'>"
+ "<input id='pw2' type='password' name='p2'>"
+ "</form>"
+ "<form>"
+ "<input id='un3' type='text' name='u3'>"
+ "<input id='pw3' type='password' name='p3'>"
+ "<input id='pw3' type='password' name='p3'>"
+ "</form>"
+ "<form>"
+ "<input id='un4' type='text' name='u4'>"
+ "<input id='pw4' type='password' name='p4'>"
+ "</form>"
+ "<form>"
+ "<input id='un5' type='text' name='u4'>"
+ "<input id='pw5' type='password' name='p4'>"
+ "</form>"
+ "<form name=\"f6'\">"
+ "<input id=\"un6'\" type='text' name=\"u6'\">"
+ "<input id=\"pw6'\" type='password' name=\"p6'\">"
+ "</form>";
+
+// A script that resets all text fields.
+static NSString* kClearInputFieldsScript =
+ @"var inputs = document.getElementsByTagName('input');"
+ "for(var i = 0; i < inputs.length; i++){"
+ " inputs[i].value = '';"
+ "}";
+
+// A script that we run after autofilling forms. It returns
+// ids and values of all non-empty fields.
+static NSString* kInputFieldValueVerificationScript =
+ @"var result='';"
+ "var inputs = document.getElementsByTagName('input');"
+ "for(var i = 0; i < inputs.length; i++){"
+ " var input = inputs[i];"
+ " if (input.value) {"
+ " result += input.id + '=' + input.value +';';"
+ " }"
+ "}; result";
+
+struct FillPasswordFormTestData {
+ const std::string origin;
+ const std::string action;
+ const char* username_field;
+ const char* username_value;
+ const char* password_field;
+ const char* password_value;
+ const BOOL should_succeed;
+ NSString* expected_result;
+};
+
+// Test that filling password forms works correctly.
+TEST_F(PasswordControllerTest, FillPasswordForm) {
+ LoadHtml(kHtmlWithMultiplePasswordForms);
+
+ EXPECT_NSEQ(@"true",
+ EvaluateJavaScriptAsString(@"__gCrWeb.hasPasswordField()"));
+
+ const std::string base_url = BaseUrl();
+ // clang-format off
+ FillPasswordFormTestData test_data[] = {
+ // Basic test: one-to-one match on the first password form.
+ {
+ base_url,
+ base_url,
+ "u0",
+ "test_user",
+ "p0",
+ "test_password",
+ YES,
+ @"un0=test_user;pw0=test_password;"
+ },
+ // Multiple forms match: they should all be autofilled.
+ {
+ base_url,
+ base_url,
+ "u4",
+ "test_user",
+ "p4",
+ "test_password",
+ YES,
+ @"un4=test_user;pw4=test_password;un5=test_user;pw5=test_password;"
+ },
+ // The form matches despite a different action: the only difference
+ // is a query and reference.
+ {
+ base_url,
+ base_url,
+ "u1",
+ "test_user",
+ "p1",
+ "test_password",
+ YES,
+ @"un1=test_user;pw1=test_password;"
+ },
+ // No match because of a different origin.
+ {
+ "http://someotherfakedomain.com",
+ base_url,
+ "u0",
+ "test_user",
+ "p0",
+ "test_password",
+ NO,
+ @""
+ },
+ // No match because of a different action.
+ {
+ base_url,
+ "http://someotherfakedomain.com",
+ "u0",
+ "test_user",
+ "p0",
+ "test_password",
+ NO,
+ @""
+ },
+ // No match because some inputs are not in the form.
+ {
+ base_url,
+ base_url,
+ "u0",
+ "test_user",
+ "p1",
+ "test_password",
+ NO,
+ @""
+ },
+ // No match because there are duplicate inputs in the form.
+ {
+ base_url,
+ base_url,
+ "u3",
+ "test_user",
+ "p3",
+ "test_password",
+ NO,
+ @""
+ },
+ // Basic test, but with quotes in the names and IDs.
+ {
+ base_url,
+ base_url,
+ "u6'",
+ "test_user",
+ "p6'",
+ "test_password",
+ YES,
+ @"un6'=test_user;pw6'=test_password;"
+ },
+ };
+ // clang-format on
+
+ for (const FillPasswordFormTestData& data : test_data) {
+ EvaluateJavaScriptAsString(kClearInputFieldsScript);
+
+ PasswordFormFillData form_data;
+ SetPasswordFormFillData(form_data, data.origin, data.action,
+ data.username_field, data.username_value,
+ data.password_field, data.password_value, nullptr,
+ nullptr, false);
+
+ __block BOOL block_was_called = NO;
+ [passwordController_ fillPasswordForm:form_data
+ completionHandler:^(BOOL success) {
+ block_was_called = YES;
+ EXPECT_EQ(data.should_succeed, success);
+ }];
+ base::test::ios::WaitUntilCondition(^bool() {
+ return block_was_called;
+ });
+
+ NSString* result =
+ EvaluateJavaScriptAsString(kInputFieldValueVerificationScript);
+ EXPECT_NSEQ(data.expected_result, result);
+ }
+}
+
+// Tests that a form is found and the found form is filled in with the given
+// username and password.
+TEST_F(PasswordControllerTest, FindAndFillOnePasswordForm) {
+ LoadHtml(@"<form><input id='un' type='text' name='u'>"
+ "<input id='pw' type='password' name='p'></form>");
+ __block int call_counter = 0;
+ __block int success_counter = 0;
+ [passwordController_ findAndFillPasswordForms:@"john.doe@gmail.com"
+ password:@"super!secret"
+ completionHandler:^(BOOL complete) {
+ ++call_counter;
+ if (complete)
+ ++success_counter;
+ }];
+ base::test::ios::WaitUntilCondition(^{
+ return call_counter == 1;
+ });
+ EXPECT_EQ(1, success_counter);
+ NSString* result =
+ EvaluateJavaScriptAsString(kInputFieldValueVerificationScript);
+ EXPECT_NSEQ(@"un=john.doe@gmail.com;pw=super!secret;", result);
+}
+
+// Tests that multiple forms on the same page are found and filled.
+// This test includes an mock injected failure on form filling to verify
+// that completion handler is called with the proper values.
+TEST_F(PasswordControllerTest, FindAndFillMultiplePasswordForms) {
+ // Fails the first call to fill password form.
+ SetFillPasswordFormFailureCount(1);
+ LoadHtml(@"<form><input id='u1' type='text' name='un1'>"
+ "<input id='p1' type='password' name='pw1'></form>"
+ "<form><input id='u2' type='text' name='un2'>"
+ "<input id='p2' type='password' name='pw2'></form>"
+ "<form><input id='u3' type='text' name='un3'>"
+ "<input id='p3' type='password' name='pw3'></form>");
+ __block int call_counter = 0;
+ __block int success_counter = 0;
+ [passwordController_ findAndFillPasswordForms:@"john.doe@gmail.com"
+ password:@"super!secret"
+ completionHandler:^(BOOL complete) {
+ ++call_counter;
+ if (complete)
+ ++success_counter;
+ LOG(INFO) << "HANDLER call " << call_counter
+ << " success " << success_counter;
+ }];
+ // There should be 3 password forms and only 2 successfully filled forms.
+ base::test::ios::WaitUntilCondition(^{
+ return call_counter == 3;
+ });
+ EXPECT_EQ(2, success_counter);
+ NSString* result =
+ EvaluateJavaScriptAsString(kInputFieldValueVerificationScript);
+ EXPECT_NSEQ(@"u2=john.doe@gmail.com;p2=super!secret;"
+ "u3=john.doe@gmail.com;p3=super!secret;",
+ result);
+}
+
+BOOL PasswordControllerTest::BasicFormFill(NSString* html) {
+ LoadHtml(html);
+ EXPECT_NSEQ(@"true",
+ EvaluateJavaScriptAsString(@"__gCrWeb.hasPasswordField()"));
+ const std::string base_url = BaseUrl();
+ PasswordFormFillData form_data;
+ SetPasswordFormFillData(form_data, base_url, base_url, "u0", "test_user",
+ "p0", "test_password", nullptr, nullptr, false);
+ __block BOOL block_was_called = NO;
+ __block BOOL return_value = NO;
+ [passwordController_ fillPasswordForm:form_data
+ completionHandler:^(BOOL success) {
+ block_was_called = YES;
+ return_value = success;
+ }];
+ base::test::ios::WaitUntilCondition(^bool() {
+ return block_was_called;
+ });
+ return return_value;
+}
+
+// Check that |fillPasswordForm| is not filled if 'readonly' attribute is set
+// on either username or password fields.
+// TODO(crbug.com/503050): Test is flaky.
+TEST_F(PasswordControllerTest, FLAKY_DontFillReadOnly) {
+ // Control check that the fill operation will succceed with well-formed form.
+ EXPECT_TRUE(BasicFormFill(@"<form>"
+ "<input id='un0' type='text' name='u0'>"
+ "<input id='pw0' type='password' name='p0'>"
+ "</form>"));
+ // Form fill should fail with 'readonly' attribute on username.
+ EXPECT_FALSE(BasicFormFill(
+ @"<form>"
+ "<input id='un0' type='text' name='u0' readonly='readonly'>"
+ "<input id='pw0' type='password' name='p0'>"
+ "</form>"));
+ // Form fill should fail with 'readonly' attribute on password.
+ EXPECT_FALSE(BasicFormFill(
+ @"<form>"
+ "<input id='un0' type='text' name='u0'>"
+ "<input id='pw0' type='password' name='p0' readonly='readonly'>"
+ "</form>"));
+}
+
+// An HTML page containing one password form. The username input field
+// also has custom event handlers. We need to verify that those event
+// handlers are still triggered even though we override them with our own.
+static NSString* kHtmlWithPasswordForm =
+ @"<form>"
+ "<input id='un' type='text' name=\"u'\""
+ " onkeyup='window.onKeyUpCalled_=true'"
+ " onchange='window.onChangeCalled_=true'>"
+ "<input id='pw' type='password' name=\"p'\">"
+ "</form>";
+
+// A script that resets indicators used to verify that custom event
+// handlers are triggered. It also finds and the username and
+// password fields and caches them for future verification.
+static NSString* kUsernameAndPasswordTestPreparationScript =
+ @"onKeyUpCalled_ = false;"
+ "onChangeCalled_ = false;"
+ "username_ = document.getElementById('un');"
+ "username_.__gCrWebAutofilled = 'false';"
+ "password_ = document.getElementById('pw');"
+ "password_.__gCrWebAutofilled = 'false';";
+
+// A script that we run after autofilling forms. It returns
+// all values for verification as a single concatenated string.
+static NSString* kUsernamePasswordVerificationScript =
+ @"var value = username_.value;"
+ "var from = username_.selectionStart;"
+ "var to = username_.selectionEnd;"
+ "value.substr(0, from) + '[' + value.substr(from, to) + ']'"
+ " + value.substr(to, value.length) + '=' + password_.value"
+ " + ', onkeyup=' + onKeyUpCalled_"
+ " + ', onchange=' + onChangeCalled_;";
+
+struct SuggestionTestData {
+ std::string description;
+ NSArray* eval_scripts;
+ NSArray* expected_suggestions;
+ NSString* expected_result;
+};
+
+// Tests that form activity correctly sends suggestions to the suggestion
+// controller.
+TEST_F(PasswordControllerTest, SuggestionUpdateTests) {
+ LoadHtml(kHtmlWithPasswordForm);
+ const std::string base_url = BaseUrl();
+ EvaluateJavaScriptAsString(kUsernameAndPasswordTestPreparationScript);
+
+ // Initialize |form_data| with test data and an indicator that autofill
+ // should not be performed while the user is entering the username so that
+ // we can test with an initially-empty username field. Testing with a
+ // username field that contains input is performed by a specific test below.
+ PasswordFormFillData form_data;
+ SetPasswordFormFillData(form_data, base_url, base_url, "u'", "user0", "p'",
+ "password0", "abc", "def", true);
+ form_data.name = base::ASCIIToUTF16(FormName(0));
+
+ __block BOOL block_was_called = NO;
+ [passwordController_ fillPasswordForm:form_data
+ completionHandler:^(BOOL success) {
+ block_was_called = YES;
+ // Verify that the fill reports failed.
+ EXPECT_FALSE(success);
+ }];
+ base::test::ios::WaitUntilCondition(^bool() {
+ return block_was_called;
+ });
+
+ // Verify that the form has not been autofilled.
+ EXPECT_NSEQ(@"[]=, onkeyup=false, onchange=false",
+ EvaluateJavaScriptAsString(kUsernamePasswordVerificationScript));
+
+ // clang-format off
+ SuggestionTestData test_data[] = {
+ {
+ "Should show all suggestions when focusing empty username field",
+ @[(@"var evt = document.createEvent('Events');"
+ "evt.initEvent('focus', true, true, window, 1);"
+ "username_.dispatchEvent(evt);"),
+ @""],
+ @[@"abc", @"user0"],
+ @"[]=, onkeyup=false, onchange=false"
+ },
+ {
+ "Should not show suggestions when focusing password field",
+ @[(@"var evt = document.createEvent('Events');"
+ "evt.initEvent('focus', true, true, window, 1);"
+ "password_.dispatchEvent(evt);"),
+ @""],
+ @[],
+ @"[]=, onkeyup=false, onchange=false"
+ },
+ {
+ "Should filter suggestions when focusing username field with input",
+ @[(@"username_.value='ab';"
+ "var evt = document.createEvent('Events');"
+ "evt.initEvent('focus', true, true, window, 1);"
+ "username_.dispatchEvent(evt);"),
+ @""],
+ @[@"abc"],
+ @"ab[]=, onkeyup=false, onchange=false"
+ },
+ {
+ "Should filter suggestions while typing",
+ @[(@"var evt = document.createEvent('Events');"
+ "evt.initEvent('focus', true, true, window, 1);"
+ "username_.dispatchEvent(evt);"),
+ (@"username_.value='ab';"
+ "evt = document.createEvent('Events');"
+ "evt.initEvent('keyup', true, true, window, 1);"
+ "evt.keyCode = 98;"
+ "username_.dispatchEvent(evt);"),
+ @""],
+ @[@"abc"],
+ @"ab[]=, onkeyup=true, onchange=false"
+ },
+ {
+ "Should unfilter suggestions after backspacing",
+ @[(@"var evt = document.createEvent('Events');"
+ "evt.initEvent('focus', true, true, window, 1);"
+ "username_.dispatchEvent(evt);"),
+ (@"username_.value='ab';"
+ "evt = document.createEvent('Events');"
+ "evt.initEvent('keyup', true, true, window, 1);"
+ "evt.keyCode = 98;"
+ "username_.dispatchEvent(evt);"),
+ (@"username_.value='';"
+ "evt = document.createEvent('Events');"
+ "evt.initEvent('keyup', true, true, window, 1);"
+ "evt.keyCode = 8;"
+ "username_.dispatchEvent(evt);"),
+ @""],
+ @[@"abc", @"user0"],
+ @"[]=, onkeyup=true, onchange=false"
+ },
+ };
+ // clang-format on
+
+ for (const SuggestionTestData& data : test_data) {
+ SCOPED_TRACE(testing::Message()
+ << "for description=" << data.description
+ << " and eval_scripts=" << data.eval_scripts);
+ // Prepare the test.
+ EvaluateJavaScriptAsString(kUsernameAndPasswordTestPreparationScript);
+
+ for (NSString* script in data.eval_scripts) {
+ // Trigger events.
+ EvaluateJavaScriptAsString(script);
+
+ // Pump the run loop so that the host can respond.
+ WaitForBackgroundTasks();
+ }
+
+ EXPECT_NSEQ(data.expected_suggestions, GetSortedSuggestionValues());
+ EXPECT_NSEQ(data.expected_result, EvaluateJavaScriptAsString(
+ kUsernamePasswordVerificationScript));
+ // Clear all suggestions.
+ [suggestionController_ setSuggestions:nil];
+ }
+}
+
+// Tests that selecting a suggestion will fill the corresponding form and field.
+TEST_F(PasswordControllerTest, SelectingSuggestionShouldFillPasswordForm) {
+ LoadHtml(kHtmlWithPasswordForm);
+ const std::string base_url = BaseUrl();
+ EvaluateJavaScriptAsString(kUsernameAndPasswordTestPreparationScript);
+
+ // Initialize |form_data| with test data and an indicator that autofill
+ // should not be performed while the user is entering the username so that
+ // we can test with an initially-empty username field.
+ PasswordFormFillData form_data;
+ SetPasswordFormFillData(form_data, base_url, base_url, "u'", "user0", "p'",
+ "password0", "abc", "def", true);
+ form_data.name = base::ASCIIToUTF16(FormName(0));
+
+ __block BOOL block_was_called = NO;
+ [passwordController_ fillPasswordForm:form_data
+ completionHandler:^(BOOL success) {
+ block_was_called = YES;
+ // Verify that the fill reports failed.
+ EXPECT_FALSE(success);
+ }];
+ base::test::ios::WaitUntilCondition(^bool() {
+ return block_was_called;
+ });
+
+ // Verify that the form has not been autofilled.
+ EXPECT_NSEQ(@"[]=, onkeyup=false, onchange=false",
+ EvaluateJavaScriptAsString(kUsernamePasswordVerificationScript));
+
+ // Tell PasswordController that a suggestion was selected. It should fill
+ // out the password form with the corresponding credentials.
+ FormSuggestion* suggestion = [FormSuggestion suggestionWithValue:@"abc"
+ displayDescription:nil
+ icon:nil
+ identifier:0];
+
+ block_was_called = NO;
+ SuggestionHandledCompletion completion = ^{
+ block_was_called = YES;
+ EXPECT_NSEQ(@"abc[]=def, onkeyup=false, onchange=false",
+ ExecuteJavaScript(kUsernamePasswordVerificationScript));
+ };
+ [passwordController_ didSelectSuggestion:suggestion
+ forField:@"u"
+ form:base::SysUTF8ToNSString(FormName(0))
+ completionHandler:completion];
+ base::test::ios::WaitUntilCondition(^bool() {
+ return block_was_called;
+ });
+}
+
+// Tests with invalid inputs.
+TEST_F(PasswordControllerTest, CheckIncorrectData) {
+ // clang-format off
+ std::string invalid_data[] = {
+ "{}",
+
+ "{ \"usernameValue\": \"fakeaccount\","
+ "\"passwords\": ["
+ "{ \"element\": \"secret\"," "\"value\": \"fakesecret\" },"
+ "]}",
+
+ "{ \"usernameElement\": \"account\","
+ "\"passwords\": ["
+ "{ \"element\": \"secret\"," "\"value\": \"fakesecret\" },"
+ "]}",
+
+ "{ \"usernameElement\": \"account\","
+ "\"usernameValue\": \"fakeaccount\","
+ "}",
+
+ "{ \"usernameElement\": \"account\","
+ "\"usernameValue\": \"fakeaccount\","
+ "\"passwords\": {},"
+ "}",
+
+ "{ \"usernameElement\": \"account\","
+ "\"usernameValue\": \"fakeaccount\","
+ "\"passwords\": ["
+ "]}",
+
+ "{ \"usernameElement\": \"account\","
+ "\"usernameValue\": \"fakeaccount\","
+ "\"passwords\": ["
+ "{ \"value\": \"fakesecret\" },"
+ "]}",
+
+ "{ \"usernameElement\": \"account\","
+ "\"usernameValue\": \"fakeaccount\","
+ "\"passwords\": ["
+ "{ \"element\": \"secret\" },"
+ "]}",
+ };
+ // clang-format on
+
+ for (const std::string& data : invalid_data) {
+ SCOPED_TRACE(testing::Message() << "for data=" << data);
+ std::unique_ptr<base::Value> json_data(base::JSONReader::Read(data, true));
+ const base::DictionaryValue* json_dict = nullptr;
+ ASSERT_TRUE(json_data->GetAsDictionary(&json_dict));
+ PasswordForm form;
+ BOOL res =
+ [passwordController_ getPasswordForm:&form
+ fromDictionary:json_dict
+ pageURL:GURL("https://www.foo.com/")];
+ EXPECT_FALSE(res);
+ }
+}
+
+// The test case below does not need the heavy fixture from above, but it
+// needs to use MockWebState.
+TEST(PasswordControllerTestSimple, SaveOnNonHTMLLandingPage) {
TestChromeBrowserState::Builder builder;
- auto pref_service =
- base::WrapUnique(new syncable_prefs::TestingPrefServiceSyncable);
- pref_service->registry()->RegisterBooleanPref(
- password_manager::prefs::kPasswordManagerSavingEnabled, true);
- builder.SetPrefService(std::move(pref_service));
std::unique_ptr<TestChromeBrowserState> browser_state(builder.Build());
MockWebState web_state;
ON_CALL(web_state, GetBrowserState())
.WillByDefault(testing::Return(browser_state.get()));
- auto client = base::WrapUnique(new MockPasswordManagerClient);
- MockPasswordManagerClient* weak_client = client.get();
- base::scoped_nsobject<PasswordController> passwordController(
- [[PasswordController alloc] initWithWebState:&web_state
- passwordsUiDelegate:nil
- client:std::move(client)]);
+
+ MockPasswordManagerClient* weak_client = nullptr;
+ base::scoped_nsobject<PasswordController> passwordController =
+ CreatePasswordController(&web_state, nullptr, &weak_client);
+ static_cast<TestingPrefServiceSimple*>(weak_client->GetPrefs())
+ ->registry()
+ ->RegisterBooleanPref(
+ password_manager::prefs::kPasswordManagerSavingEnabled, true);
// Use a mock LogManager to detect that OnPasswordFormsRendered has been
// called. TODO(crbug.com/598672): this is a hack, we should modularize the

Powered by Google App Engine
This is Rietveld 408576698