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..fd9aef6e69de30c12c3034a07b0e077366acff2f 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/json/json_reader.h" |
+#include "base/mac/bind_objc_block.h" |
#include "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,1196 @@ 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; |
+}; |
+ |
+TEST_F(PasswordControllerTest, PopulatePasswordFormWithDictionary) { |
Eugene But (OOO till 7-30)
2016/07/13 22:30:31
If you understand the purpose if these tests, coul
vabr (Chromium)
2016/07/14 07:36:31
Done.
|
+ // 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. |
+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. |
+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_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 |