// Copyright 2020 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#import "components/password_manager/ios/shared_password_controller.h"
#import "base/memory/raw_ptr.h"
#import "base/strings/sys_string_conversions.h"
#import "base/test/metrics/histogram_tester.h"
#import "base/test/scoped_feature_list.h"
#import "base/test/task_environment.h"
#import "components/autofill/core/browser/autofill_driver_router.h"
#import "components/autofill/core/browser/autofill_test_utils.h"
#import "components/autofill/core/browser/form_structure.h"
#import "components/autofill/core/browser/test_autofill_client.h"
#import "components/autofill/core/browser/test_browser_autofill_manager.h"
#import "components/autofill/core/browser/ui/suggestion_type.h"
#import "components/autofill/core/common/autofill_features.h"
#import "components/autofill/core/common/autofill_test_utils.h"
#import "components/autofill/core/common/form_data.h"
#import "components/autofill/core/common/form_data_test_api.h"
#import "components/autofill/core/common/password_form_generation_data.h"
#import "components/autofill/core/common/password_generation_util.h"
#import "components/autofill/core/common/unique_ids.h"
#import "components/autofill/ios/browser/autofill_driver_ios.h"
#import "components/autofill/ios/browser/autofill_driver_ios_factory.h"
#import "components/autofill/ios/browser/autofill_util.h"
#import "components/autofill/ios/browser/form_suggestion.h"
#import "components/autofill/ios/browser/form_suggestion_provider_query.h"
#import "components/autofill/ios/browser/password_autofill_agent.h"
#import "components/autofill/ios/browser/test_autofill_manager_injector.h"
#import "components/autofill/ios/form_util/child_frame_registrar.h"
#import "components/autofill/ios/form_util/form_activity_params.h"
#import "components/password_manager/core/browser/mock_password_manager.h"
#import "components/password_manager/core/browser/password_generation_frame_helper.h"
#import "components/password_manager/core/browser/password_manager_interface.h"
#import "components/password_manager/core/browser/stub_password_manager_client.h"
#import "components/password_manager/core/browser/stub_password_manager_driver.h"
#import "components/password_manager/core/common/password_manager_features.h"
#import "components/password_manager/ios/constants.h"
#import "components/password_manager/ios/ios_password_manager_driver.h"
#import "components/password_manager/ios/ios_password_manager_driver_factory.h"
#import "components/password_manager/ios/password_controller_driver_helper.h"
#import "components/password_manager/ios/password_form_helper.h"
#import "components/password_manager/ios/password_manager_ios_util.h"
#import "components/password_manager/ios/password_manager_java_script_feature.h"
#import "components/password_manager/ios/password_suggestion_helper.h"
#import "components/password_manager/ios/shared_password_controller+private.h"
#import "components/password_manager/ios/test_helpers.h"
#import "ios/web/public/test/fakes/fake_navigation_context.h"
#import "ios/web/public/test/fakes/fake_web_frame.h"
#import "ios/web/public/test/fakes/fake_web_frames_manager.h"
#import "ios/web/public/test/fakes/fake_web_state.h"
#import "testing/gmock/include/gmock/gmock.h"
#import "testing/gtest/include/gtest/gtest.h"
#import "testing/gtest_mac.h"
#import "testing/platform_test.h"
#import "third_party/ocmock/OCMock/OCMock.h"
#define andCompareStringAtIndex(expected_string, index) \
andDo(^(NSInvocation * invocation) { \
const std::string* param; \
[invocation getArgument:¶m atIndex:index + 2]; \
EXPECT_EQ(*param, expected_string); \
})
namespace password_manager {
namespace {
using autofill::AutofillDriverIOS;
using autofill::AutofillDriverIOSFactory;
using autofill::FormData;
using autofill::LocalFrameToken;
using autofill::PasswordFormFillData;
using autofill::RemoteFrameToken;
using autofill::TestAutofillManagerInjector;
using autofill::TestBrowserAutofillManager;
using autofill::password_generation::PasswordGenerationType;
using autofill::test::CreateTestFormField;
using autofill::test::MakeLocalFrameToken;
using autofill::test::MakeRemoteFrameToken;
using base::SysNSStringToUTF8;
using base::SysUTF16ToNSString;
using password_manager::IsCrossOriginIframe;
using password_manager::PasswordGenerationFrameHelper;
using ::testing::_;
using ::testing::Return;
const std::string kTestURL = "https://www.chromium.org/";
NSString* kTestFrameID = @"dummy-frame-id";
// Creates renderer FormData tied to a frame that can compose a xframe browser
// form.
FormData CreateFormDataForRenderFrameHost(
web::WebFrame* frame,
LocalFrameToken host_frame_token,
std::vector<autofill::FormFieldData> fields) {
FormData form;
form.set_url(frame->GetSecurityOrigin());
form.set_action(form.url());
form.set_host_frame(host_frame_token);
form.set_renderer_id(autofill::test::MakeFormRendererId());
for (autofill::FormFieldData& field : fields) {
field.set_host_frame(form.host_frame());
field.set_host_form_id(form.renderer_id());
}
form.set_fields(std::move(fields));
return form;
}
} // namespace
class MockPasswordGenerationFrameHelper : public PasswordGenerationFrameHelper {
public:
MOCK_METHOD(std::u16string,
GeneratePassword,
(const GURL&,
autofill::password_generation::PasswordGenerationType,
autofill::FormSignature,
autofill::FieldSignature,
uint64_t),
(override));
MOCK_METHOD(bool, IsGenerationEnabled, (bool), (const));
explicit MockPasswordGenerationFrameHelper()
: PasswordGenerationFrameHelper(nullptr, nullptr) {}
};
class SharedPasswordControllerTest : public PlatformTest {
public:
SharedPasswordControllerTest() : PlatformTest() {
delegate_ = OCMProtocolMock(@protocol(SharedPasswordControllerDelegate));
password_manager::PasswordManagerClient* client_ptr =
&password_manager_client_;
[[[delegate_ stub] andReturnValue:OCMOCK_VALUE(client_ptr)]
passwordManagerClient];
form_helper_ = OCMStrictClassMock([PasswordFormHelper class]);
suggestion_helper_ = OCMStrictClassMock([PasswordSuggestionHelper class]);
driver_helper_ = OCMStrictClassMock([PasswordControllerDriverHelper class]);
OCMExpect([form_helper_ setDelegate:[OCMArg any]]);
OCMExpect([suggestion_helper_ setDelegate:[OCMArg any]]);
auto web_frames_manager = std::make_unique<web::FakeWebFramesManager>();
web_frames_manager_ = web_frames_manager.get();
web::ContentWorld content_world =
PasswordManagerJavaScriptFeature::GetInstance()
->GetSupportedContentWorld();
web_state_.SetWebFramesManager(content_world,
std::move(web_frames_manager));
AutofillDriverIOSFactory::CreateForWebState(
&web_state_, &autofill_client_, /*bridge=*/nil, /*locale=*/"en");
// The manager injector must be created before creating the controller to
// make sure it can exchange the manager before the controller starts
// observing it.
autofill_manager_injector_ = std::make_unique<
TestAutofillManagerInjector<TestBrowserAutofillManager>>(&web_state_);
controller_ =
[[SharedPasswordController alloc] initWithWebState:&web_state_
manager:&password_manager_
formHelper:form_helper_
suggestionHelper:suggestion_helper_
driverHelper:driver_helper_];
controller_.delegate = delegate_;
[suggestion_helper_ verify];
[form_helper_ verify];
web_state_.SetCurrentURL(GURL(kTestURL));
}
void SetUp() override {
PlatformTest::SetUp();
PasswordGenerationFrameHelper* password_generation_helper_ptr =
&password_generation_helper_;
OCMStub(
[driver_helper_ PasswordGenerationHelper:static_cast<web::WebFrame*>(
[OCMArg anyPointer])])
.andReturn(password_generation_helper_ptr);
EXPECT_CALL(password_manager_, GetClient)
.WillRepeatedly(Return(&password_manager_client_));
// It's not possible to mock the driver, and it has to be non-null, so we
// have the mock DriverHelper return a real driver.
auto dummy_web_frame =
web::FakeWebFrame::CreateMainWebFrame(GURL(kTestURL));
dummy_driver_ = IOSPasswordManagerDriverFactory::GetRetainableDriver(
&web_state_, dummy_web_frame.get());
OCMStub([driver_helper_ PasswordManagerDriver:static_cast<web::WebFrame*>(
[OCMArg anyPointer])])
.andReturn(dummy_driver_.get());
}
protected:
void AddWebFrame(std::unique_ptr<web::WebFrame> frame,
id completion_handler) {
OCMExpect([form_helper_ findPasswordFormsInFrame:frame.get()
completionHandler:completion_handler]);
web_frames_manager_->AddWebFrame(std::move(frame));
}
void AddWebFrame(std::unique_ptr<web::WebFrame> frame) {
AddWebFrame(std::move(frame), [OCMArg any]);
}
base::test::TaskEnvironment task_environment_;
autofill::test::AutofillUnitTestEnvironment autofill_test_environment_;
autofill::TestAutofillClient autofill_client_;
std::unique_ptr<TestAutofillManagerInjector<TestBrowserAutofillManager>>
autofill_manager_injector_;
web::FakeWebState web_state_;
raw_ptr<web::FakeWebFramesManager> web_frames_manager_;
testing::StrictMock<MockPasswordManager> password_manager_;
testing::StrictMock<MockPasswordGenerationFrameHelper>
password_generation_helper_;
id form_helper_;
id suggestion_helper_;
id driver_helper_;
scoped_refptr<IOSPasswordManagerDriver> dummy_driver_;
password_manager::StubPasswordManagerClient password_manager_client_;
id delegate_;
SharedPasswordController* controller_;
};
TEST_F(SharedPasswordControllerTest,
PasswordManagerIsNotNotifiedAboutHeuristicsPredictions) {
auto web_frame =
web::FakeWebFrame::Create(SysNSStringToUTF8(kTestFrameID),
/*is_main_frame=*/true, GURL(kTestURL));
web::WebFrame* frame = web_frame.get();
AddWebFrame(std::move(web_frame));
EXPECT_CALL(password_manager_, ProcessAutofillPredictions).Times(0);
// Simulate seeing a form.
TestBrowserAutofillManager* manager =
autofill_manager_injector_->GetForFrame(frame);
ASSERT_TRUE(manager);
FormData test_form = autofill::test::CreateTestPersonalInformationFormData();
// `OnFormsSeen` emits a `OnFieldTypesDetermined` event, but with source
// heuristics - this should be ignored by the `SharedPasswordController`.
manager->OnFormsSeen(/*updated_forms=*/{test_form}, /*removed_forms=*/{});
}
// Tests that the password manager of a single main frame is notified about
// server predictions for a form.
TEST_F(SharedPasswordControllerTest,
PasswordManagerIsNotifiedAboutServerPredictions_SingleFrame) {
auto web_frame =
web::FakeWebFrame::Create(SysNSStringToUTF8(kTestFrameID),
/*is_main_frame=*/true, GURL(kTestURL));
web::WebFrame* frame = web_frame.get();
AddWebFrame(std::move(web_frame));
EXPECT_CALL(password_manager_, ProcessAutofillPredictions);
// Simulate seeing a form.
TestBrowserAutofillManager* manager =
autofill_manager_injector_->GetForFrame(frame);
ASSERT_TRUE(manager);
FormData test_form = autofill::test::CreateTestPersonalInformationFormData();
manager->OnFormsSeen(/*updated_forms=*/{test_form}, /*removed_forms=*/{});
// Trigger `OnFieldTypesDetetermined` with source `kAutofillServer` explicitly
// to simulate receiving server predictions.
using Observer = autofill::AutofillManager::Observer;
manager->NotifyObservers(&Observer::OnFieldTypesDetermined,
test_form.global_id(),
Observer::FieldTypeSource::kAutofillServer);
}
// Tests that the password manager of each frame is notified about server
// predictions when a form streches across multiple frames.
TEST_F(SharedPasswordControllerTest,
PasswordManagerIsNotifiedAboutServerPredictions_AcrossFrames) {
base::test::ScopedFeatureList feature_list(
autofill::features::kAutofillAcrossIframesIos);
const LocalFrameToken main_frame_local_frame_token(MakeLocalFrameToken());
const LocalFrameToken child_frame_local_token(MakeLocalFrameToken());
const RemoteFrameToken child_frame_remote_token(MakeRemoteFrameToken());
auto main_frame =
web::FakeWebFrame::Create(main_frame_local_frame_token.ToString(),
/*is_main_frame=*/true, GURL(kTestURL));
web::WebFrame* main_frame_ptr = main_frame.get();
AddWebFrame(std::move(main_frame));
auto child_frame =
web::FakeWebFrame::Create(child_frame_local_token.ToString(),
/*is_main_frame=*/true, GURL(kTestURL));
web::WebFrame* child_frame_ptr = child_frame.get();
AddWebFrame(std::move(child_frame));
FormData main_form = CreateFormDataForRenderFrameHost(
main_frame_ptr, main_frame_local_frame_token,
{CreateTestFormField("Search", "search", "",
autofill::FormControlType::kInputText)});
{
autofill::FrameTokenWithPredecessor child_token;
child_token.token = child_frame_remote_token;
child_token.predecessor = 64;
main_form.set_child_frames({child_token});
}
FormData child_form = CreateFormDataForRenderFrameHost(
child_frame_ptr, child_frame_local_token,
{CreateTestFormField("Username", "username", "",
autofill::FormControlType::kInputText),
CreateTestFormField("Password", "password", "",
autofill::FormControlType::kInputPassword)});
// Simulate seeing a form in the main frame.
{
auto* driver =
AutofillDriverIOS::FromWebStateAndWebFrame(&web_state_, main_frame_ptr);
driver->FormsSeen(/*updated_forms=*/{main_form},
/*removed_forms=*/{});
}
// Simulate seeing a form in the child frame.
{
auto* driver = AutofillDriverIOS::FromWebStateAndWebFrame(&web_state_,
child_frame_ptr);
driver->FormsSeen(/*updated_forms=*/{child_form},
/*removed_forms=*/{});
}
// Complete registration of the child frame so it can be included in the
// browser form.
{
auto* registrar =
autofill::ChildFrameRegistrar::GetOrCreateForWebState(&web_state_);
registrar->RegisterMapping(child_frame_remote_token,
child_frame_local_token);
}
// Verify that the browser form is correctly constructed, composed of the main
// frame form and child frame form, having 3 fields in total.
autofill::FormStructure* browser_form =
autofill_manager_injector_->GetForFrame(main_frame_ptr)
->FindCachedFormById(main_form.global_id());
ASSERT_TRUE(browser_form);
ASSERT_EQ(3u, browser_form->field_count());
// Expect to process predictions for each renderer form that composes the
// browser form. Validate that the arguments are correct.
{
auto* password_driver =
IOSPasswordManagerDriverFactory::FromWebStateAndWebFrame(
&web_state_, main_frame_ptr);
EXPECT_CALL(
password_manager_,
ProcessAutofillPredictions(password_driver,
::testing::Property(&FormData::renderer_id,
main_form.renderer_id()),
::testing::SizeIs(3)));
}
{
auto* password_driver =
IOSPasswordManagerDriverFactory::FromWebStateAndWebFrame(
&web_state_, child_frame_ptr);
EXPECT_CALL(password_manager_,
ProcessAutofillPredictions(
password_driver,
::testing::Property(&FormData::renderer_id,
child_form.renderer_id()),
::testing::SizeIs(3)));
}
// Trigger `OnFieldTypesDetetermined` with source `kAutofillServer` explicitly
// to simulate receiving server predictions.
// using Observer = autofill::AutofillManager::Observer;
TestBrowserAutofillManager* manager =
autofill_manager_injector_->GetForFrame(main_frame_ptr);
using Observer = autofill::AutofillManager::Observer;
manager->NotifyObservers(&Observer::OnFieldTypesDetermined,
main_form.global_id(),
Observer::FieldTypeSource::kAutofillServer);
}
// Test that PasswordManager is notified of main frame navigation.
TEST_F(SharedPasswordControllerTest,
PasswordManagerDidNavigationMainFrameOnNavigationFinished) {
web::FakeNavigationContext navigation_context;
navigation_context.SetHasCommitted(true);
navigation_context.SetIsSameDocument(false);
navigation_context.SetIsRendererInitiated(true);
auto web_frame =
web::FakeWebFrame::Create(SysNSStringToUTF8(kTestFrameID),
/*is_main_frame=*/true, GURL(kTestURL));
AddWebFrame(std::move(web_frame));
EXPECT_CALL(password_manager_, DidNavigateMainFrame(true));
OCMExpect([suggestion_helper_ resetForNewPage]);
web_state_.OnNavigationFinished(&navigation_context);
}
// Tests that forms are found, parsed, and sent to PasswordManager.
TEST_F(SharedPasswordControllerTest, FormsArePropagatedOnHTMLPageLoad) {
web_state_.SetCurrentURL(GURL(kTestURL));
web_state_.SetContentIsHTML(true);
auto web_frame =
web::FakeWebFrame::Create("dummy-frame-id",
/*is_main_frame=*/true, GURL(kTestURL));
web::WebFrame* frame = web_frame.get();
id mock_completion_handler = [OCMArg checkWithBlock:^(void (
^completionHandler)(const std::vector<FormData>& forms, uint32_t maxID)) {
OCMExpect([driver_helper_ PasswordManagerDriver:frame]);
EXPECT_CALL(password_manager_, OnPasswordFormsParsed);
EXPECT_CALL(password_manager_, OnPasswordFormsRendered);
FormData form_data = test_helpers::MakeSimpleFormData();
completionHandler({form_data}, 1);
return YES;
}];
AddWebFrame(std::move(web_frame), mock_completion_handler);
web_state_.OnPageLoaded(web::PageLoadCompletionStatus::SUCCESS);
[suggestion_helper_ verify];
[form_helper_ verify];
}
// Tests form finding and parsing is not triggered for non HTML pages.
TEST_F(SharedPasswordControllerTest, NoFormsArePropagatedOnNonHTMLPageLoad) {
web_state_.SetCurrentURL(GURL(kTestURL));
web_state_.SetContentIsHTML(false);
const std::string web_frame_id = SysNSStringToUTF8(kTestFrameID);
auto web_frame = web::FakeWebFrame::Create(
web_frame_id, /*is_main_frame=*/true, GURL(kTestURL));
web::WebFrame* frame = web_frame.get();
web_frames_manager_->AddWebFrame(std::move(web_frame));
[[form_helper_ reject] findPasswordFormsInFrame:frame
completionHandler:[OCMArg any]];
OCMExpect([driver_helper_ PasswordManagerDriver:frame]);
OCMExpect([[suggestion_helper_ ignoringNonObjectArgs]
processWithNoSavedCredentialsWithFrameId:""])
.andCompareStringAtIndex(web_frame_id, 0);
EXPECT_CALL(password_manager_, OnPasswordFormsRendered);
web_state_.OnPageLoaded(web::PageLoadCompletionStatus::SUCCESS);
[suggestion_helper_ verify];
[form_helper_ verify];
}
// Tests that suggestions are reported as unavailable for nonpassword forms.
TEST_F(SharedPasswordControllerTest,
CheckNoSuggestionsAreAvailableForNonPasswordForm) {
FormSuggestionProviderQuery* form_query = [[FormSuggestionProviderQuery alloc]
initWithFormName:@"form"
formRendererID:autofill::FormRendererId(0)
fieldIdentifier:@"field"
fieldRendererID:autofill::FieldRendererId(1)
fieldType:@"text"
type:@"focus"
typedValue:@""
frameID:kTestFrameID];
auto web_frame =
web::FakeWebFrame::Create(SysNSStringToUTF8(kTestFrameID),
/*is_main_frame=*/false, GURL(kTestURL));
web::WebFrame* frame = web_frame.get();
AddWebFrame(std::move(web_frame));
id mock_completion_handler =
[OCMArg checkWithBlock:^BOOL(void (^completionHandler)(BOOL)) {
// Ensure |suggestion_helper_| reports no suggestions.
completionHandler(NO);
return YES;
}];
[[suggestion_helper_ expect]
checkIfSuggestionsAvailableForForm:form_query
completionHandler:mock_completion_handler];
[[suggestion_helper_ expect] isPasswordFieldOnForm:form_query webFrame:frame];
__block BOOL completion_was_called = NO;
[controller_ checkIfSuggestionsAvailableForForm:form_query
hasUserGesture:NO
webState:&web_state_
completionHandler:^(BOOL suggestionsAvailable) {
EXPECT_FALSE(suggestionsAvailable);
completion_was_called = YES;
}];
EXPECT_TRUE(completion_was_called);
[suggestion_helper_ verify];
}
// Tests that no suggestions are returned if PasswordSuggestionHelper has none.
TEST_F(SharedPasswordControllerTest, ReturnsNoSuggestionsIfNoneAreAvailable) {
FormSuggestionProviderQuery* form_query = [[FormSuggestionProviderQuery alloc]
initWithFormName:@"form"
formRendererID:autofill::FormRendererId(0)
fieldIdentifier:@"field"
fieldRendererID:autofill::FieldRendererId(1)
fieldType:kObfuscatedFieldType // Ensures this is a password form.
type:@"focus"
typedValue:@""
frameID:kTestFrameID];
const std::string web_frame_id = SysNSStringToUTF8(kTestFrameID);
auto web_frame =
web::FakeWebFrame::Create(web_frame_id,
/*is_main_frame=*/false, GURL(kTestURL));
web::WebFrame* frame = web_frame.get();
AddWebFrame(std::move(web_frame));
OCMExpect([suggestion_helper_ retrieveSuggestionsWithForm:form_query])
.andReturn(@[]);
OCMExpect([[suggestion_helper_ ignoringNonObjectArgs]
isPasswordFieldOnForm:form_query
webFrame:frame]);
OCMExpect([driver_helper_ PasswordManagerDriver:frame]);
EXPECT_CALL(password_generation_helper_, IsGenerationEnabled(true))
.WillOnce(Return(true));
__block BOOL completion_was_called = NO;
[controller_
retrieveSuggestionsForForm:form_query
webState:&web_state_
completionHandler:^(NSArray<FormSuggestion*>* suggestions,
id<FormSuggestionProvider> delegate) {
EXPECT_EQ(0UL, suggestions.count);
EXPECT_EQ(delegate, controller_);
completion_was_called = YES;
}];
EXPECT_TRUE(completion_was_called);
}
// Tests that no suggestions are returned if the frame was destroyed.
TEST_F(SharedPasswordControllerTest, ReturnsNoSuggestionsIfFrameDestroyed) {
FormSuggestionProviderQuery* form_query = [[FormSuggestionProviderQuery alloc]
initWithFormName:@"form"
formRendererID:autofill::FormRendererId(0)
fieldIdentifier:@"field"
fieldRendererID:autofill::FieldRendererId(1)
fieldType:kObfuscatedFieldType // Ensures this is a password form.
type:@"focus"
typedValue:@""
frameID:kTestFrameID];
web::WebFrame* frame = nullptr;
const std::string frame_id = "";
OCMExpect([suggestion_helper_ retrieveSuggestionsWithForm:form_query])
.andReturn(@[]);
OCMExpect([[suggestion_helper_ ignoringNonObjectArgs]
isPasswordFieldOnForm:form_query
webFrame:frame]);
OCMExpect([driver_helper_ PasswordManagerDriver:frame]);
__block BOOL completion_was_called = NO;
[controller_
retrieveSuggestionsForForm:form_query
webState:&web_state_
completionHandler:^(NSArray<FormSuggestion*>* suggestions,
id<FormSuggestionProvider> delegate) {
EXPECT_EQ(0UL, suggestions.count);
EXPECT_EQ(delegate, controller_);
completion_was_called = YES;
}];
EXPECT_TRUE(completion_was_called);
}
// Tests that suggestions are returned if PasswordSuggestionHelper has some.
TEST_F(SharedPasswordControllerTest, ReturnsSuggestionsIfAvailable) {
FormSuggestionProviderQuery* form_query = [[FormSuggestionProviderQuery alloc]
initWithFormName:@"form"
formRendererID:autofill::FormRendererId(0)
fieldIdentifier:@"field"
fieldRendererID:autofill::FieldRendererId(1)
fieldType:kObfuscatedFieldType // Ensures this is a password form.
type:@"focus"
typedValue:@""
frameID:kTestFrameID];
FormSuggestion* suggestion = [FormSuggestion
suggestionWithValue:@"value"
displayDescription:@"display-description"
icon:nil
type:autofill::SuggestionType::kAutocompleteEntry
backendIdentifier:nil
requiresReauth:NO
acceptanceA11yAnnouncement:nil
metadata:{.is_single_username_form = true}];
const std::string web_frame_id = SysNSStringToUTF8(kTestFrameID);
auto web_frame =
web::FakeWebFrame::Create(web_frame_id,
/*is_main_frame=*/false, GURL(kTestURL));
web::WebFrame* frame = web_frame.get();
AddWebFrame(std::move(web_frame));
OCMExpect([suggestion_helper_ retrieveSuggestionsWithForm:form_query])
.andReturn(@[ suggestion ]);
OCMExpect([[suggestion_helper_ ignoringNonObjectArgs]
isPasswordFieldOnForm:form_query
webFrame:frame]);
OCMExpect([driver_helper_ PasswordManagerDriver:frame]);
EXPECT_CALL(password_generation_helper_, IsGenerationEnabled(true))
.WillOnce(Return(true));
__block BOOL completion_was_called = NO;
[controller_
retrieveSuggestionsForForm:form_query
webState:&web_state_
completionHandler:^(NSArray<FormSuggestion*>* suggestions,
id<FormSuggestionProvider> delegate) {
EXPECT_EQ(1UL, suggestions.count);
// Verify that the metadata is correctly copied over.
EXPECT_TRUE([suggestions firstObject]
.metadata.is_single_username_form);
EXPECT_EQ(delegate, controller_);
completion_was_called = YES;
}];
EXPECT_TRUE(completion_was_called);
}
// Tests that the "Suggest a password" suggestion is returned if the form is
// eligible for generation.
TEST_F(SharedPasswordControllerTest,
ReturnsGenerateSuggestionIfFormIsEligibleForGeneration) {
FormSuggestionProviderQuery* form_query = [[FormSuggestionProviderQuery alloc]
initWithFormName:@"form"
formRendererID:autofill::FormRendererId(0)
fieldIdentifier:@"field"
fieldRendererID:autofill::FieldRendererId(1)
fieldType:kObfuscatedFieldType // Ensures this is a password form.
type:@"focus"
typedValue:@""
frameID:kTestFrameID];
const std::string web_frame_id = SysNSStringToUTF8(kTestFrameID);
auto web_frame =
web::FakeWebFrame::Create(web_frame_id,
/*is_main_frame=*/false, GURL(kTestURL));
web::WebFrame* frame = web_frame.get();
AddWebFrame(std::move(web_frame));
OCMExpect([suggestion_helper_ retrieveSuggestionsWithForm:form_query])
.andReturn(@[]);
OCMExpect([[suggestion_helper_ ignoringNonObjectArgs]
isPasswordFieldOnForm:form_query
webFrame:frame]);
autofill::PasswordFormGenerationData form_generation_data = {
form_query.formRendererID, form_query.fieldRendererID,
form_query.fieldRendererID};
[controller_ formEligibleForGenerationFound:form_generation_data];
__block BOOL completion_was_called = NO;
OCMExpect([driver_helper_ PasswordManagerDriver:frame]);
EXPECT_CALL(password_generation_helper_, IsGenerationEnabled(true))
.WillOnce(Return(true));
[controller_
retrieveSuggestionsForForm:form_query
webState:&web_state_
completionHandler:^(NSArray<FormSuggestion*>* suggestions,
id<FormSuggestionProvider> delegate) {
ASSERT_EQ(1UL, suggestions.count);
FormSuggestion* suggestion = suggestions.firstObject;
EXPECT_EQ(autofill::SuggestionType::kGeneratePasswordEntry,
suggestion.type);
EXPECT_EQ(delegate, controller_);
completion_was_called = YES;
}];
EXPECT_TRUE(completion_was_called);
}
// Tests that accepting a "Suggest a password" suggestion will give a suggested
// password to the delegate.
TEST_F(SharedPasswordControllerTest, SuggestsGeneratedPassword) {
GURL origin("https://www.google.com/login");
const uint64_t max_length = 10;
FormData form_data;
form_data.set_url(origin);
form_data.set_action(origin);
form_data.set_name(u"login_form");
form_data.set_renderer_id(autofill::test::MakeFormRendererId());
autofill::FormFieldData field;
field.set_name(u"Username");
field.set_id_attribute(field.name());
field.set_name_attribute(field.name());
field.set_value(u"googleuser");
field.set_form_control_type(autofill::FormControlType::kInputText);
field.set_renderer_id(autofill::test::MakeFieldRendererId());
test_api(form_data).Append(field);
field.set_name(u"Passwd");
field.set_id_attribute(field.name());
field.set_name_attribute(field.name());
field.set_value(u"p4ssword");
field.set_form_control_type(autofill::FormControlType::kInputPassword);
field.set_renderer_id(autofill::test::MakeFieldRendererId());
field.set_max_length(max_length);
test_api(form_data).Append(field);
autofill::FormFieldData password_field_data = form_data.fields().back();
autofill::FormRendererId form_id = form_data.renderer_id();
autofill::FieldRendererId field_id = password_field_data.renderer_id();
autofill::PasswordFormGenerationData form_generation_data = {
form_id, field_id,
/*confirmation_field=*/autofill::FieldRendererId(0)};
[controller_ formEligibleForGenerationFound:form_generation_data];
FormSuggestion* suggestion = [FormSuggestion
suggestionWithValue:@"test-value"
displayDescription:@"test-description"
icon:nil
type:autofill::SuggestionType::kGeneratePasswordEntry
backendIdentifier:nil
requiresReauth:NO];
[[delegate_ expect] sharedPasswordController:controller_
showGeneratedPotentialPassword:[OCMArg isNotNil]
proactive:NO
decisionHandler:[OCMArg any]];
EXPECT_CALL(password_manager_, SetGenerationElementAndTypeForForm);
auto web_frame =
web::FakeWebFrame::Create(SysNSStringToUTF8(kTestFrameID),
/*is_main_frame=*/false, GURL(kTestURL));
web::FakeWebFrame* frame = web_frame.get();
AddWebFrame(std::move(web_frame));
id extract_completion_handler_arg =
[OCMArg checkWithBlock:^(void (^completion_handler)(BOOL, FormData)) {
completion_handler(/*found=*/YES, form_data);
return YES;
}];
[[form_helper_ expect]
extractPasswordFormData:form_id
inFrame:frame
completionHandler:extract_completion_handler_arg];
// Verify the parameters that |GeneratePassword| receives.
autofill::FormSignature form_signature =
autofill::CalculateFormSignature(form_data);
autofill::FieldSignature field_signature =
autofill::CalculateFieldSignatureForField(password_field_data);
OCMExpect([driver_helper_ PasswordManagerDriver:frame]);
EXPECT_CALL(password_generation_helper_,
GeneratePassword(web_state_.GetLastCommittedURL(),
PasswordGenerationType::kAutomatic,
form_signature, field_signature, max_length));
[controller_ didSelectSuggestion:suggestion
atIndex:0
form:@"test-form-name"
formRendererID:form_id
fieldIdentifier:@"test-field-id"
fieldRendererID:field_id
frameID:kTestFrameID
completionHandler:nil];
[delegate_ verify];
}
// Tests that generated passwords are presaved.
TEST_F(SharedPasswordControllerTest, PresavesGeneratedPassword) {
base::HistogramTester histogram_tester;
autofill::FormRendererId form_id(0);
autofill::FieldRendererId field_id(1);
autofill::PasswordFormGenerationData form_generation_data = {
form_id, field_id, field_id};
[controller_ formEligibleForGenerationFound:form_generation_data];
web_state_.SetCurrentURL(GURL(kTestURL));
web_state_.SetContentIsHTML(true);
auto web_frame =
web::FakeWebFrame::Create(SysNSStringToUTF8(kTestFrameID),
/*is_main_frame=*/true, GURL(kTestURL));
web::FakeWebFrame* frame = web_frame.get();
AddWebFrame(std::move(web_frame));
FormSuggestion* suggestion = [FormSuggestion
suggestionWithValue:@"test-value"
displayDescription:@"test-description"
icon:nil
type:autofill::SuggestionType::kGeneratePasswordEntry
backendIdentifier:nil
requiresReauth:NO];
id decision_handler_arg =
[OCMArg checkWithBlock:^(void (^decision_handler)(BOOL)) {
decision_handler(/*accept=*/YES);
return YES;
}];
[[delegate_ expect] sharedPasswordController:controller_
showGeneratedPotentialPassword:[OCMArg isNotNil]
proactive:NO
decisionHandler:decision_handler_arg];
id fill_completion_handler_arg =
[OCMArg checkWithBlock:^(void (^completion_handler)(BOOL)) {
completion_handler(/*success=*/YES);
return YES;
}];
[[form_helper_ expect] fillPasswordForm:form_id
inFrame:frame
newPasswordIdentifier:field_id
confirmPasswordIdentifier:field_id
generatedPassword:[OCMArg isNotNil]
completionHandler:fill_completion_handler_arg];
FormData form_data = test_helpers::MakeSimpleFormData();
id extract_completion_handler_arg =
[OCMArg checkWithBlock:^(void (^completion_handler)(BOOL, FormData)) {
completion_handler(/*found=*/YES, form_data);
return YES;
}];
[[form_helper_ expect]
extractPasswordFormData:form_id
inFrame:frame
completionHandler:extract_completion_handler_arg];
OCMStub([driver_helper_ PasswordManagerDriver:frame]);
EXPECT_CALL(password_generation_helper_, GeneratePassword);
EXPECT_CALL(password_manager_, OnPresaveGeneratedPassword);
EXPECT_CALL(password_manager_, SetGenerationElementAndTypeForForm);
[controller_ didSelectSuggestion:suggestion
atIndex:0
form:@"test-form-name"
formRendererID:form_id
fieldIdentifier:@"test-field-id"
fieldRendererID:field_id
frameID:kTestFrameID
completionHandler:nil];
[delegate_ verify];
histogram_tester.ExpectUniqueSample(
"PasswordGeneration.Event",
autofill::password_generation::PASSWORD_ACCEPTED, 1);
histogram_tester.ExpectUniqueSample(
"PasswordGeneration.iOS.AcceptedGeneratedPasswordSource",
AcceptedGeneratedPasswordSourceType::kSuggestion, 1);
}
// Tests that triggering password generation on the last focused field triggers
// the generation flow.
TEST_F(SharedPasswordControllerTest, TriggerPasswordGeneration) {
base::HistogramTester histogram_tester;
autofill::FormActivityParams params;
params.form_renderer_id = autofill::FormRendererId(0);
params.field_type = "password";
params.field_renderer_id = autofill::FieldRendererId(1);
params.type = "focus";
params.input_missing = false;
auto web_frame = web::FakeWebFrame::Create("frame-id", /*is_main_frame=*/true,
GURL(kTestURL));
web::FakeWebFrame* frame = web_frame.get();
AddWebFrame(std::move(web_frame));
[controller_ webState:&web_state_
didRegisterFormActivity:params
inFrame:frame];
[[delegate_ expect] sharedPasswordController:controller_
showGeneratedPotentialPassword:[OCMArg isNotNil]
proactive:NO
decisionHandler:[OCMArg any]];
EXPECT_CALL(password_manager_, SetGenerationElementAndTypeForForm);
FormData form_data = test_helpers::MakeSimpleFormData();
id extract_completion_handler_arg =
[OCMArg checkWithBlock:^(void (^completion_handler)(BOOL, FormData)) {
completion_handler(/*found=*/YES, form_data);
return YES;
}];
[[form_helper_ expect]
extractPasswordFormData:params.form_renderer_id
inFrame:frame
completionHandler:extract_completion_handler_arg];
OCMExpect([driver_helper_ PasswordManagerDriver:frame]);
EXPECT_CALL(password_generation_helper_, GeneratePassword);
[controller_ triggerPasswordGeneration];
[delegate_ verify];
histogram_tester.ExpectUniqueSample(
"PasswordGeneration.Event",
autofill::password_generation::PASSWORD_GENERATION_CONTEXT_MENU_PRESSED,
1);
}
// Tests that triggering password generation on the last focused field does not
// trigger the generation flow if the last reported form activity did not
// provide valid form and field identifiers.
TEST_F(SharedPasswordControllerTest, LastFocusedFieldData) {
autofill::FormActivityParams params;
params.form_renderer_id = autofill::FormRendererId(0);
params.field_type = "password";
params.field_renderer_id = autofill::FieldRendererId(1);
params.type = "focus";
params.input_missing = true;
auto web_frame = web::FakeWebFrame::Create("frame-id", /*is_main_frame=*/true,
GURL(kTestURL));
[controller_ webState:&web_state_
didRegisterFormActivity:params
inFrame:web_frame.get()];
[[delegate_ reject] sharedPasswordController:controller_
showGeneratedPotentialPassword:[OCMArg isNotNil]
proactive:NO
decisionHandler:[OCMArg any]];
[controller_ triggerPasswordGeneration];
[delegate_ verify];
}
// Tests that detecting element additions (form_changed events) is not
// interpreted as form submissions.
TEST_F(SharedPasswordControllerTest,
DoesntInterpretElementAdditionsAsFormSubmission) {
id mock_completion_handler = [OCMArg checkWithBlock:^(void (
^completionHandler)(const std::vector<FormData>& forms, uint32_t maxID)) {
EXPECT_CALL(password_manager_, OnPasswordFormsParsed);
// OnPasswordFormsRendered is responsible for detecting submissions.
// Making sure it's not called after element additions.
EXPECT_CALL(password_manager_, OnPasswordFormsRendered).Times(0);
FormData form_data = test_helpers::MakeSimpleFormData();
completionHandler({form_data}, 1);
return YES;
}];
auto web_frame = web::FakeWebFrame::Create("frame-id", /*is_main_frame=*/true,
GURL(kTestURL));
web::FakeWebFrame* frame = web_frame.get();
OCMExpect([driver_helper_ PasswordManagerDriver:frame]);
AddWebFrame(std::move(web_frame));
OCMExpect([form_helper_ findPasswordFormsInFrame:frame
completionHandler:mock_completion_handler]);
autofill::FormActivityParams params;
params.type = "form_changed";
[controller_ webState:&web_state_
didRegisterFormActivity:params
inFrame:frame];
[suggestion_helper_ verify];
[form_helper_ verify];
}
class SharedPasswordControllerTestWithRealSuggestionHelper
: public PlatformTest {
public:
SharedPasswordControllerTestWithRealSuggestionHelper() : PlatformTest() {
delegate_ = OCMProtocolMock(@protocol(SharedPasswordControllerDelegate));
password_manager::PasswordManagerClient* client_ptr =
&password_manager_client_;
[[[delegate_ stub] andReturnValue:OCMOCK_VALUE(client_ptr)]
passwordManagerClient];
suggestion_helper_ =
[[PasswordSuggestionHelper alloc] initWithWebState:&web_state_];
form_helper_ = OCMStrictClassMock([PasswordFormHelper class]);
OCMExpect([form_helper_ setDelegate:[OCMArg any]]);
auto web_frames_manager = std::make_unique<web::FakeWebFramesManager>();
web_frames_manager_ = web_frames_manager.get();
web::ContentWorld content_world =
PasswordManagerJavaScriptFeature::GetInstance()
->GetSupportedContentWorld();
web_state_.SetWebFramesManager(content_world,
std::move(web_frames_manager));
PasswordControllerDriverHelper* driver_helper =
[[PasswordControllerDriverHelper alloc] initWithWebState:&web_state_];
controller_ =
[[SharedPasswordController alloc] initWithWebState:&web_state_
manager:&password_manager_
formHelper:form_helper_
suggestionHelper:suggestion_helper_
driverHelper:driver_helper];
[form_helper_ verify];
controller_.delegate = delegate_;
AutofillDriverIOSFactory::CreateForWebState(
&web_state_, &autofill_client_, /*bridge=*/nil, /*locale=*/"en");
web_state_.SetCurrentURL(GURL(kTestURL));
}
void SetUp() override {
PlatformTest::SetUp();
EXPECT_CALL(password_manager_, GetClient)
.WillRepeatedly(Return(&password_manager_client_));
}
void AddWebFrame(std::unique_ptr<web::WebFrame> frame) {
web_frames_manager_->AddWebFrame(std::move(frame));
}
protected:
base::test::TaskEnvironment task_environment_;
autofill::TestAutofillClient autofill_client_;
web::FakeWebState web_state_;
raw_ptr<web::FakeWebFramesManager> web_frames_manager_;
testing::StrictMock<MockPasswordManager> password_manager_;
PasswordSuggestionHelper* suggestion_helper_;
id form_helper_;
id delegate_;
password_manager::StubPasswordManagerClient password_manager_client_;
SharedPasswordController* controller_;
};
// Tests the completion handler for suggestions availability is not called
// until password manager replies with suggestions.
TEST_F(SharedPasswordControllerTestWithRealSuggestionHelper,
WaitForPasswordmanagerResponseToShowSuggestions) {
// Simulate that the form is parsed and sent to PasswordManager.
FormData form = test_helpers::MakeSimpleFormData();
const std::string web_frame_id = SysNSStringToUTF8(kTestFrameID);
auto web_frame =
web::FakeWebFrame::Create(web_frame_id,
/*is_main_frame=*/true, GURL(kTestURL));
web::WebFrame* frame = web_frame.get();
[[form_helper_ expect] findPasswordFormsInFrame:frame
completionHandler:[OCMArg any]];
AddWebFrame(std::move(web_frame));
EXPECT_CALL(password_manager_, OnPasswordFormsParsed);
EXPECT_CALL(password_manager_, OnPasswordFormsRendered);
[controller_ didFinishPasswordFormExtraction:{form}
triggeredByFormChange:false
inFrame:frame];
// Simulate user focusing the field in a form before the password store
// response is received.
FormSuggestionProviderQuery* form_query = [[FormSuggestionProviderQuery alloc]
initWithFormName:SysUTF16ToNSString(form.name())
formRendererID:form.renderer_id()
fieldIdentifier:SysUTF16ToNSString(form.fields()[0].name())
fieldRendererID:form.fields()[0].renderer_id()
fieldType:@"text"
type:@"focus"
typedValue:@""
frameID:kTestFrameID];
[[form_helper_ expect] findPasswordFormsInFrame:frame
completionHandler:[OCMArg any]];
__block BOOL completion_was_called = NO;
[controller_ checkIfSuggestionsAvailableForForm:form_query
hasUserGesture:NO
webState:&web_state_
completionHandler:^(BOOL suggestionsAvailable) {
completion_was_called = YES;
}];
// Check that completion handler wasn't called.
EXPECT_FALSE(completion_was_called);
// Receive suggestions from PasswordManager.
PasswordFormFillData form_fill_data;
test_helpers::SetPasswordFormFillData(
form.url().spec(), "", form.renderer_id().value(), "",
form.fields()[0].renderer_id().value(), "[email protected]", "",
form.fields()[1].renderer_id().value(), "super!secret", nullptr, nullptr,
&form_fill_data);
[controller_ processPasswordFormFillData:form_fill_data
forFrameId:web_frame_id
isMainFrame:frame->IsMainFrame()
forSecurityOrigin:frame->GetSecurityOrigin()];
// Check that completion handler was called.
EXPECT_TRUE(completion_was_called);
}
// Tests the completion handler for suggestions availability is not called
// until password manager replies with suggestions.
TEST_F(SharedPasswordControllerTestWithRealSuggestionHelper,
WaitForPasswordManagerResponseToShowSuggestionsTwoFields) {
// Simulate that the form is parsed and sent to PasswordManager.
FormData form = test_helpers::MakeSimpleFormData();
const std::string web_frame_id = SysNSStringToUTF8(kTestFrameID);
auto web_frame =
web::FakeWebFrame::Create(web_frame_id,
/*is_main_frame=*/true, GURL(kTestURL));
web::WebFrame* frame = web_frame.get();
[[form_helper_ expect] findPasswordFormsInFrame:frame
completionHandler:[OCMArg any]];
AddWebFrame(std::move(web_frame));
EXPECT_CALL(password_manager_, OnPasswordFormsParsed);
EXPECT_CALL(password_manager_, OnPasswordFormsRendered);
[controller_ didFinishPasswordFormExtraction:{form}
triggeredByFormChange:false
inFrame:frame];
// Simulate user focusing the field in a form before the password store
// response is received.
FormSuggestionProviderQuery* form_query1 =
[[FormSuggestionProviderQuery alloc]
initWithFormName:SysUTF16ToNSString(form.name())
formRendererID:form.renderer_id()
fieldIdentifier:SysUTF16ToNSString(form.fields()[0].name())
fieldRendererID:form.fields()[0].renderer_id()
fieldType:@"text"
type:@"focus"
typedValue:@""
frameID:kTestFrameID];
[[form_helper_ expect] findPasswordFormsInFrame:frame
completionHandler:[OCMArg any]];
__block BOOL completion_was_called1 = NO;
[controller_ checkIfSuggestionsAvailableForForm:form_query1
hasUserGesture:NO
webState:&web_state_
completionHandler:^(BOOL suggestionsAvailable) {
completion_was_called1 = YES;
}];
// Check that completion handler wasn't called.
EXPECT_FALSE(completion_was_called1);
// Simulate user focusing another field in a form before the password store
// response is received.
FormSuggestionProviderQuery* form_query2 =
[[FormSuggestionProviderQuery alloc]
initWithFormName:SysUTF16ToNSString(form.name())
formRendererID:form.renderer_id()
fieldIdentifier:SysUTF16ToNSString(form.fields()[1].name())
fieldRendererID:form.fields()[1].renderer_id()
fieldType:kObfuscatedFieldType
type:@"focus"
typedValue:@""
frameID:kTestFrameID];
[[form_helper_ expect] findPasswordFormsInFrame:frame
completionHandler:[OCMArg any]];
__block BOOL completion_was_called2 = NO;
[controller_ checkIfSuggestionsAvailableForForm:form_query2
hasUserGesture:NO
webState:&web_state_
completionHandler:^(BOOL suggestionsAvailable) {
completion_was_called2 = YES;
}];
// Check that completion handler wasn't called yet, not until processing fill
// data.
EXPECT_FALSE(completion_was_called1);
EXPECT_FALSE(completion_was_called2);
// Receive suggestions from PasswordManager.
PasswordFormFillData form_fill_data;
test_helpers::SetPasswordFormFillData(
form.url().spec(), "", form.renderer_id().value(), "",
form.fields()[0].renderer_id().value(), "[email protected]", "",
form.fields()[1].renderer_id().value(), "super!secret", nullptr, nullptr,
&form_fill_data);
[controller_ processPasswordFormFillData:form_fill_data
forFrameId:web_frame_id
isMainFrame:frame->IsMainFrame()
forSecurityOrigin:frame->GetSecurityOrigin()];
// Check that completion handlers were called.
EXPECT_TRUE(completion_was_called1);
EXPECT_TRUE(completion_was_called2);
}
// Test that the password suggestions for cross-origin iframes have the origin
// as their description.
TEST_F(SharedPasswordControllerTestWithRealSuggestionHelper,
CrossOriginIframeSugesstionHasOriginAsDescription) {
// Simulate that the form is parsed and sent to PasswordManager.
FormData form = test_helpers::MakeSimpleFormData();
web_state_.SetCurrentURL(GURL());
web_state_.SetContentIsHTML(true);
const std::string web_frame_id = SysNSStringToUTF8(kTestFrameID);
auto web_frame =
web::FakeWebFrame::Create(web_frame_id,
/*is_main_frame=*/false, GURL(kTestURL));
web::WebFrame* frame = web_frame.get();
[[form_helper_ expect] findPasswordFormsInFrame:frame
completionHandler:[OCMArg any]];
AddWebFrame(std::move(web_frame));
ASSERT_TRUE(IsCrossOriginIframe(&web_state_, frame->IsMainFrame(),
frame->GetSecurityOrigin()));
PasswordFormFillData form_fill_data;
test_helpers::SetPasswordFormFillData(
kTestURL, "", form.renderer_id().value(), "",
form.fields()[0].renderer_id().value(), "[email protected]", "",
form.fields()[1].renderer_id().value(), "super!secret", nullptr, nullptr,
&form_fill_data);
[controller_ processPasswordFormFillData:form_fill_data
forFrameId:web_frame_id
isMainFrame:frame->IsMainFrame()
forSecurityOrigin:frame->GetSecurityOrigin()];
FormSuggestionProviderQuery* form_query = [[FormSuggestionProviderQuery alloc]
initWithFormName:@"form"
formRendererID:autofill::FormRendererId(0)
fieldIdentifier:@"field"
fieldRendererID:autofill::FieldRendererId(1)
fieldType:kObfuscatedFieldType
type:@"focus"
typedValue:@""
frameID:kTestFrameID];
[controller_
retrieveSuggestionsForForm:form_query
webState:&web_state_
completionHandler:^(NSArray<FormSuggestion*>* suggestions,
id<FormSuggestionProvider> delegate) {
// Assert that kTestURL contains [suggestions[0]
// displayDescription].
ASSERT_NE(kTestURL.find(SysNSStringToUTF8(
[suggestions[0] displayDescription])),
std::string::npos);
}];
}
// Tests that attachListenersForBottomSheet, from the
// PasswordSuggestionHelperDelegate protocol, is properly used by the
// PasswordSuggestionHelper object.
TEST_F(SharedPasswordControllerTestWithRealSuggestionHelper,
AttachListenersForBottomSheet) {
// Simulate that the form is parsed and sent to PasswordManager.
FormData form = test_helpers::MakeSimpleFormData();
const std::string web_frame_id = SysNSStringToUTF8(kTestFrameID);
auto web_frame = web::FakeWebFrame::Create(
web_frame_id, /*is_main_frame=*/true, GURL(kTestURL));
web::WebFrame* frame = web_frame.get();
[[form_helper_ expect] findPasswordFormsInFrame:frame
completionHandler:[OCMArg any]];
AddWebFrame(std::move(web_frame));
EXPECT_CALL(password_manager_, OnPasswordFormsParsed);
EXPECT_CALL(password_manager_, OnPasswordFormsRendered);
[controller_ didFinishPasswordFormExtraction:{form}
triggeredByFormChange:false
inFrame:frame];
// Receive suggestions from PasswordManager.
PasswordFormFillData form_fill_data;
test_helpers::SetPasswordFormFillData(
form.url().spec(), "", form.renderer_id().value(), "",
form.fields()[0].renderer_id().value(), "[email protected]", "",
form.fields()[1].renderer_id().value(), "super!secret", nullptr, nullptr,
&form_fill_data);
std::vector<autofill::FieldRendererId> rendererIds;
OCMExpect([[delegate_ ignoringNonObjectArgs]
attachListenersForBottomSheet:rendererIds
forFrameId:""])
.andCompareStringAtIndex(web_frame_id, 1);
[controller_ processPasswordFormFillData:form_fill_data
forFrameId:web_frame_id
isMainFrame:frame->IsMainFrame()
forSecurityOrigin:frame->GetSecurityOrigin()];
[delegate_ verify];
}
// Tests frameDidBecomeAvailable supports cross-origin iframes.
TEST_F(SharedPasswordControllerTest,
FrameDidBecomeAvailableCrossOriginIframe) {
web_state_.SetCurrentURL(GURL());
web_state_.SetContentIsHTML(true);
auto web_frame =
web::FakeWebFrame::Create(SysNSStringToUTF8(kTestFrameID),
/*is_main_frame=*/false, GURL(kTestURL));
web::WebFrame* frame = web_frame.get();
[[form_helper_ expect] findPasswordFormsInFrame:frame
completionHandler:[OCMArg any]];
web_frames_manager_->AddWebFrame(std::move(web_frame));
ASSERT_TRUE(IsCrossOriginIframe(&web_state_, frame->IsMainFrame(),
frame->GetSecurityOrigin()));
[form_helper_ verify];
}
// Tests frameWillBecomeUnavailable supports cross-origin iframes.
TEST_F(SharedPasswordControllerTest,
FrameWillBecomeUnavailableCrossOriginIframe) {
web_state_.SetCurrentURL(GURL());
web_state_.SetContentIsHTML(true);
auto web_frame =
web::FakeWebFrame::Create(SysNSStringToUTF8(kTestFrameID),
/*is_main_frame=*/false, GURL(kTestURL));
web::WebFrame* frame = web_frame.get();
AddWebFrame(std::move(web_frame));
ASSERT_TRUE(IsCrossOriginIframe(&web_state_, frame->IsMainFrame(),
frame->GetSecurityOrigin()));
// OCMExpect([driver_helper_ PasswordManagerDriver:frame]);
EXPECT_CALL(password_manager_, OnIframeDetach).Times(1);
web_frames_manager_->RemoveWebFrame(frame->GetFrameId());
}
// Tests checkIfSuggestionsAvailableForForm supports cross-origin iframes.
TEST_F(SharedPasswordControllerTest,
CheckIfSuggestionsAvailableForFormCrossOriginIframe) {
web_state_.SetCurrentURL(GURL());
web_state_.SetContentIsHTML(true);
auto web_frame =
web::FakeWebFrame::Create(SysNSStringToUTF8(kTestFrameID),
/*is_main_frame=*/false, GURL(kTestURL));
web::WebFrame* frame = web_frame.get();
AddWebFrame(std::move(web_frame));
ASSERT_TRUE(IsCrossOriginIframe(&web_state_, frame->IsMainFrame(),
frame->GetSecurityOrigin()));
FormSuggestionProviderQuery* form_query = [[FormSuggestionProviderQuery alloc]
initWithFormName:@"form"
formRendererID:autofill::FormRendererId(0)
fieldIdentifier:@"field"
fieldRendererID:autofill::FieldRendererId(1)
fieldType:@"text"
type:@"focus"
typedValue:@""
frameID:kTestFrameID];
id mock_completion_handler =
[OCMArg checkWithBlock:^BOOL(void (^completionHandler)(BOOL)) {
completionHandler(YES);
return YES;
}];
[[suggestion_helper_ expect]
checkIfSuggestionsAvailableForForm:form_query
completionHandler:mock_completion_handler];
[[suggestion_helper_ expect] isPasswordFieldOnForm:form_query webFrame:frame];
[controller_ checkIfSuggestionsAvailableForForm:form_query
hasUserGesture:NO
webState:&web_state_
completionHandler:^(BOOL suggestionsAvailable) {
EXPECT_TRUE(suggestionsAvailable);
}];
}
// Tests retrieveSuggestionsForForm supports cross-origin iframes.
TEST_F(SharedPasswordControllerTest,
RetrieveSuggestionsForFormCrossOriginIframe) {
web_state_.SetCurrentURL(GURL());
web_state_.SetContentIsHTML(true);
const std::string web_frame_id = SysNSStringToUTF8(kTestFrameID);
auto web_frame =
web::FakeWebFrame::Create(web_frame_id,
/*is_main_frame=*/false, GURL(kTestURL));
web::WebFrame* frame = web_frame.get();
AddWebFrame(std::move(web_frame));
ASSERT_TRUE(IsCrossOriginIframe(&web_state_, frame->IsMainFrame(),
frame->GetSecurityOrigin()));
FormSuggestionProviderQuery* form_query = [[FormSuggestionProviderQuery alloc]
initWithFormName:@"form"
formRendererID:autofill::FormRendererId(0)
fieldIdentifier:@"field"
fieldRendererID:autofill::FieldRendererId(1)
fieldType:@"text"
type:@"focus"
typedValue:@""
frameID:kTestFrameID];
OCMExpect([suggestion_helper_ retrieveSuggestionsWithForm:form_query])
.andReturn(@[]);
OCMExpect([[suggestion_helper_ ignoringNonObjectArgs]
isPasswordFieldOnForm:form_query
webFrame:frame]);
EXPECT_CALL(password_generation_helper_, IsGenerationEnabled(true))
.WillOnce(Return(false));
[controller_
retrieveSuggestionsForForm:form_query
webState:&web_state_
completionHandler:^(NSArray<FormSuggestion*>* suggestions,
id<FormSuggestionProvider> delegate){
}];
[suggestion_helper_ verify];
}
// Tests formHelper didSubmitForm supports cross-origin iframes.
TEST_F(SharedPasswordControllerTest,
FormHelperDidSubmitFormForFormCrossOriginIframe) {
web_state_.SetCurrentURL(GURL());
web_state_.SetContentIsHTML(true);
auto web_frame =
web::FakeWebFrame::Create(SysNSStringToUTF8(kTestFrameID),
/*is_main_frame=*/false, GURL(kTestURL));
web::WebFrame* frame = web_frame.get();
AddWebFrame(std::move(web_frame));
ASSERT_TRUE(IsCrossOriginIframe(&web_state_, frame->IsMainFrame(),
frame->GetSecurityOrigin()));
OCMExpect([driver_helper_ PasswordManagerDriver:frame]);
EXPECT_CALL(password_manager_, OnSubframeFormSubmission).Times(1);
FormData form_data;
[controller_ formHelper:form_helper_ didSubmitForm:form_data inFrame:frame];
}
// Tests didRegisterFormActivity supports cross-origin iframes.
TEST_F(SharedPasswordControllerTest,
DidRegisterFormActivityForFormCrossOriginIframe) {
web_state_.SetCurrentURL(GURL());
web_state_.SetContentIsHTML(true);
auto web_frame =
web::FakeWebFrame::Create(SysNSStringToUTF8(kTestFrameID),
/*is_main_frame=*/false, GURL(kTestURL));
web::WebFrame* frame = web_frame.get();
ASSERT_TRUE(IsCrossOriginIframe(&web_state_, frame->IsMainFrame(),
frame->GetSecurityOrigin()));
OCMExpect([form_helper_ findPasswordFormsInFrame:frame
completionHandler:[OCMArg any]]);
web_frames_manager_->AddWebFrame(std::move(web_frame));
autofill::FormActivityParams params;
params.type = "form_changed";
OCMExpect([form_helper_ findPasswordFormsInFrame:frame
completionHandler:[OCMArg any]]);
[controller_ webState:&web_state_
didRegisterFormActivity:params
inFrame:frame];
[form_helper_ verify];
}
// Tests didRegisterFormRemoval supports cross-origin iframes.
TEST_F(SharedPasswordControllerTest,
DidRegisterFormRemovalForFormCrossOriginIframe) {
web_state_.SetCurrentURL(GURL());
web_state_.SetContentIsHTML(true);
auto web_frame =
web::FakeWebFrame::Create(SysNSStringToUTF8(kTestFrameID),
/*is_main_frame=*/false, GURL(kTestURL));
web::WebFrame* frame = web_frame.get();
AddWebFrame(std::move(web_frame));
ASSERT_TRUE(IsCrossOriginIframe(&web_state_, frame->IsMainFrame(),
frame->GetSecurityOrigin()));
OCMExpect([driver_helper_ PasswordManagerDriver:frame]);
EXPECT_CALL(password_manager_, OnPasswordFormsRemoved).Times(1);
autofill::FormRemovalParams params;
params.removed_forms = {autofill::FormRendererId()};
[controller_ webState:&web_state_
didRegisterFormRemoval:params
inFrame:frame];
}
// Tests that password generation is terminated correctly when the
// user declines the dialog.
TEST_F(SharedPasswordControllerTest, DeclinePasswordGenerationDialog) {
autofill::FormRendererId form_id(0);
autofill::FieldRendererId field_id(1);
autofill::PasswordFormGenerationData form_generation_data = {
form_id, field_id, field_id};
[controller_ formEligibleForGenerationFound:form_generation_data];
web_state_.SetCurrentURL(GURL(kTestURL));
web_state_.SetContentIsHTML(true);
auto web_frame =
web::FakeWebFrame::Create(SysNSStringToUTF8(kTestFrameID),
/*is_main_frame=*/true, GURL(kTestURL));
web::FakeWebFrame* frame = web_frame.get();
AddWebFrame(std::move(web_frame));
// Create a password generation suggestion.
FormSuggestion* suggestion = [FormSuggestion
suggestionWithValue:@"test-value"
displayDescription:@"test-description"
icon:nil
type:autofill::SuggestionType::kGeneratePasswordEntry
backendIdentifier:nil
requiresReauth:NO];
// Triggering password generation will trigger a new form extraction.
// Simulate it completes successfully.
FormData form_data = test_helpers::MakeSimpleFormData();
id extract_completion_handler_arg =
[OCMArg checkWithBlock:^(void (^completion_handler)(BOOL, FormData)) {
completion_handler(/*found=*/YES, form_data);
return YES;
}];
[[form_helper_ expect]
extractPasswordFormData:form_id
inFrame:frame
completionHandler:extract_completion_handler_arg];
// Simulate the user declining the generated password in the dialog.
id decision_handler_arg =
[OCMArg checkWithBlock:^(void (^decision_handler)(BOOL)) {
decision_handler(/*accept=*/NO);
return YES;
}];
[[delegate_ expect] sharedPasswordController:controller_
showGeneratedPotentialPassword:[OCMArg isNotNil]
proactive:NO
decisionHandler:decision_handler_arg];
OCMStub([driver_helper_ PasswordManagerDriver:frame]);
EXPECT_CALL(password_generation_helper_, GeneratePassword);
EXPECT_CALL(password_manager_, SetGenerationElementAndTypeForForm);
// Check that the generation is terminated.
EXPECT_CALL(password_manager_, OnPasswordNoLongerGenerated);
[controller_ didSelectSuggestion:suggestion
atIndex:0
form:@"test-form-name"
formRendererID:form_id
fieldIdentifier:@"test-field-id"
fieldRendererID:field_id
frameID:kTestFrameID
completionHandler:nil];
}
// Tests that upon calling DidFillField() on the agent, the delegate implemented
// and owned by the SharedPasswordController correctly calls the password
// manager to update its state.
TEST_F(SharedPasswordControllerTest, DidFillField) {
GURL url("https://example.com");
auto frame = web::FakeWebFrame::Create("frameID", true, url);
autofill::FormRendererId form_id(1);
autofill::FieldRendererId field_id(2);
const std::u16string value(u"value");
auto* driver = IOSPasswordManagerDriverFactory::FromWebStateAndWebFrame(
&web_state_, frame.get());
EXPECT_CALL(password_manager_,
UpdateStateOnUserInput(
driver, std::make_optional<FormRendererId>(form_id), field_id,
value));
auto* agent = autofill::PasswordAutofillAgent::FromWebState(&web_state_);
agent->DidFillField(frame.get(), form_id, field_id, value);
}
// TODO(crbug.com/40701292): Finish unit testing the rest of the public API.
} // namespace password_manager