Chromium Code Reviews| 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 |