blob: 72b22e2d823b8fd82dbe9b83c3f7a01dd75c2348 [file] [log] [blame]
// Copyright 2025 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include <vector>
#include "base/base64.h"
#include "base/test/run_until.h"
#include "chrome/test/base/chrome_test_utils.h"
#include "chrome/test/base/platform_browser_test.h"
#include "components/autofill/content/browser/content_autofill_driver.h"
#include "components/autofill/content/browser/test_autofill_client_injector.h"
#include "components/autofill/content/browser/test_autofill_manager_injector.h"
#include "components/autofill/content/browser/test_content_autofill_client.h"
#include "components/autofill/core/browser/crowdsourcing/autofill_crowdsourcing_manager.h"
#include "components/autofill/core/browser/foundations/browser_autofill_manager.h"
#include "components/autofill/core/browser/foundations/browser_autofill_manager_test_api.h"
#include "components/autofill/core/browser/foundations/test_autofill_manager_waiter.h"
#include "components/autofill/core/browser/test_utils/autofill_test_utils.h"
#include "components/autofill/core/browser/ui/test_autofill_external_delegate.h"
#include "components/autofill/core/common/signatures.h"
#include "components/one_time_tokens/core/browser/one_time_token.h"
#include "components/one_time_tokens/core/browser/one_time_token_service_impl.h"
#include "components/one_time_tokens/core/browser/sms_otp_backend.h"
#include "content/public/test/browser_test.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
namespace autofill {
namespace {
// This implementation is for testing. It lets us manually control and simulate
// the moment an SMS is received for one-time passwords (OTP).
class FakeSmsOtpBackend : public one_time_tokens::SmsOtpBackend {
public:
using CallbackType =
base::OnceCallback<void(const one_time_tokens::OtpFetchReply&)>;
FakeSmsOtpBackend() = default;
~FakeSmsOtpBackend() override = default;
// one_time_tokens::SmsOtpBackend:
void RetrieveSmsOtp(CallbackType callback) override;
// Simulates the reception of an SMS.
void NotifyCallbacks(const one_time_tokens::OtpFetchReply& reply);
size_t num_callbacks() const { return callbacks_.size(); }
private:
std::vector<CallbackType> callbacks_;
};
void FakeSmsOtpBackend::RetrieveSmsOtp(
FakeSmsOtpBackend::CallbackType callback) {
callbacks_.push_back(std::move(callback));
}
void FakeSmsOtpBackend::NotifyCallbacks(
const one_time_tokens::OtpFetchReply& reply) {
for (auto& callback : callbacks_) {
std::move(callback).Run(reply);
}
callbacks_.clear();
}
// AutofillCrowdsourcingManager that classifies every field as a ONE_TIME_CODE.
class FakeAutofillCrowdsourcingManager : public AutofillCrowdsourcingManager {
public:
FakeAutofillCrowdsourcingManager(AutofillClient* autofill_client,
version_info::Channel channel);
~FakeAutofillCrowdsourcingManager() override = default;
bool StartQueryRequest(
const std::vector<raw_ptr<const FormStructure, VectorExperimental>>&
forms,
std::optional<net::IsolationInfo> isolation_info,
base::OnceCallback<void(std::optional<QueryResponse>)> callback) override;
bool StartUploadRequest(std::vector<AutofillUploadContents> upload_contents,
mojom::SubmissionSource form_submission_source,
bool is_password_manager_upload) override;
};
FakeAutofillCrowdsourcingManager::FakeAutofillCrowdsourcingManager(
AutofillClient* autofill_client,
version_info::Channel channel)
: AutofillCrowdsourcingManager(autofill_client, channel) {}
bool FakeAutofillCrowdsourcingManager::StartQueryRequest(
const std::vector<raw_ptr<const FormStructure, VectorExperimental>>& forms,
std::optional<net::IsolationInfo> isolation_info,
base::OnceCallback<void(std::optional<QueryResponse>)> callback) {
// Generate a response that classifies each field as a ONE_TIME_CODE field.
std::vector<FormSignature> queried_form_signatures;
AutofillQueryResponse response;
for (const FormStructure* form : forms) {
queried_form_signatures.push_back(form->form_signature());
auto* form_suggestion = response.add_form_suggestions();
for (const auto& field : form->fields()) {
auto* field_suggestion = form_suggestion->add_field_suggestions();
field_suggestion->set_field_signature(
CalculateFieldSignatureForField(*field).value());
*field_suggestion->add_predictions() =
test::CreateFieldPrediction(ONE_TIME_CODE, /*is_override=*/false);
}
}
std::move(callback).Run(AutofillCrowdsourcingManager::QueryResponse{
base::Base64Encode(response.SerializeAsString()),
queried_form_signatures});
return true;
}
bool FakeAutofillCrowdsourcingManager::StartUploadRequest(
std::vector<AutofillUploadContents> upload_contents,
mojom::SubmissionSource form_submission_source,
bool is_password_manager_upload) {
// Overridden to prevent network communication from tests.
return true;
}
class OtpTestAutofillManager : public BrowserAutofillManager {
public:
explicit OtpTestAutofillManager(ContentAutofillDriver* driver)
: BrowserAutofillManager(driver) {
test_api(*this).SetExternalDelegate(
std::make_unique<TestAutofillExternalDelegate>(this));
}
~OtpTestAutofillManager() override = default;
[[nodiscard]] testing::AssertionResult WaitForFormsSeen(
int min_num_awaited_calls) {
return forms_seen_waiter_.Wait(min_num_awaited_calls);
}
[[nodiscard]] testing::AssertionResult WaitForSuggestionsShown(
int min_num_awaited_calls) {
return suggestions_shown_waiter_.Wait(min_num_awaited_calls);
}
TestAutofillExternalDelegate& external_delegate() {
return *static_cast<TestAutofillExternalDelegate*>(
test_api(*this).external_delegate());
}
private:
TestAutofillManagerWaiter forms_seen_waiter_{
*this,
{AutofillManagerEvent::kFormsSeen}};
TestAutofillManagerWaiter suggestions_shown_waiter_{
*this,
{AutofillManagerEvent::kAskForValuesToFill}};
};
class OtpTestAutofillClient : public TestContentAutofillClient {
public:
explicit OtpTestAutofillClient(content::WebContents* web_contents)
: TestContentAutofillClient(web_contents) {
set_crowdsourcing_manager(
std::make_unique<FakeAutofillCrowdsourcingManager>(
this, version_info::Channel::STABLE));
set_sms_otp_backend(std::make_unique<FakeSmsOtpBackend>());
set_one_time_token_service(
std::make_unique<one_time_tokens::OneTimeTokenServiceImpl>(
GetSmsOtpBackend()));
}
~OtpTestAutofillClient() override = default;
FakeSmsOtpBackend& sms_otp_backend() {
return *static_cast<FakeSmsOtpBackend*>(GetSmsOtpBackend());
}
};
} // namespace
class OtpManagerBrowserTest : public PlatformBrowserTest {
void SetUpOnMainThread() override;
protected:
content::WebContents* web_contents() {
return chrome_test_utils::GetActiveWebContents(this);
}
OtpTestAutofillManager& autofill_manager() {
return *autofill_manager_injector_[web_contents()];
}
OtpTestAutofillClient& autofill_client() {
return *autofill_client_injector_[web_contents()];
}
// The server communication is mocked out, so we don't need to disable it
// in the test.
test::AutofillBrowserTestEnvironment autofill_test_environment_{
{.disable_server_communication = false}};
TestAutofillClientInjector<OtpTestAutofillClient> autofill_client_injector_;
TestAutofillManagerInjector<OtpTestAutofillManager>
autofill_manager_injector_;
};
void OtpManagerBrowserTest::SetUpOnMainThread() {
PlatformBrowserTest::SetUpOnMainThread();
ASSERT_TRUE(embedded_test_server()->Start());
}
// Test fixture for the interaction between the WebOTP API and Autofill for
// OTPs. If the parameter is true, the WebOTP API should be invoked in the test.
class OtpManagerWithWebOtpApiBrowserTest
: public OtpManagerBrowserTest,
public testing::WithParamInterface<bool> {
protected:
bool is_webotp_api_used() const { return GetParam(); }
};
INSTANTIATE_TEST_SUITE_P(
/* no prefix */,
OtpManagerWithWebOtpApiBrowserTest,
::testing::Bool());
// This integration test ensures that an OTP is correctly suggested by Autofill
// by default (when the WebOTP API is not used) but that an OTP does not get
// autofilled if a website calls the WebOTP API before.
IN_PROC_BROWSER_TEST_P(OtpManagerWithWebOtpApiBrowserTest,
SmsOtpAutofillIntegrationTest) {
GURL url = embedded_test_server()->GetURL("/autofill/sms_otp_form.html");
// Navigate to page and wait for form to be classified
ASSERT_TRUE(chrome_test_utils::NavigateToURL(web_contents(), url));
ASSERT_TRUE(autofill_manager().WaitForFormsSeen(1));
// Immediately when a form field is classified as an OTP field, a subscription
// for OTPs should be started if an SMS OTP backend exists.
ASSERT_EQ(autofill_client().sms_otp_backend().num_callbacks(), 1u);
// Using the WebOTP API should block the Autofill OTP feature.
if (is_webotp_api_used()) {
// Request an OTP via the WebOTP API. The JavaScript code returns true so
// ExecJs does not wait for the resolution of the pending Promise.
ASSERT_TRUE(ExecJs(web_contents(),
R"(navigator.credentials.get({
otp: {transport:['sms']}
});
true;)",
content::EXECUTE_SCRIPT_DEFAULT_OPTIONS,
/*world_id=*/1));
// As the WebOTP is asynchronous, wait until the use has been propagated.
ASSERT_TRUE(base::test::RunUntil(
[&] { return autofill_client().DocumentUsedWebOTP(); }));
// From this point on, Autofill should not suggest OTPs anymore.
}
// Simulate an OTP arriving.
autofill_client().sms_otp_backend().NotifyCallbacks(
one_time_tokens::OtpFetchReply(
one_time_tokens::OneTimeToken(
one_time_tokens::OneTimeTokenType::kSmsOtp, "123456",
base::Time::Now()),
/*request_complete=*/true));
// Simulate click on field.
const std::map<FormGlobalId, std::unique_ptr<FormStructure>>& forms =
autofill_manager().form_structures();
ASSERT_EQ(forms.size(), 1u);
const std::unique_ptr<FormStructure>& form = forms.begin()->second;
const std::unique_ptr<AutofillField>& first_field = *form->fields().begin();
autofill_manager().OnAskForValuesToFill(
form->ToFormData(), first_field->global_id(), gfx::Rect(),
AutofillSuggestionTriggerSource::kFormControlElementClicked,
/*password_request=*/std::nullopt);
ASSERT_TRUE(autofill_manager().WaitForSuggestionsShown(1));
// Verify expectations: The OTP should be suggested by autofill unless the
// WebOTP API was called.
const TestAutofillExternalDelegate& external_delegate =
autofill_manager().external_delegate();
const std::vector<Suggestion>& suggestions = external_delegate.suggestions();
if (is_webotp_api_used()) {
EXPECT_EQ(suggestions.size(), 0u);
} else {
ASSERT_EQ(suggestions.size(), 1u);
EXPECT_EQ(suggestions[0].main_text.value, u"123456");
}
}
} // namespace autofill