blob: eeb6b3e98409cd1babb16a8e4109929f4c4ce24f [file] [log] [blame]
// Copyright 2019 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.
#include <memory>
#include <string>
#include "base/test/bind.h"
#include "base/test/metrics/histogram_tester.h"
#include "components/ukm/test_ukm_recorder.h"
#include "content/browser/browser_main_loop.h"
#include "content/browser/sms/sms_fetcher_impl.h"
#include "content/browser/sms/test/mock_sms_provider.h"
#include "content/browser/sms/test/mock_sms_web_contents_delegate.h"
#include "content/browser/sms/webotp_service.h"
#include "content/browser/web_contents/web_contents_impl.h"
#include "content/public/browser/sms_fetcher.h"
#include "content/public/common/content_switches.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
#include "content/public/test/content_browser_test.h"
#include "content/public/test/content_browser_test_utils.h"
#include "content/public/test/content_mock_cert_verifier.h"
#include "content/shell/browser/shell.h"
#include "content/test/content_browser_test_utils_internal.h"
#include "net/cert/mock_cert_verifier.h"
#include "net/dns/mock_host_resolver.h"
#include "net/test/embedded_test_server/default_handlers.h"
#include "services/metrics/public/cpp/ukm_builders.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "third_party/blink/public/common/sms/webotp_service_outcome.h"
using blink::mojom::SmsStatus;
using ::testing::_;
using ::testing::Invoke;
using ::testing::NiceMock;
using ::testing::Return;
namespace content {
using UserConsent = SmsFetcher::UserConsent;
namespace {
class SmsBrowserTest : public ContentBrowserTest {
public:
using Entry = ukm::builders::SMSReceiver;
using FailureType = SmsFetchFailureType;
SmsBrowserTest() = default;
~SmsBrowserTest() override = default;
void SetUpCommandLine(base::CommandLine* command_line) override {
ContentBrowserTest::SetUpCommandLine(command_line);
command_line->AppendSwitch(
switches::kEnableExperimentalWebPlatformFeatures);
command_line->AppendSwitchASCII(switches::kWebOtpBackend,
switches::kWebOtpBackendSmsVerification);
cert_verifier_.SetUpCommandLine(command_line);
}
void ExpectOutcomeUKM(const GURL& url, blink::WebOTPServiceOutcome outcome) {
auto entries = ukm_recorder()->GetEntriesByName(Entry::kEntryName);
if (entries.empty())
FAIL() << "No WebOTPServiceOutcome was recorded";
for (const auto* const entry : entries) {
const int64_t* metric = ukm_recorder()->GetEntryMetric(entry, "Outcome");
if (metric && *metric == static_cast<int>(outcome)) {
SUCCEED();
return;
}
}
FAIL() << "Expected WebOTPServiceOutcome was not recorded";
}
void ExpectOutcomeWithCrossOriginUKM(blink::WebOTPServiceOutcome outcome,
bool expect_cross_origin) {
auto entries = ukm_recorder()->GetEntriesByName(Entry::kEntryName);
if (entries.empty())
FAIL() << "No WebOTPServiceOutcome was recorded";
for (const auto* const entry : entries) {
const int64_t* metric = ukm_recorder()->GetEntryMetric(entry, "Outcome");
if (metric && *metric == static_cast<int>(outcome)) {
bool actual_cross_origin =
*ukm_recorder()->GetEntryMetric(entry, "IsCrossOriginFrame");
if (actual_cross_origin == expect_cross_origin) {
SUCCEED();
return;
}
}
}
FAIL() << "Expected Outcome with cross-origin info was not recorded";
}
void ExpectTimingUKM(const std::string& metric_name) {
auto entries = ukm_recorder()->GetEntriesByName(Entry::kEntryName);
ASSERT_FALSE(entries.empty());
for (const auto* const entry : entries) {
if (ukm_recorder()->GetEntryMetric(entry, metric_name)) {
SUCCEED();
return;
}
}
FAIL() << "Expected UKM was not recorded";
}
void ExpectNoOutcomeUKM() {
EXPECT_TRUE(ukm_recorder()->GetEntriesByName(Entry::kEntryName).empty());
}
void ExpectSmsParsingStatusMetrics(
const base::HistogramTester& histogram_tester,
int status) {
histogram_tester.ExpectBucketCount("Blink.Sms.Receive.SmsParsingStatus",
status, 1);
auto entries = ukm_recorder()->GetEntriesByName(Entry::kEntryName);
ASSERT_FALSE(entries.empty());
for (const auto* const entry : entries) {
const int64_t* metric =
ukm_recorder()->GetEntryMetric(entry, "SmsParsingStatus");
if (metric && *metric == status) {
SUCCEED();
return;
}
}
FAIL() << "Expected UKM was not recorded";
}
void ExpectSmsPrompt() {
EXPECT_CALL(delegate_, CreateSmsPrompt(_, _, _, _, _))
.WillOnce(Invoke([&](RenderFrameHost*, const OriginList&,
const std::string&, base::OnceClosure on_confirm,
base::OnceClosure on_cancel) {
confirm_callback_ = std::move(on_confirm);
dismiss_callback_ = std::move(on_cancel);
}));
}
void ConfirmPrompt() {
if (!confirm_callback_.is_null()) {
std::move(confirm_callback_).Run();
dismiss_callback_.Reset();
return;
}
FAIL() << "SmsInfobar not available";
}
void DismissPrompt() {
if (dismiss_callback_.is_null()) {
FAIL() << "SmsInfobar not available";
}
std::move(dismiss_callback_).Run();
confirm_callback_.Reset();
}
protected:
void SetUpOnMainThread() override {
ContentBrowserTest::TearDownOnMainThread();
host_resolver()->AddRule("*", "127.0.0.1");
https_server_.ServeFilesFromSourceDirectory(GetTestDataFilePath());
SetupCrossSiteRedirector(&https_server_);
net::test_server::RegisterDefaultHandlers(&https_server_);
ASSERT_TRUE(https_server_.Start());
IsolateAllSitesForTesting(base::CommandLine::ForCurrentProcess());
cert_verifier_.mock_cert_verifier()->set_default_result(net::OK);
ukm_recorder_ = std::make_unique<ukm::TestAutoSetUkmRecorder>();
}
void SetUpInProcessBrowserTestFixture() override {
cert_verifier_.SetUpInProcessBrowserTestFixture();
}
void TearDownInProcessBrowserTestFixture() override {
cert_verifier_.TearDownInProcessBrowserTestFixture();
}
SmsFetcher* GetSmsFetcher() {
return SmsFetcher::Get(shell()->web_contents()->GetBrowserContext());
}
ukm::TestAutoSetUkmRecorder* ukm_recorder() { return ukm_recorder_.get(); }
NiceMock<MockSmsWebContentsDelegate> delegate_;
// Similar to net::MockCertVerifier, but also updates the CertVerifier
// used by the NetworkService.
ContentMockCertVerifier cert_verifier_;
std::unique_ptr<ukm::TestAutoSetUkmRecorder> ukm_recorder_;
base::OnceClosure confirm_callback_;
base::OnceClosure dismiss_callback_;
FrameTreeVisualizer visualizer_;
net::EmbeddedTestServer https_server_{net::EmbeddedTestServer::TYPE_HTTPS};
};
} // namespace
IN_PROC_BROWSER_TEST_F(SmsBrowserTest, Receive) {
base::HistogramTester histogram_tester;
GURL url = GetTestUrl(nullptr, "simple_page.html");
EXPECT_TRUE(NavigateToURL(shell(), url));
shell()->web_contents()->SetDelegate(&delegate_);
ExpectSmsPrompt();
auto provider = std::make_unique<MockSmsProvider>();
MockSmsProvider* mock_provider_ptr = provider.get();
BrowserMainLoop::GetInstance()->SetSmsProviderForTesting(std::move(provider));
// Test that SMS content can be retrieved after navigator.credentials.get().
std::string script = R"(
(async () => {
let cred = await navigator.credentials.get({otp: {transport: ["sms"]}});
return cred.code;
}) ();
)";
EXPECT_CALL(*mock_provider_ptr, Retrieve(_, _)).WillOnce(Invoke([&]() {
mock_provider_ptr->NotifyReceive(OriginList{url::Origin::Create(url)},
"hello", UserConsent::kNotObtained);
ConfirmPrompt();
}));
// Wait for UKM to be recorded to avoid race condition between outcome
// capture and evaluation.
base::RunLoop ukm_loop;
ukm_recorder()->SetOnAddEntryCallback(Entry::kEntryName,
ukm_loop.QuitClosure());
EXPECT_EQ("hello", EvalJs(shell(), script));
ukm_loop.Run();
ASSERT_FALSE(GetSmsFetcher()->HasSubscribers());
content::FetchHistogramsFromChildProcesses();
ExpectOutcomeUKM(url, blink::WebOTPServiceOutcome::kSuccess);
ExpectTimingUKM("TimeSmsReceiveMs");
ExpectTimingUKM("TimeSuccessMs");
histogram_tester.ExpectTotalCount("Blink.Sms.Receive.TimeSuccess", 1);
}
IN_PROC_BROWSER_TEST_F(SmsBrowserTest, AtMostOneSmsRequestPerOrigin) {
GURL url = GetTestUrl(nullptr, "simple_page.html");
EXPECT_TRUE(NavigateToURL(shell(), url));
shell()->web_contents()->SetDelegate(&delegate_);
ExpectSmsPrompt();
auto provider = std::make_unique<MockSmsProvider>();
MockSmsProvider* mock_provider_ptr = provider.get();
BrowserMainLoop::GetInstance()->SetSmsProviderForTesting(std::move(provider));
std::string script = R"(
(async () => {
let firstRequest = navigator.credentials.get({otp: {transport: ["sms"]}})
.catch(({name}) => {
return name;
});
let secondRequest = navigator.credentials.get({otp: {transport: ["sms"]}})
.then(({code}) => {
return code;
});
return Promise.all([firstRequest, secondRequest]);
}) ();
)";
EXPECT_CALL(*mock_provider_ptr, Retrieve(_, _))
.WillOnce(Return())
.WillOnce(Invoke([&]() {
mock_provider_ptr->NotifyReceive(OriginList{url::Origin::Create(url)},
"hello", UserConsent::kNotObtained);
ConfirmPrompt();
}));
// Wait for UKM to be recorded to avoid race condition between outcome
// capture and evaluation.
base::RunLoop ukm_loop1;
ukm_recorder()->SetOnAddEntryCallback(Entry::kEntryName,
ukm_loop1.QuitClosure());
EXPECT_EQ(ListValueOf("AbortError", "hello"), EvalJs(shell(), script));
ukm_loop1.Run();
ASSERT_FALSE(GetSmsFetcher()->HasSubscribers());
ExpectOutcomeUKM(url, blink::WebOTPServiceOutcome::kSuccess);
}
// Disabled test: https://crbug.com/1052385
IN_PROC_BROWSER_TEST_F(SmsBrowserTest,
DISABLED_AtMostOneSmsRequestPerOriginPerTab) {
auto provider = std::make_unique<MockSmsProvider>();
MockSmsProvider* mock_provider_ptr = provider.get();
BrowserMainLoop::GetInstance()->SetSmsProviderForTesting(std::move(provider));
Shell* tab1 = CreateBrowser();
Shell* tab2 = CreateBrowser();
GURL url = GetTestUrl(nullptr, "simple_page.html");
EXPECT_TRUE(NavigateToURL(tab1, url));
EXPECT_TRUE(NavigateToURL(tab2, url));
tab1->web_contents()->SetDelegate(&delegate_);
tab2->web_contents()->SetDelegate(&delegate_);
EXPECT_CALL(*mock_provider_ptr, Retrieve(_, _)).Times(3);
// Make 1 request on tab1 that is expected to be cancelled when the 2nd
// request is made.
EXPECT_TRUE(ExecJs(tab1, R"(
var firstRequest = navigator.credentials.get({otp: {transport: ["sms"]}})
.catch(({name}) => {
return name;
});
)"));
// Make 1 request on tab2 to verify requests on tab1 have no affect on tab2.
EXPECT_TRUE(ExecJs(tab2, R"(
var request = navigator.credentials.get({otp: {transport: ["sms"]}})
.then(({code}) => {
return code;
});
)"));
// Make a 2nd request on tab1 to verify the 1st request gets cancelled when
// the 2nd request is made.
EXPECT_TRUE(ExecJs(tab1, R"(
var secondRequest = navigator.credentials
.get({otp: {transport: ["sms"]}})
.then(({code}) => {
return code;
});
)"));
EXPECT_EQ("AbortError", EvalJs(tab1, "firstRequest"));
ASSERT_TRUE(GetSmsFetcher()->HasSubscribers());
{
base::RunLoop ukm_loop;
ExpectSmsPrompt();
// Wait for UKM to be recorded to avoid race condition between outcome
// capture and evaluation.
ukm_recorder()->SetOnAddEntryCallback(Entry::kEntryName,
ukm_loop.QuitClosure());
mock_provider_ptr->NotifyReceive(OriginList{url::Origin::Create(url)},
"hello1", UserConsent::kNotObtained);
ConfirmPrompt();
ukm_loop.Run();
}
EXPECT_EQ("hello1", EvalJs(tab2, "request"));
ASSERT_TRUE(GetSmsFetcher()->HasSubscribers());
ExpectOutcomeUKM(url, blink::WebOTPServiceOutcome::kSuccess);
ukm_recorder()->Purge();
{
base::RunLoop ukm_loop;
ExpectSmsPrompt();
// Wait for UKM to be recorded to avoid race condition between outcome
// capture and evaluation.
ukm_recorder()->SetOnAddEntryCallback(Entry::kEntryName,
ukm_loop.QuitClosure());
mock_provider_ptr->NotifyReceive(OriginList{url::Origin::Create(url)},
"hello2", UserConsent::kNotObtained);
ConfirmPrompt();
ukm_loop.Run();
}
EXPECT_EQ("hello2", EvalJs(tab1, "secondRequest"));
ASSERT_FALSE(GetSmsFetcher()->HasSubscribers());
ExpectOutcomeUKM(url, blink::WebOTPServiceOutcome::kSuccess);
}
IN_PROC_BROWSER_TEST_F(SmsBrowserTest, Reload) {
GURL url = GetTestUrl(nullptr, "simple_page.html");
EXPECT_TRUE(NavigateToURL(shell(), url));
auto provider = std::make_unique<MockSmsProvider>();
MockSmsProvider* mock_provider_ptr = provider.get();
BrowserMainLoop::GetInstance()->SetSmsProviderForTesting(std::move(provider));
base::RunLoop loop;
EXPECT_CALL(*mock_provider_ptr, Retrieve(_, _)).WillOnce(Invoke([&loop]() {
// Deliberately avoid calling NotifyReceive() to simulate
// a request that has been received but not fulfilled.
loop.Quit();
}));
ExecuteScriptAsync(shell(), R"(
navigator.credentials.get({otp: {transport: ["sms"]}});
)");
loop.Run();
ASSERT_TRUE(GetSmsFetcher()->HasSubscribers());
ASSERT_TRUE(mock_provider_ptr->HasObservers());
// Reload the page. This destroys the ExecutionContext and resets any HeapMojo
// connections.
EXPECT_TRUE(NavigateToURL(shell(), url));
ASSERT_FALSE(GetSmsFetcher()->HasSubscribers());
ExpectOutcomeUKM(url, blink::WebOTPServiceOutcome::kUnhandledRequest);
}
IN_PROC_BROWSER_TEST_F(SmsBrowserTest, Close) {
GURL url = GetTestUrl(nullptr, "simple_page.html");
EXPECT_TRUE(NavigateToURL(shell(), url));
auto provider = std::make_unique<MockSmsProvider>();
MockSmsProvider* mock_provider_ptr = provider.get();
BrowserMainLoop::GetInstance()->SetSmsProviderForTesting(std::move(provider));
base::RunLoop loop;
EXPECT_CALL(*mock_provider_ptr, Retrieve(_, _)).WillOnce(Invoke([&loop]() {
loop.Quit();
}));
ExecuteScriptAsync(shell(), R"(
navigator.credentials.get({otp: {transport: ["sms"]}});
)");
loop.Run();
ASSERT_TRUE(mock_provider_ptr->HasObservers());
auto* fetcher = GetSmsFetcher();
shell()->Close();
ASSERT_FALSE(fetcher->HasSubscribers());
ExpectOutcomeUKM(url, blink::WebOTPServiceOutcome::kUnhandledRequest);
}
// Disabled test: https://crbug.com/1052385
IN_PROC_BROWSER_TEST_F(SmsBrowserTest, DISABLED_TwoTabsSameOrigin) {
auto provider = std::make_unique<MockSmsProvider>();
MockSmsProvider* mock_provider_ptr = provider.get();
BrowserMainLoop::GetInstance()->SetSmsProviderForTesting(std::move(provider));
Shell* tab1 = CreateBrowser();
Shell* tab2 = CreateBrowser();
GURL url = GetTestUrl(nullptr, "simple_page.html");
EXPECT_TRUE(NavigateToURL(tab1, url));
EXPECT_TRUE(NavigateToURL(tab2, url));
tab1->web_contents()->SetDelegate(&delegate_);
tab2->web_contents()->SetDelegate(&delegate_);
EXPECT_CALL(*mock_provider_ptr, Retrieve(_, _)).Times(2);
std::string script = R"(
var otp = navigator.credentials.get({otp: {transport: ["sms"]}})
.then(({code}) => {
return code;
});
)";
// First tab registers an observer.
EXPECT_TRUE(ExecJs(tab1, script));
// Second tab registers an observer.
EXPECT_TRUE(ExecJs(tab2, script));
ASSERT_TRUE(mock_provider_ptr->HasObservers());
{
base::RunLoop ukm_loop;
ExpectSmsPrompt();
// Wait for UKM to be recorded to avoid race condition between outcome
// capture and evaluation.
ukm_recorder()->SetOnAddEntryCallback(Entry::kEntryName,
ukm_loop.QuitClosure());
mock_provider_ptr->NotifyReceive(OriginList{url::Origin::Create(url)},
"hello1", UserConsent::kNotObtained);
ConfirmPrompt();
ukm_loop.Run();
}
EXPECT_EQ("hello1", EvalJs(tab1, "otp"));
ASSERT_TRUE(mock_provider_ptr->HasObservers());
ExpectOutcomeUKM(url, blink::WebOTPServiceOutcome::kSuccess);
ukm_recorder()->Purge();
{
base::RunLoop ukm_loop;
ExpectSmsPrompt();
// Wait for UKM to be recorded to avoid race condition between outcome
// capture and evaluation.
ukm_recorder()->SetOnAddEntryCallback(Entry::kEntryName,
ukm_loop.QuitClosure());
mock_provider_ptr->NotifyReceive(OriginList{url::Origin::Create(url)},
"hello2", UserConsent::kNotObtained);
ConfirmPrompt();
ukm_loop.Run();
}
EXPECT_EQ("hello2", EvalJs(tab2, "otp"));
ASSERT_FALSE(GetSmsFetcher()->HasSubscribers());
ExpectOutcomeUKM(url, blink::WebOTPServiceOutcome::kSuccess);
}
// Disabled test: https://crbug.com/1052385
IN_PROC_BROWSER_TEST_F(SmsBrowserTest, DISABLED_TwoTabsDifferentOrigin) {
auto provider = std::make_unique<MockSmsProvider>();
MockSmsProvider* mock_provider_ptr = provider.get();
BrowserMainLoop::GetInstance()->SetSmsProviderForTesting(std::move(provider));
Shell* tab1 = CreateBrowser();
Shell* tab2 = CreateBrowser();
net::EmbeddedTestServer https_server(net::EmbeddedTestServer::TYPE_HTTPS);
https_server.ServeFilesFromSourceDirectory(GetTestDataFilePath());
ASSERT_TRUE(https_server.Start());
GURL url1 = https_server.GetURL("a.com", "/simple_page.html");
GURL url2 = https_server.GetURL("b.com", "/simple_page.html");
EXPECT_TRUE(NavigateToURL(tab1, url1));
EXPECT_TRUE(NavigateToURL(tab2, url2));
std::string script = R"(
var otp = navigator.credentials.get({otp: {transport: ["sms"]}})
.then(({code}) => {
return code;
});
)";
base::RunLoop loop;
EXPECT_CALL(*mock_provider_ptr, Retrieve(_, _)).Times(2);
tab1->web_contents()->SetDelegate(&delegate_);
tab2->web_contents()->SetDelegate(&delegate_);
EXPECT_TRUE(ExecJs(tab1, script));
EXPECT_TRUE(ExecJs(tab2, script));
ASSERT_TRUE(mock_provider_ptr->HasObservers());
{
base::RunLoop ukm_loop;
ExpectSmsPrompt();
// Wait for UKM to be recorded to avoid race condition between outcome
// capture and evaluation.
ukm_recorder()->SetOnAddEntryCallback(Entry::kEntryName,
ukm_loop.QuitClosure());
mock_provider_ptr->NotifyReceive(OriginList{url::Origin::Create(url1)},
"hello1", UserConsent::kNotObtained);
ConfirmPrompt();
ukm_loop.Run();
}
EXPECT_EQ("hello1", EvalJs(tab1, "otp"));
ASSERT_TRUE(mock_provider_ptr->HasObservers());
{
base::RunLoop ukm_loop;
ExpectSmsPrompt();
// Wait for UKM to be recorded to avoid race condition between outcome
// capture and evaluation.
ukm_recorder()->SetOnAddEntryCallback(Entry::kEntryName,
ukm_loop.QuitClosure());
mock_provider_ptr->NotifyReceive(OriginList{url::Origin::Create(url2)},
"hello2", UserConsent::kNotObtained);
ConfirmPrompt();
ukm_loop.Run();
}
EXPECT_EQ("hello2", EvalJs(tab2, "otp"));
ASSERT_FALSE(GetSmsFetcher()->HasSubscribers());
ExpectOutcomeUKM(url1, blink::WebOTPServiceOutcome::kSuccess);
ExpectOutcomeUKM(url2, blink::WebOTPServiceOutcome::kSuccess);
}
IN_PROC_BROWSER_TEST_F(SmsBrowserTest, SmsReceivedAfterTabIsClosed) {
GURL url = GetTestUrl(nullptr, "simple_page.html");
EXPECT_TRUE(NavigateToURL(shell(), url));
auto provider = std::make_unique<MockSmsProvider>();
MockSmsProvider* mock_provider_ptr = provider.get();
BrowserMainLoop::GetInstance()->SetSmsProviderForTesting(std::move(provider));
base::RunLoop loop;
EXPECT_CALL(*mock_provider_ptr, Retrieve(_, _)).WillOnce(Invoke([&loop]() {
loop.Quit();
}));
ExecuteScriptAsync(shell(), R"(
// kicks off an sms receiver call, but deliberately leaves it hanging.
navigator.credentials.get({otp: {transport: ["sms"]}});
)");
loop.Run();
shell()->Close();
mock_provider_ptr->NotifyReceive(OriginList{url::Origin::Create(url)},
"hello", UserConsent::kObtained);
ExpectOutcomeUKM(url, blink::WebOTPServiceOutcome::kUnhandledRequest);
}
IN_PROC_BROWSER_TEST_F(SmsBrowserTest, AbortAfterSmsRetrieval) {
GURL url = GetTestUrl(nullptr, "simple_page.html");
EXPECT_TRUE(NavigateToURL(shell(), url));
auto provider = std::make_unique<MockSmsProvider>();
MockSmsProvider* mock_provider_ptr = provider.get();
BrowserMainLoop::GetInstance()->SetSmsProviderForTesting(std::move(provider));
shell()->web_contents()->SetDelegate(&delegate_);
ExpectSmsPrompt();
EXPECT_CALL(*mock_provider_ptr, Retrieve(_, _))
.WillOnce(Invoke([&mock_provider_ptr, &url]() {
mock_provider_ptr->NotifyReceive(OriginList{url::Origin::Create(url)},
"hello", UserConsent::kNotObtained);
}));
EXPECT_TRUE(ExecJs(shell(), R"(
var controller = new AbortController();
var signal = controller.signal;
var request = navigator.credentials
.get({otp: {transport: ["sms"]}, signal: signal})
.catch(({name}) => {
return name;
});
)"));
base::RunLoop ukm_loop;
// Wait for UKM to be recorded to avoid race condition between outcome
// capture and evaluation.
ukm_recorder()->SetOnAddEntryCallback(Entry::kEntryName,
ukm_loop.QuitClosure());
EXPECT_TRUE(ExecJs(shell(), R"( controller.abort(); )"));
ukm_loop.Run();
EXPECT_EQ("AbortError", EvalJs(shell(), "request"));
ExpectOutcomeUKM(url, blink::WebOTPServiceOutcome::kAborted);
}
IN_PROC_BROWSER_TEST_F(SmsBrowserTest, SmsFetcherUAF) {
base::CommandLine::ForCurrentProcess()->AppendSwitchASCII(
switches::kWebOtpBackend, switches::kWebOtpBackendUserConsent);
GURL url = GetTestUrl(nullptr, "simple_page.html");
EXPECT_TRUE(NavigateToURL(shell(), url));
auto* provider = new NiceMock<MockSmsProvider>();
BrowserMainLoop::GetInstance()->SetSmsProviderForTesting(
base::WrapUnique(provider));
shell()->web_contents()->SetDelegate(&delegate_);
auto* fetcher = SmsFetcher::Get(shell()->web_contents()->GetBrowserContext());
auto* fetcher2 =
SmsFetcher::Get(shell()->web_contents()->GetBrowserContext());
mojo::Remote<blink::mojom::WebOTPService> service;
mojo::Remote<blink::mojom::WebOTPService> service2;
RenderFrameHost* render_frame_host = shell()->web_contents()->GetMainFrame();
EXPECT_TRUE(WebOTPService::Create(fetcher, render_frame_host,
service.BindNewPipeAndPassReceiver()));
EXPECT_TRUE(WebOTPService::Create(fetcher2, render_frame_host,
service2.BindNewPipeAndPassReceiver()));
base::RunLoop navigate;
EXPECT_CALL(*provider, Retrieve(_, _))
.WillOnce(Invoke([&]() {
static_cast<SmsFetcherImpl*>(fetcher)->OnReceive(
OriginList{url::Origin::Create(url)}, "ABC234",
UserConsent::kObtained);
}))
.WillOnce(Invoke([&]() {
static_cast<SmsFetcherImpl*>(fetcher2)->OnReceive(
OriginList{url::Origin::Create(url)}, "DEF567",
UserConsent::kObtained);
}));
service->Receive(base::BindLambdaForTesting(
[](SmsStatus status, const absl::optional<std::string>& otp) {
EXPECT_EQ(SmsStatus::kSuccess, status);
EXPECT_EQ("ABC234", otp);
}));
service2->Receive(base::BindLambdaForTesting(
[&navigate](SmsStatus status, const absl::optional<std::string>& otp) {
EXPECT_EQ(SmsStatus::kSuccess, status);
EXPECT_EQ("DEF567", otp);
navigate.Quit();
}));
navigate.Run();
}
IN_PROC_BROWSER_TEST_F(SmsBrowserTest, ReportWebOTPInUseCounter) {
GURL url = GetTestUrl(nullptr, "simple_page.html");
EXPECT_TRUE(NavigateToURL(shell(), url));
shell()->web_contents()->SetDelegate(&delegate_);
ExpectSmsPrompt();
auto provider = std::make_unique<MockSmsProvider>();
MockSmsProvider* mock_provider_ptr = provider.get();
BrowserMainLoop::GetInstance()->SetSmsProviderForTesting(std::move(provider));
EXPECT_CALL(*mock_provider_ptr, Retrieve(_, _)).WillOnce(Invoke([&]() {
mock_provider_ptr->NotifyReceive(OriginList{url::Origin::Create(url)},
"hello", UserConsent::kNotObtained);
ConfirmPrompt();
}));
base::HistogramTester histogram_tester;
std::string script = R"(
(async () => {
let cred = await navigator.credentials.get({otp: {transport: ["sms"]}});
return cred.code;
}) ();
)";
EXPECT_EQ("hello", EvalJs(shell(), script));
content::FetchHistogramsFromChildProcesses();
histogram_tester.ExpectBucketCount("Blink.UseCounter.Features",
blink::mojom::WebFeature::kWebOTP, 1);
}
IN_PROC_BROWSER_TEST_F(SmsBrowserTest, UpdateRenderFrameHostWithWebOTPUsage) {
base::HistogramTester histogram_tester;
GURL url = GetTestUrl(nullptr, "simple_page.html");
EXPECT_TRUE(NavigateToURL(shell(), url));
shell()->web_contents()->SetDelegate(&delegate_);
ExpectSmsPrompt();
auto provider = std::make_unique<MockSmsProvider>();
MockSmsProvider* mock_provider_ptr = provider.get();
BrowserMainLoop::GetInstance()->SetSmsProviderForTesting(std::move(provider));
EXPECT_CALL(*mock_provider_ptr, Retrieve(_, _)).WillOnce(Invoke([&]() {
mock_provider_ptr->NotifyReceive(OriginList{url::Origin::Create(url)},
"hello", UserConsent::kNotObtained);
ConfirmPrompt();
}));
RenderFrameHost* render_frame_host = shell()->web_contents()->GetMainFrame();
EXPECT_FALSE(render_frame_host->DocumentUsedWebOTP());
// navigator.credentials.get() creates an WebOTPService which will notify the
// RenderFrameHost that WebOTP has been used.
std::string script = R"(
(async () => {
let cred = await navigator.credentials.get({otp: {transport: ["sms"]}});
return cred.code;
}) ();
)";
EXPECT_EQ("hello", EvalJs(shell(), script));
EXPECT_TRUE(render_frame_host->DocumentUsedWebOTP());
}
IN_PROC_BROWSER_TEST_F(SmsBrowserTest, RecordBackendNotAvailableAsOutcome) {
GURL url = GetTestUrl(nullptr, "simple_page.html");
EXPECT_TRUE(NavigateToURL(shell(), url));
auto provider = std::make_unique<MockSmsProvider>();
MockSmsProvider* mock_provider_ptr = provider.get();
BrowserMainLoop::GetInstance()->SetSmsProviderForTesting(std::move(provider));
shell()->web_contents()->SetDelegate(&delegate_);
EXPECT_CALL(*mock_provider_ptr, Retrieve(_, _))
.WillOnce(Invoke([&mock_provider_ptr]() {
mock_provider_ptr->NotifyFailure(FailureType::kBackendNotAvailable);
}));
base::RunLoop ukm_loop;
// Wait for UKM to be recorded to avoid race condition between outcome
// capture and evaluation.
ukm_recorder()->SetOnAddEntryCallback(Entry::kEntryName,
ukm_loop.QuitClosure());
ExecuteScriptAsync(shell(), R"(
navigator.credentials.get({otp: {transport: ["sms"]}});
)");
ukm_loop.Run();
ExpectOutcomeUKM(url, blink::WebOTPServiceOutcome::kBackendNotAvailable);
}
IN_PROC_BROWSER_TEST_F(SmsBrowserTest,
NotRecordFailureForMultiplePendingOrigins) {
auto provider = std::make_unique<MockSmsProvider>();
MockSmsProvider* mock_provider_ptr = provider.get();
BrowserMainLoop::GetInstance()->SetSmsProviderForTesting(std::move(provider));
Shell* tab1 = CreateBrowser();
Shell* tab2 = CreateBrowser();
net::EmbeddedTestServer https_server(net::EmbeddedTestServer::TYPE_HTTPS);
https_server.ServeFilesFromSourceDirectory(GetTestDataFilePath());
ASSERT_TRUE(https_server.Start());
GURL url1 = https_server.GetURL("a.com", "/simple_page.html");
GURL url2 = https_server.GetURL("b.com", "/simple_page.html");
EXPECT_TRUE(NavigateToURL(tab1, url1));
EXPECT_TRUE(NavigateToURL(tab2, url2));
tab1->web_contents()->SetDelegate(&delegate_);
tab2->web_contents()->SetDelegate(&delegate_);
base::RunLoop loop;
EXPECT_CALL(*mock_provider_ptr, Retrieve(_, _))
.WillOnce(Invoke([]() {
// Leave the first request unhandled to make sure there's a pending
// origin.
}))
.WillOnce(Invoke([&]() {
mock_provider_ptr->NotifyFailure(FailureType::kBackendNotAvailable);
loop.Quit();
}));
std::string script = R"(
navigator.credentials.get({otp: {transport: ["sms"]}})
)";
ExecuteScriptAsync(tab1, script);
ExecuteScriptAsync(tab2, script);
loop.Run();
ExpectNoOutcomeUKM();
}
// Disabled test: https://crbug.com/1134455
IN_PROC_BROWSER_TEST_F(SmsBrowserTest, DISABLED_RecordPendingOriginCount) {
base::HistogramTester histogram_tester;
auto provider = std::make_unique<MockSmsProvider>();
MockSmsProvider* mock_provider_ptr = provider.get();
BrowserMainLoop::GetInstance()->SetSmsProviderForTesting(std::move(provider));
Shell* tab1 = CreateBrowser();
Shell* tab2 = CreateBrowser();
net::EmbeddedTestServer https_server(net::EmbeddedTestServer::TYPE_HTTPS);
https_server.ServeFilesFromSourceDirectory(GetTestDataFilePath());
ASSERT_TRUE(https_server.Start());
GURL url1 = https_server.GetURL("a.com", "/simple_page.html");
GURL url2 = https_server.GetURL("b.com", "/simple_page.html");
EXPECT_TRUE(NavigateToURL(tab1, url1));
EXPECT_TRUE(NavigateToURL(tab2, url2));
EXPECT_CALL(*mock_provider_ptr, Retrieve(_, _)).Times(2);
tab1->web_contents()->SetDelegate(&delegate_);
tab2->web_contents()->SetDelegate(&delegate_);
std::string script = R"(
var request = navigator.credentials.get({otp: {transport: ["sms"]}})
.then(({code}) => {
return code;
});
)";
ExecuteScriptAsync(tab1, script);
ExecuteScriptAsync(tab2, script);
ExpectSmsPrompt();
mock_provider_ptr->NotifyReceive(OriginList{url::Origin::Create(url1)},
"code1", UserConsent::kNotObtained);
ConfirmPrompt();
EXPECT_EQ("code1", EvalJs(tab1, "request"));
ExpectSmsPrompt();
mock_provider_ptr->NotifyReceive(OriginList{url::Origin::Create(url2)},
"code2", UserConsent::kNotObtained);
ConfirmPrompt();
EXPECT_EQ("code2", EvalJs(tab2, "request"));
histogram_tester.ExpectBucketCount("Blink.Sms.PendingOriginCount", 1, 1);
histogram_tester.ExpectBucketCount("Blink.Sms.PendingOriginCount", 2, 1);
}
// Verifies that the metrics are correctly recorded when an invalid SMS cannot
// be parsed.
IN_PROC_BROWSER_TEST_F(SmsBrowserTest, RecordSmsNotParsedMetrics) {
base::HistogramTester histogram_tester;
auto entries =
ukm_recorder_->GetEntriesByName(ukm::builders::SMSReceiver::kEntryName);
ASSERT_TRUE(entries.empty());
GURL url = GetTestUrl(nullptr, "simple_page.html");
EXPECT_TRUE(NavigateToURL(shell(), url));
auto provider = std::make_unique<MockSmsProvider>();
MockSmsProvider* mock_provider_ptr = provider.get();
BrowserMainLoop::GetInstance()->SetSmsProviderForTesting(std::move(provider));
const std::string invalid_sms = "Your OTP is: 1234.\n!example.com #1234";
base::RunLoop loop;
EXPECT_CALL(*mock_provider_ptr, Retrieve(_, _)).WillOnce(Invoke([&]() {
// Calls NotifyReceive with an invalid sms and record sms parse failure
// metrics.
mock_provider_ptr->NotifyReceiveForTesting(invalid_sms,
UserConsent::kObtained);
loop.Quit();
}));
ExecuteScriptAsync(shell(), R"(
navigator.credentials.get({otp: {transport: ["sms"]}});
)");
loop.Run();
ASSERT_TRUE(mock_provider_ptr->HasObservers());
content::FetchHistogramsFromChildProcesses();
ExpectSmsParsingStatusMetrics(
histogram_tester,
static_cast<int>(SmsParser::SmsParsingStatus::kOTPFormatRegexNotMatch));
histogram_tester.ExpectBucketCount(
"Blink.Sms.Receive.SmsParsingStatus",
static_cast<int>(SmsParser::SmsParsingStatus::kParsed), 0);
}
// Verifies that a valid SMS can be parsed and no metrics regarding parsing
// failure should be recorded. Note that the metrics about successful parsing
// are tested separately below.
IN_PROC_BROWSER_TEST_F(SmsBrowserTest, SmsParsed) {
base::HistogramTester histogram_tester;
auto entries =
ukm_recorder_->GetEntriesByName(ukm::builders::SMSReceiver::kEntryName);
ASSERT_TRUE(entries.empty());
GURL url = GetTestUrl(nullptr, "simple_page.html");
EXPECT_TRUE(NavigateToURL(shell(), url));
shell()->web_contents()->SetDelegate(&delegate_);
auto provider = std::make_unique<MockSmsProvider>();
MockSmsProvider* mock_provider_ptr = provider.get();
BrowserMainLoop::GetInstance()->SetSmsProviderForTesting(std::move(provider));
const std::string valid_sms = "Your OTP is: 1234.\n@example.com #1234";
base::RunLoop loop;
EXPECT_CALL(*mock_provider_ptr, Retrieve(_, _)).WillOnce(Invoke([&]() {
mock_provider_ptr->NotifyReceiveForTesting(valid_sms,
UserConsent::kObtained);
loop.Quit();
}));
ExecuteScriptAsync(shell(), R"(
navigator.credentials.get({otp: {transport: ["sms"]}});
)");
loop.Run();
ASSERT_TRUE(GetSmsFetcher()->HasSubscribers());
ASSERT_TRUE(mock_provider_ptr->HasObservers());
histogram_tester.ExpectBucketCount(
"Blink.Sms.Receive.SmsParsingStatus",
static_cast<int>(SmsParser::SmsParsingStatus::kOTPFormatRegexNotMatch),
0);
histogram_tester.ExpectBucketCount(
"Blink.Sms.Receive.SmsParsingStatus",
static_cast<int>(SmsParser::SmsParsingStatus::kHostAndPortNotParsed), 0);
histogram_tester.ExpectBucketCount(
"Blink.Sms.Receive.SmsParsingStatus",
static_cast<int>(SmsParser::SmsParsingStatus::kGURLNotValid), 0);
}
// Verifies that after an SMS is parsed the metrics regarding successful parsing
// are recorded.
IN_PROC_BROWSER_TEST_F(SmsBrowserTest, RecordSmsParsedMetrics) {
base::HistogramTester histogram_tester;
GURL url = GetTestUrl(nullptr, "simple_page.html");
EXPECT_TRUE(NavigateToURL(shell(), url));
shell()->web_contents()->SetDelegate(&delegate_);
ExpectSmsPrompt();
auto provider = std::make_unique<MockSmsProvider>();
MockSmsProvider* mock_provider_ptr = provider.get();
BrowserMainLoop::GetInstance()->SetSmsProviderForTesting(std::move(provider));
EXPECT_CALL(*mock_provider_ptr, Retrieve(_, _)).WillOnce(Invoke([&]() {
// WebOTP does not accept ports in the origin and the test server requires
// ports. Therefore we cannot create an SMS with valid origin from the test.
// Bypassing the issue by calling NotifyReceive directly to test metrics
// recording logic.
mock_provider_ptr->NotifyReceive(OriginList{url::Origin::Create(url)},
"1234", UserConsent::kNotObtained);
ConfirmPrompt();
}));
// Wait for UKM to be recorded to avoid race condition between outcome
// capture and evaluation.
base::RunLoop ukm_loop;
ukm_recorder()->SetOnAddEntryCallback(Entry::kEntryName,
ukm_loop.QuitClosure());
EXPECT_EQ("1234", EvalJs(shell(), R"(
(async () => {
let cred = await navigator.credentials.get({otp: {transport: ["sms"]}});
return cred.code;
}) ();
)"));
ukm_loop.Run();
content::FetchHistogramsFromChildProcesses();
ExpectSmsParsingStatusMetrics(
histogram_tester, static_cast<int>(SmsParser::SmsParsingStatus::kParsed));
histogram_tester.ExpectBucketCount(
"Blink.Sms.Receive.SmsParsingStatus",
static_cast<int>(SmsParser::SmsParsingStatus::kOTPFormatRegexNotMatch),
0);
}
IN_PROC_BROWSER_TEST_F(SmsBrowserTest, TwoUniqueOrigins) {
GURL main_url(
https_server_.GetURL("a.com", "/cross_site_iframe_factory.html?a(b)"));
EXPECT_TRUE(NavigateToURL(shell(), main_url));
FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
->GetFrameTree()
->root();
FrameTreeNode* child = root->child_at(0);
GURL b_url(https_server_.GetURL("b.com", "/title1.html"));
EXPECT_TRUE(NavigateToURLFromRenderer(child, b_url));
EXPECT_EQ(
" Site A ------------ proxies for B\n"
" +--Site B ------- proxies for A\n"
"Where A = https://a.com/\n"
" B = https://b.com/",
visualizer_.DepictFrameTree(root));
auto* fetcher = SmsFetcher::Get(shell()->web_contents()->GetBrowserContext());
mojo::Remote<blink::mojom::WebOTPService> service;
RenderFrameHost* render_frame_host = child->current_frame_host();
EXPECT_TRUE(WebOTPService::Create(fetcher, render_frame_host,
service.BindNewPipeAndPassReceiver()));
}
IN_PROC_BROWSER_TEST_F(SmsBrowserTest, ThreeUniqueOrigins) {
GURL main_url(
https_server_.GetURL("a.com", "/cross_site_iframe_factory.html?a(b(c))"));
EXPECT_TRUE(NavigateToURL(shell(), main_url));
FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
->GetFrameTree()
->root();
FrameTreeNode* child = root->child_at(0);
FrameTreeNode* grand_child = child->child_at(0);
GURL c_url(https_server_.GetURL("c.com", "/title1.html"));
EXPECT_TRUE(NavigateToURLFromRenderer(grand_child, c_url));
EXPECT_EQ(
" Site A ------------ proxies for B C\n"
" +--Site B ------- proxies for A C\n"
" +--Site C -- proxies for A B\n"
"Where A = https://a.com/\n"
" B = https://b.com/\n"
" C = https://c.com/",
visualizer_.DepictFrameTree(root));
auto* fetcher = SmsFetcher::Get(shell()->web_contents()->GetBrowserContext());
mojo::Remote<blink::mojom::WebOTPService> service;
RenderFrameHost* render_frame_host = grand_child->current_frame_host();
EXPECT_FALSE(WebOTPService::Create(fetcher, render_frame_host,
service.BindNewPipeAndPassReceiver()));
}
IN_PROC_BROWSER_TEST_F(SmsBrowserTest, TwoUniqueOriginsConsecutive) {
GURL main_url(
https_server_.GetURL("a.com", "/cross_site_iframe_factory.html?a(b(b))"));
EXPECT_TRUE(NavigateToURL(shell(), main_url));
FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
->GetFrameTree()
->root();
FrameTreeNode* child = root->child_at(0);
FrameTreeNode* grand_child = child->child_at(0);
GURL b_url(https_server_.GetURL("b.com", "/title1.html"));
EXPECT_TRUE(NavigateToURLFromRenderer(grand_child, b_url));
EXPECT_EQ(
" Site A ------------ proxies for B\n"
" +--Site B ------- proxies for A\n"
" +--Site B -- proxies for A\n"
"Where A = https://a.com/\n"
" B = https://b.com/",
visualizer_.DepictFrameTree(root));
auto* fetcher = SmsFetcher::Get(shell()->web_contents()->GetBrowserContext());
mojo::Remote<blink::mojom::WebOTPService> service;
RenderFrameHost* render_frame_host = grand_child->current_frame_host();
EXPECT_TRUE(WebOTPService::Create(fetcher, render_frame_host,
service.BindNewPipeAndPassReceiver()));
}
IN_PROC_BROWSER_TEST_F(SmsBrowserTest, TwoUniqueOriginsInconsecutive) {
GURL main_url(
https_server_.GetURL("a.com", "/cross_site_iframe_factory.html?a(b(a))"));
EXPECT_TRUE(NavigateToURL(shell(), main_url));
FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
->GetFrameTree()
->root();
FrameTreeNode* child = root->child_at(0);
FrameTreeNode* grand_child = child->child_at(0);
GURL a_url(https_server_.GetURL("a.com", "/title1.html"));
EXPECT_TRUE(NavigateToURLFromRenderer(grand_child, a_url));
EXPECT_EQ(
" Site A ------------ proxies for B\n"
" +--Site B ------- proxies for A\n"
" +--Site A -- proxies for B\n"
"Where A = https://a.com/\n"
" B = https://b.com/",
visualizer_.DepictFrameTree(root));
auto* fetcher = SmsFetcher::Get(shell()->web_contents()->GetBrowserContext());
mojo::Remote<blink::mojom::WebOTPService> service;
RenderFrameHost* render_frame_host = grand_child->current_frame_host();
EXPECT_FALSE(WebOTPService::Create(fetcher, render_frame_host,
service.BindNewPipeAndPassReceiver()));
}
IN_PROC_BROWSER_TEST_F(SmsBrowserTest, RecordOutcomeWithCrossOriginFrame) {
GURL main_url(https_server_.GetURL(
"a.com", "/cross_site_iframe_factory.html?a(b{allow-otp-credentials})"));
EXPECT_TRUE(NavigateToURL(shell(), main_url));
FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
->GetFrameTree()
->root();
shell()->web_contents()->SetDelegate(&delegate_);
auto provider = std::make_unique<MockSmsProvider>();
MockSmsProvider* mock_provider_ptr = provider.get();
BrowserMainLoop::GetInstance()->SetSmsProviderForTesting(std::move(provider));
EXPECT_CALL(*mock_provider_ptr, Retrieve(_, _)).WillOnce(Invoke([&]() {
mock_provider_ptr->NotifyFailure(FailureType::kBackendNotAvailable);
}));
// Wait for UKM to be recorded to avoid race condition between outcome
// capture and evaluation.
base::RunLoop ukm_loop;
ukm_recorder()->SetOnAddEntryCallback(Entry::kEntryName,
ukm_loop.QuitClosure());
GURL b_url(https_server_.GetURL("b.com", "/page_with_webotp.html"));
FrameTreeNode* child = root->child_at(0);
EXPECT_TRUE(NavigateToURLFromRenderer(child, b_url));
EXPECT_EQ(
" Site A ------------ proxies for B\n"
" +--Site B ------- proxies for A\n"
"Where A = https://a.com/\n"
" B = https://b.com/",
visualizer_.DepictFrameTree(root));
ukm_loop.Run();
content::FetchHistogramsFromChildProcesses();
ExpectOutcomeWithCrossOriginUKM(
blink::WebOTPServiceOutcome::kBackendNotAvailable,
/* is_cross_origin_frame */ true);
}
IN_PROC_BROWSER_TEST_F(SmsBrowserTest, RecordOutcomeWithSameOriginFrame) {
GURL main_url(
https_server_.GetURL("a.com", "/cross_site_iframe_factory.html?a(a)"));
EXPECT_TRUE(NavigateToURL(shell(), main_url));
FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
->GetFrameTree()
->root();
shell()->web_contents()->SetDelegate(&delegate_);
auto provider = std::make_unique<MockSmsProvider>();
MockSmsProvider* mock_provider_ptr = provider.get();
BrowserMainLoop::GetInstance()->SetSmsProviderForTesting(std::move(provider));
EXPECT_CALL(*mock_provider_ptr, Retrieve(_, _)).WillOnce(Invoke([&]() {
mock_provider_ptr->NotifyFailure(FailureType::kBackendNotAvailable);
}));
// Wait for UKM to be recorded to avoid race condition between outcome
// capture and evaluation.
base::RunLoop ukm_loop;
ukm_recorder()->SetOnAddEntryCallback(Entry::kEntryName,
ukm_loop.QuitClosure());
GURL b_url(https_server_.GetURL("a.com", "/page_with_webotp.html"));
FrameTreeNode* child = root->child_at(0);
EXPECT_TRUE(NavigateToURLFromRenderer(child, b_url));
EXPECT_EQ(
" Site A\n"
" +--Site A\n"
"Where A = https://a.com/",
visualizer_.DepictFrameTree(root));
ukm_loop.Run();
content::FetchHistogramsFromChildProcesses();
ExpectOutcomeWithCrossOriginUKM(
blink::WebOTPServiceOutcome::kBackendNotAvailable,
/* is_cross_origin_frame */ false);
}
} // namespace content