| // 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 "content/browser/sms/webotp_service.h" |
| |
| #include <memory> |
| #include <string> |
| #include <utility> |
| |
| #include "base/bind.h" |
| #include "base/callback_helpers.h" |
| #include "base/command_line.h" |
| #include "base/memory/ptr_util.h" |
| #include "base/memory/raw_ptr.h" |
| #include "base/memory/weak_ptr.h" |
| #include "base/run_loop.h" |
| #include "base/task/single_thread_task_runner.h" |
| #include "base/test/bind.h" |
| #include "base/test/metrics/histogram_tester.h" |
| #include "components/ukm/test_ukm_recorder.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/test/mock_user_consent_handler.h" |
| #include "content/browser/sms/user_consent_handler.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/navigation_simulator.h" |
| #include "content/public/test/test_browser_context.h" |
| #include "content/public/test/test_renderer_host.h" |
| #include "mojo/public/cpp/bindings/remote.h" |
| #include "mojo/public/cpp/test_support/test_utils.h" |
| #include "services/metrics/public/cpp/ukm_builders.h" |
| #include "services/service_manager/public/cpp/bind_source_info.h" |
| #include "testing/gmock/include/gmock/gmock.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| #include "third_party/abseil-cpp/absl/types/optional.h" |
| #include "third_party/blink/public/common/sms/webotp_service_outcome.h" |
| #include "third_party/blink/public/mojom/sms/webotp_service.mojom-shared.h" |
| #include "third_party/blink/public/mojom/sms/webotp_service.mojom.h" |
| |
| using absl::optional; |
| using base::BindLambdaForTesting; |
| using blink::mojom::SmsStatus; |
| using blink::mojom::WebOTPService; |
| using std::string; |
| using ::testing::_; |
| using ::testing::ByMove; |
| using ::testing::Invoke; |
| using ::testing::NiceMock; |
| using ::testing::Return; |
| using ::testing::StrictMock; |
| using url::Origin; |
| |
| namespace content { |
| |
| class RenderFrameHost; |
| |
| using Entry = ukm::builders::SMSReceiver; |
| using FailureType = SmsFetchFailureType; |
| using UserConsent = SmsFetcher::UserConsent; |
| |
| namespace { |
| |
| const char kTestUrl[] = "https://www.google.com"; |
| |
| class StubWebContentsDelegate : public WebContentsDelegate {}; |
| |
| // Service encapsulates a WebOTPService endpoint, with all of its dependencies |
| // mocked out (and the common plumbing needed to inject them), and a |
| // mojo::Remote<WebOTPService> endpoint that tests can use to make requests. |
| // It exposes some common methods, like MakeRequest and NotifyReceive, but it |
| // also exposes the low level mocks that enables tests to set expectations and |
| // control the testing environment. |
| class Service { |
| protected: |
| Service(WebContents* web_contents, |
| const Origin& origin, |
| std::unique_ptr<UserConsentHandler> user_consent_handler) |
| : fetcher_(&provider_), |
| consent_handler_(std::move(user_consent_handler)) { |
| // Set a stub delegate because sms service checks existence of delegate and |
| // cancels requests early if one does not exist. |
| web_contents->SetDelegate(&contents_delegate_); |
| |
| // WebOTPService is a DocumentService and normally self-deletes. For the |
| // purposes of the test, `~Service` is responsible for manually cleaning |
| // up `service_`. A normal std::unique_ptr<T> is not allowed here, since a |
| // DocumentService implementation must be deleted by calling one of the |
| // `*AndDeleteThis()` methods. |
| service_ = &WebOTPService::CreateForTesting( |
| &fetcher_, OriginList{origin}, *web_contents->GetPrimaryMainFrame(), |
| service_remote_.BindNewPipeAndPassReceiver()); |
| service_->SetConsentHandlerForTesting(consent_handler_.get()); |
| } |
| |
| public: |
| explicit Service(WebContents* web_contents) |
| : Service(web_contents, |
| web_contents->GetPrimaryMainFrame()->GetLastCommittedOrigin(), |
| /* avoid showing user prompts */ |
| std::make_unique<NoopUserConsentHandler>()) {} |
| |
| ~Service() { |
| // WebOTPService sends IPCs in its destructor, so for the unit test, pretend |
| // that this works. |
| service_->WillBeDestroyed( |
| DocumentServiceDestructionReason::kEndOfDocumentLifetime); |
| service_->ResetAndDeleteThis(); |
| } |
| |
| NiceMock<MockSmsProvider>* provider() { return &provider_; } |
| SmsFetcher* fetcher() { return &fetcher_; } |
| UserConsentHandler* consent_handler() { return consent_handler_.get(); } |
| |
| void MakeRequest(WebOTPService::ReceiveCallback callback) { |
| service_remote_->Receive(std::move(callback)); |
| } |
| |
| void AbortRequest() { service_remote_->Abort(); } |
| |
| void NotifyReceive(const GURL& url, |
| const string& otp, |
| /* avoid showing user prompts */ |
| UserConsent consent_requirement = UserConsent::kObtained) { |
| provider_.NotifyReceive(OriginList{Origin::Create(url)}, otp, |
| consent_requirement); |
| } |
| |
| void NotifyFailure(FailureType failure_type) { |
| service_->OnFailure(failure_type); |
| } |
| |
| void ActivateTimer() { service_->OnTimeout(); } |
| |
| private: |
| StubWebContentsDelegate contents_delegate_; |
| NiceMock<MockSmsProvider> provider_; |
| SmsFetcherImpl fetcher_; |
| std::unique_ptr<UserConsentHandler> consent_handler_; |
| mojo::Remote<blink::mojom::WebOTPService> service_remote_; |
| raw_ptr<WebOTPService> service_; |
| }; |
| |
| class WebOTPServiceTest : public RenderViewHostTestHarness { |
| public: |
| WebOTPServiceTest(const WebOTPServiceTest&) = delete; |
| WebOTPServiceTest& operator=(const WebOTPServiceTest&) = delete; |
| |
| protected: |
| WebOTPServiceTest() { |
| ukm_recorder_ = std::make_unique<ukm::TestAutoSetUkmRecorder>(); |
| } |
| ~WebOTPServiceTest() override = default; |
| |
| const base::HistogramTester& histogram_tester() const { |
| return histogram_tester_; |
| } |
| |
| ukm::TestAutoSetUkmRecorder* ukm_recorder() { return ukm_recorder_.get(); } |
| |
| void ExpectOutcomeUKM(const GURL& url, blink::WebOTPServiceOutcome outcome) { |
| auto entries = ukm_recorder()->GetEntriesByName(Entry::kEntryName); |
| |
| if (entries.empty()) |
| FAIL() << "No WebOTPServiceOutcome was recorded"; |
| |
| // There are non-outcome metrics under the same entry of SMSReceiver UKM. We |
| // need to make sure that the outcome metric only includes the expected one. |
| for (const auto* const entry : entries) { |
| const int64_t* metric = ukm_recorder()->GetEntryMetric(entry, "Outcome"); |
| if (metric && *metric != static_cast<int>(outcome)) |
| FAIL() << "Unexpected outcome was recorded"; |
| } |
| |
| SUCCEED(); |
| } |
| |
| 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 RecordFailureOutcomeWithTimerActivation( |
| FailureType failure_type, |
| blink::WebOTPServiceOutcome expected_outcome) { |
| GURL url = GURL(kTestUrl); |
| NavigateAndCommit(url); |
| |
| Service service(web_contents()); |
| |
| base::RunLoop ukm_loop; |
| ukm_recorder()->SetOnAddEntryCallback(Entry::kEntryName, |
| ukm_loop.QuitClosure()); |
| |
| EXPECT_CALL(*service.provider(), Retrieve(_, _)).WillOnce(Invoke([&]() { |
| service.NotifyFailure(failure_type); |
| // Triggers the timer immediately to emulate the timeout behavior. |
| service.ActivateTimer(); |
| })); |
| |
| service.MakeRequest(base::DoNothing()); |
| |
| ukm_loop.Run(); |
| |
| ExpectOutcomeUKM(url, expected_outcome); |
| ASSERT_FALSE(service.fetcher()->HasSubscribers()); |
| } |
| |
| void NotRecordFailureOutcomeWithoutTimerActivation(FailureType failure_type) { |
| GURL url = GURL(kTestUrl); |
| NavigateAndCommit(url); |
| |
| Service service(web_contents()); |
| |
| base::RunLoop loop; |
| EXPECT_CALL(*service.provider(), Retrieve(_, _)).WillOnce(Invoke([&]() { |
| service.NotifyFailure(failure_type); |
| loop.Quit(); |
| })); |
| |
| service.MakeRequest(base::DoNothing()); |
| |
| loop.Run(); |
| ExpectNoOutcomeUKM(); |
| ASSERT_FALSE(service.fetcher()->HasSubscribers()); |
| } |
| |
| void RecordFailureOutcomeUponPreviousRequestCancelled( |
| FailureType failure_type, |
| blink::WebOTPServiceOutcome expected_outcome) { |
| GURL url = GURL(kTestUrl); |
| NavigateAndCommit(url); |
| |
| Service service(web_contents()); |
| |
| base::RunLoop loop; |
| EXPECT_CALL(*service.provider(), Retrieve(_, _)).WillOnce(Invoke([&]() { |
| service.NotifyFailure(failure_type); |
| loop.Quit(); |
| })); |
| |
| service.MakeRequest(base::DoNothing()); |
| |
| loop.Run(); |
| |
| ::testing::Mock::VerifyAndClear(&service); |
| base::RunLoop loop2; |
| |
| EXPECT_CALL(*service.provider(), Retrieve(_, _)).WillOnce(Invoke([&]() { |
| loop2.Quit(); |
| })); |
| |
| // The second request to the same service cancels the previous outstanding |
| // request. |
| service.MakeRequest(base::DoNothing()); |
| |
| loop2.Run(); |
| |
| ExpectOutcomeUKM(url, expected_outcome); |
| } |
| |
| void RecordFailureOutcomeUponDestruction( |
| FailureType failure_type, |
| blink::WebOTPServiceOutcome expected_outcome) { |
| GURL url = GURL(kTestUrl); |
| NavigateAndCommit(url); |
| { |
| Service service(web_contents()); |
| |
| base::RunLoop loop; |
| EXPECT_CALL(*service.provider(), Retrieve(_, _)).WillOnce(Invoke([&]() { |
| service.NotifyFailure(failure_type); |
| loop.Quit(); |
| })); |
| |
| service.MakeRequest(base::DoNothing()); |
| |
| loop.Run(); |
| } |
| // |service| going out of scope means that the outstanding request would be |
| // considered failed with |kUnhandledRequest|. |
| |
| ExpectOutcomeUKM(url, expected_outcome); |
| } |
| |
| private: |
| base::HistogramTester histogram_tester_; |
| std::unique_ptr<ukm::TestAutoSetUkmRecorder> ukm_recorder_; |
| }; |
| |
| } // namespace |
| |
| TEST_F(WebOTPServiceTest, Basic) { |
| NavigateAndCommit(GURL(kTestUrl)); |
| |
| Service service(web_contents()); |
| |
| base::RunLoop loop; |
| |
| EXPECT_CALL(*service.provider(), Retrieve(_, _)) |
| .WillOnce(Invoke( |
| [&service]() { service.NotifyReceive(GURL(kTestUrl), "hi"); })); |
| |
| service.MakeRequest(BindLambdaForTesting( |
| [&loop](SmsStatus status, const optional<string>& otp) { |
| EXPECT_EQ(SmsStatus::kSuccess, status); |
| EXPECT_EQ("hi", otp.value()); |
| loop.Quit(); |
| })); |
| |
| loop.Run(); |
| |
| ASSERT_FALSE(service.fetcher()->HasSubscribers()); |
| } |
| |
| TEST_F(WebOTPServiceTest, HandlesMultipleCalls) { |
| NavigateAndCommit(GURL(kTestUrl)); |
| |
| Service service(web_contents()); |
| |
| { |
| base::RunLoop loop; |
| |
| EXPECT_CALL(*service.provider(), Retrieve(_, _)) |
| .WillOnce(Invoke( |
| [&service]() { service.NotifyReceive(GURL(kTestUrl), "first"); })); |
| |
| service.MakeRequest(BindLambdaForTesting( |
| [&loop](SmsStatus status, const optional<string>& otp) { |
| EXPECT_EQ("first", otp.value()); |
| EXPECT_EQ(SmsStatus::kSuccess, status); |
| loop.Quit(); |
| })); |
| |
| loop.Run(); |
| } |
| |
| { |
| base::RunLoop loop; |
| |
| EXPECT_CALL(*service.provider(), Retrieve(_, _)) |
| .WillOnce(Invoke( |
| [&service]() { service.NotifyReceive(GURL(kTestUrl), "second"); })); |
| |
| service.MakeRequest(BindLambdaForTesting( |
| [&loop](SmsStatus status, const optional<string>& otp) { |
| EXPECT_EQ("second", otp.value()); |
| EXPECT_EQ(SmsStatus::kSuccess, status); |
| loop.Quit(); |
| })); |
| |
| loop.Run(); |
| } |
| } |
| |
| TEST_F(WebOTPServiceTest, IgnoreFromOtherOrigins) { |
| NavigateAndCommit(GURL(kTestUrl)); |
| |
| Service service(web_contents()); |
| |
| SmsStatus sms_status; |
| optional<string> response; |
| |
| base::RunLoop sms_loop; |
| |
| EXPECT_CALL(*service.provider(), Retrieve(_, _)) |
| .WillOnce(Invoke([&service]() { |
| // Delivers an SMS from an unrelated origin first and expect the |
| // receiver to ignore it. |
| service.NotifyReceive(GURL("http://b.com"), "wrong"); |
| service.NotifyReceive(GURL(kTestUrl), "right"); |
| })); |
| |
| service.MakeRequest( |
| BindLambdaForTesting([&sms_status, &response, &sms_loop]( |
| SmsStatus status, const optional<string>& otp) { |
| sms_status = status; |
| response = otp; |
| sms_loop.Quit(); |
| })); |
| |
| sms_loop.Run(); |
| |
| EXPECT_EQ("right", response.value()); |
| EXPECT_EQ(SmsStatus::kSuccess, sms_status); |
| } |
| |
| TEST_F(WebOTPServiceTest, ExpectOneReceiveTwo) { |
| NavigateAndCommit(GURL(kTestUrl)); |
| |
| Service service(web_contents()); |
| |
| SmsStatus sms_status; |
| optional<string> response; |
| |
| base::RunLoop sms_loop; |
| |
| EXPECT_CALL(*service.provider(), Retrieve(_, _)) |
| .WillOnce(Invoke([&service]() { |
| // Delivers two SMSes for the same origin, even if only one was being |
| // expected. |
| ASSERT_TRUE(service.fetcher()->HasSubscribers()); |
| service.NotifyReceive(GURL(kTestUrl), "first"); |
| ASSERT_FALSE(service.fetcher()->HasSubscribers()); |
| service.NotifyReceive(GURL(kTestUrl), "second"); |
| })); |
| |
| service.MakeRequest( |
| BindLambdaForTesting([&sms_status, &response, &sms_loop]( |
| SmsStatus status, const optional<string>& otp) { |
| sms_status = status; |
| response = otp; |
| sms_loop.Quit(); |
| })); |
| |
| sms_loop.Run(); |
| |
| EXPECT_EQ("first", response.value()); |
| EXPECT_EQ(SmsStatus::kSuccess, sms_status); |
| } |
| |
| TEST_F(WebOTPServiceTest, AtMostOneSmsRequestPerOrigin) { |
| NavigateAndCommit(GURL(kTestUrl)); |
| |
| Service service(web_contents()); |
| |
| SmsStatus sms_status1; |
| optional<string> response1; |
| SmsStatus sms_status2; |
| optional<string> response2; |
| |
| base::RunLoop sms1_loop, sms2_loop; |
| |
| EXPECT_CALL(*service.provider(), Retrieve(_, _)) |
| .WillOnce(Return()) |
| .WillOnce(Invoke( |
| [&service]() { service.NotifyReceive(GURL(kTestUrl), "second"); })); |
| |
| service.MakeRequest( |
| BindLambdaForTesting([&sms_status1, &response1, &sms1_loop]( |
| SmsStatus status, const optional<string>& otp) { |
| sms_status1 = status; |
| response1 = otp; |
| sms1_loop.Quit(); |
| })); |
| |
| // Make the 2nd SMS request which will cancel the 1st request because only |
| // one request can be pending per origin per tab. |
| service.MakeRequest( |
| BindLambdaForTesting([&sms_status2, &response2, &sms2_loop]( |
| SmsStatus status, const optional<string>& otp) { |
| sms_status2 = status; |
| response2 = otp; |
| sms2_loop.Quit(); |
| })); |
| |
| sms1_loop.Run(); |
| sms2_loop.Run(); |
| |
| EXPECT_EQ(absl::nullopt, response1); |
| EXPECT_EQ(SmsStatus::kCancelled, sms_status1); |
| |
| EXPECT_EQ("second", response2.value()); |
| EXPECT_EQ(SmsStatus::kSuccess, sms_status2); |
| } |
| |
| TEST_F(WebOTPServiceTest, CleansUp) { |
| NavigateAndCommit(GURL(kTestUrl)); |
| |
| NiceMock<MockSmsWebContentsDelegate> delegate; |
| WebContentsImpl* web_contents_impl = |
| static_cast<WebContentsImpl*>(web_contents()); |
| web_contents_impl->SetDelegate(&delegate); |
| |
| NiceMock<MockSmsProvider> provider; |
| SmsFetcherImpl fetcher(&provider); |
| mojo::Remote<blink::mojom::WebOTPService> service; |
| EXPECT_TRUE(WebOTPService::Create(&fetcher, main_rfh(), |
| service.BindNewPipeAndPassReceiver())); |
| |
| base::RunLoop navigate; |
| |
| EXPECT_CALL(provider, Retrieve(_, _)).WillOnce(Invoke([&navigate]() { |
| navigate.Quit(); |
| })); |
| |
| base::RunLoop reload; |
| |
| service->Receive(base::BindLambdaForTesting( |
| [&reload](SmsStatus status, const optional<string>& otp) { |
| EXPECT_EQ(SmsStatus::kUnhandledRequest, status); |
| EXPECT_EQ(absl::nullopt, otp); |
| reload.Quit(); |
| })); |
| |
| navigate.Run(); |
| |
| // Simulates the user reloading the page and navigating away, which |
| // destructs the service. |
| NavigateAndCommit(GURL(kTestUrl)); |
| |
| reload.Run(); |
| |
| ASSERT_FALSE(fetcher.HasSubscribers()); |
| } |
| |
| TEST_F(WebOTPServiceTest, Abort) { |
| NavigateAndCommit(GURL(kTestUrl)); |
| |
| Service service(web_contents()); |
| |
| base::RunLoop loop; |
| |
| service.MakeRequest(BindLambdaForTesting( |
| [&loop](SmsStatus status, const optional<string>& otp) { |
| EXPECT_EQ(SmsStatus::kAborted, status); |
| EXPECT_EQ(absl::nullopt, otp); |
| loop.Quit(); |
| })); |
| |
| service.AbortRequest(); |
| |
| loop.Run(); |
| |
| ASSERT_FALSE(service.fetcher()->HasSubscribers()); |
| } |
| |
| // Following tests exercise parts of sms service logic that depend on user |
| // prompting. In particular how we handle incoming request while there is an |
| // active in-flight prompts. |
| |
| class ServiceWithPrompt : public Service { |
| public: |
| explicit ServiceWithPrompt(WebContents* web_contents) |
| : Service(web_contents, |
| web_contents->GetPrimaryMainFrame()->GetLastCommittedOrigin(), |
| std::make_unique<NiceMock<MockUserConsentHandler>>()) { |
| mock_handler_ = |
| static_cast<NiceMock<MockUserConsentHandler>*>(consent_handler()); |
| } |
| |
| void ExpectRequestUserConsent() { |
| EXPECT_CALL(*mock_handler_, RequestUserConsent(_, _)) |
| .WillOnce( |
| Invoke([=](const std::string&, CompletionCallback on_complete) { |
| on_complete_callback_ = std::move(on_complete); |
| })); |
| |
| EXPECT_CALL(*mock_handler_, is_async()).WillRepeatedly(Return(true)); |
| EXPECT_CALL(*mock_handler_, is_active()).WillRepeatedly(Invoke([=]() { |
| return !on_complete_callback_.is_null(); |
| })); |
| } |
| |
| void ConfirmPrompt() { |
| if (on_complete_callback_.is_null()) { |
| FAIL() << "User prompt is not available"; |
| } |
| std::move(on_complete_callback_).Run(UserConsentResult::kApproved); |
| on_complete_callback_.Reset(); |
| } |
| |
| void DismissPrompt() { |
| if (on_complete_callback_.is_null()) { |
| FAIL() << "User prompt is not available"; |
| } |
| std::move(on_complete_callback_).Run(UserConsentResult::kDenied); |
| ActivateTimer(); |
| on_complete_callback_.Reset(); |
| } |
| |
| bool IsPromptOpen() const { return !on_complete_callback_.is_null(); } |
| |
| private: |
| // The actual consent handler is owned by WebOTPService but we keep a ptr to |
| // it so it can be used to set expectations for it. It is safe since the |
| // sms service lifetime is the same as this object. |
| raw_ptr<NiceMock<MockUserConsentHandler>> mock_handler_; |
| CompletionCallback on_complete_callback_; |
| }; |
| |
| TEST_F(WebOTPServiceTest, SecondRequestDuringPrompt) { |
| NavigateAndCommit(GURL(kTestUrl)); |
| |
| ServiceWithPrompt service(web_contents()); |
| |
| SmsStatus sms_status1; |
| optional<string> response1; |
| SmsStatus sms_status2; |
| optional<string> response2; |
| |
| base::RunLoop sms_loop; |
| |
| // Expect SMS Prompt to be created once. |
| service.ExpectRequestUserConsent(); |
| |
| EXPECT_CALL(*service.provider(), Retrieve(_, _)) |
| .WillOnce(Invoke([&service]() { |
| service.NotifyReceive(GURL(kTestUrl), "second", |
| UserConsent::kNotObtained); |
| })); |
| |
| // First request. |
| service.MakeRequest( |
| BindLambdaForTesting([&sms_status1, &response1, &service]( |
| SmsStatus status, const optional<string>& otp) { |
| sms_status1 = status; |
| response1 = otp; |
| service.ConfirmPrompt(); |
| })); |
| |
| // Make second request before confirming prompt. |
| service.MakeRequest( |
| BindLambdaForTesting([&sms_status2, &response2, &sms_loop]( |
| SmsStatus status, const optional<string>& otp) { |
| sms_status2 = status; |
| response2 = otp; |
| sms_loop.Quit(); |
| })); |
| |
| sms_loop.Run(); |
| |
| EXPECT_EQ(absl::nullopt, response1); |
| EXPECT_EQ(SmsStatus::kCancelled, sms_status1); |
| |
| EXPECT_EQ("second", response2.value()); |
| EXPECT_EQ(SmsStatus::kSuccess, sms_status2); |
| } |
| |
| TEST_F(WebOTPServiceTest, AbortWhilePrompt) { |
| NavigateAndCommit(GURL(kTestUrl)); |
| |
| ServiceWithPrompt service(web_contents()); |
| |
| base::RunLoop loop; |
| |
| service.ExpectRequestUserConsent(); |
| |
| service.MakeRequest(BindLambdaForTesting( |
| [&loop](SmsStatus status, const optional<string>& otp) { |
| EXPECT_EQ(SmsStatus::kAborted, status); |
| EXPECT_EQ(absl::nullopt, otp); |
| loop.Quit(); |
| })); |
| |
| EXPECT_CALL(*service.provider(), Retrieve(_, _)) |
| .WillOnce(Invoke([&service]() { |
| service.NotifyReceive(GURL(kTestUrl), "ABC", UserConsent::kNotObtained); |
| EXPECT_TRUE(service.IsPromptOpen()); |
| service.AbortRequest(); |
| })); |
| |
| loop.Run(); |
| |
| ASSERT_FALSE(service.fetcher()->HasSubscribers()); |
| |
| service.ConfirmPrompt(); |
| } |
| |
| TEST_F(WebOTPServiceTest, RequestAfterAbortWhilePrompt) { |
| NavigateAndCommit(GURL(kTestUrl)); |
| |
| ServiceWithPrompt service(web_contents()); |
| |
| { |
| base::RunLoop loop; |
| |
| service.ExpectRequestUserConsent(); |
| |
| service.MakeRequest(BindLambdaForTesting( |
| [&loop](SmsStatus status, const optional<string>& otp) { |
| EXPECT_EQ(SmsStatus::kAborted, status); |
| EXPECT_EQ(absl::nullopt, otp); |
| loop.Quit(); |
| })); |
| |
| EXPECT_CALL(*service.provider(), Retrieve(_, _)) |
| .WillOnce(Invoke([&service]() { |
| service.NotifyReceive(GURL(kTestUrl), "hi", |
| UserConsent::kNotObtained); |
| EXPECT_TRUE(service.IsPromptOpen()); |
| service.AbortRequest(); |
| })); |
| |
| loop.Run(); |
| } |
| |
| ASSERT_FALSE(service.fetcher()->HasSubscribers()); |
| |
| // Confirm to dismiss prompt for a request that has already aborted. |
| service.ConfirmPrompt(); |
| |
| { |
| base::RunLoop loop; |
| |
| service.ExpectRequestUserConsent(); |
| |
| service.MakeRequest(BindLambdaForTesting( |
| [&loop](SmsStatus status, const optional<string>& otp) { |
| // Verify that the 2nd request completes successfully after prompt |
| // confirmation. |
| EXPECT_EQ(SmsStatus::kSuccess, status); |
| EXPECT_EQ("hi2", otp.value()); |
| loop.Quit(); |
| })); |
| |
| EXPECT_CALL(*service.provider(), Retrieve(_, _)) |
| .WillOnce(Invoke([&service]() { |
| service.NotifyReceive(GURL(kTestUrl), "hi2", |
| UserConsent::kNotObtained); |
| service.ConfirmPrompt(); |
| })); |
| |
| loop.Run(); |
| } |
| } |
| |
| TEST_F(WebOTPServiceTest, SecondRequestWhilePrompt) { |
| NavigateAndCommit(GURL(kTestUrl)); |
| |
| ServiceWithPrompt service(web_contents()); |
| |
| base::RunLoop callback_loop1, callback_loop2, req_loop; |
| |
| service.ExpectRequestUserConsent(); |
| |
| service.MakeRequest(BindLambdaForTesting( |
| [&callback_loop1](SmsStatus status, const optional<string>& otp) { |
| EXPECT_EQ(SmsStatus::kAborted, status); |
| EXPECT_EQ(absl::nullopt, otp); |
| callback_loop1.Quit(); |
| })); |
| |
| EXPECT_CALL(*service.provider(), Retrieve(_, _)) |
| .WillOnce(Invoke([&service]() { |
| service.NotifyReceive(GURL(kTestUrl), "hi", UserConsent::kNotObtained); |
| service.AbortRequest(); |
| })); |
| |
| callback_loop1.Run(); |
| |
| base::ThreadTaskRunnerHandle::Get()->PostTaskAndReply( |
| FROM_HERE, BindLambdaForTesting([&]() { |
| service.MakeRequest(BindLambdaForTesting( |
| [&callback_loop2](SmsStatus status, const optional<string>& otp) { |
| EXPECT_EQ(SmsStatus::kSuccess, status); |
| EXPECT_EQ("hi", otp.value()); |
| callback_loop2.Quit(); |
| })); |
| }), |
| req_loop.QuitClosure()); |
| |
| req_loop.Run(); |
| |
| // Simulate pressing 'Verify' on Infobar. |
| service.ConfirmPrompt(); |
| |
| callback_loop2.Run(); |
| |
| ASSERT_FALSE(service.fetcher()->HasSubscribers()); |
| } |
| |
| TEST_F(WebOTPServiceTest, RecordTimeMetricsForContinueOnSuccess) { |
| NavigateAndCommit(GURL(kTestUrl)); |
| |
| ServiceWithPrompt service(web_contents()); |
| |
| base::RunLoop loop; |
| |
| service.ExpectRequestUserConsent(); |
| |
| EXPECT_CALL(*service.provider(), Retrieve(_, _)) |
| .WillOnce(Invoke([&service]() { |
| service.NotifyReceive(GURL(kTestUrl), "ABC", UserConsent::kNotObtained); |
| service.ConfirmPrompt(); |
| })); |
| |
| service.MakeRequest(BindLambdaForTesting( |
| [&loop](SmsStatus status, const optional<string>& otp) { loop.Quit(); })); |
| |
| loop.Run(); |
| |
| histogram_tester().ExpectTotalCount("Blink.Sms.Receive.TimeContinueOnSuccess", |
| 1); |
| histogram_tester().ExpectTotalCount("Blink.Sms.Receive.TimeSmsReceive", 1); |
| } |
| |
| TEST_F(WebOTPServiceTest, RecordMetricsForCancelOnSuccess) { |
| NavigateAndCommit(GURL(kTestUrl)); |
| |
| ServiceWithPrompt service(web_contents()); |
| |
| // Histogram will be recorded if the SMS has already arrived. |
| base::RunLoop loop; |
| |
| service.ExpectRequestUserConsent(); |
| |
| EXPECT_CALL(*service.provider(), Retrieve(_, _)) |
| .WillOnce(Invoke([&service]() { |
| service.NotifyReceive(GURL(kTestUrl), "hi", UserConsent::kNotObtained); |
| service.DismissPrompt(); |
| })); |
| |
| service.MakeRequest(BindLambdaForTesting( |
| [&loop](SmsStatus status, const optional<string>& otp) { loop.Quit(); })); |
| |
| loop.Run(); |
| |
| histogram_tester().ExpectTotalCount("Blink.Sms.Receive.TimeCancelOnSuccess", |
| 1); |
| histogram_tester().ExpectTotalCount("Blink.Sms.Receive.TimeSmsReceive", 1); |
| } |
| |
| TEST_F(WebOTPServiceTest, RecordTimeoutAsOutcomeWithoutFailure) { |
| GURL url = GURL(kTestUrl); |
| NavigateAndCommit(url); |
| |
| ServiceWithPrompt service(web_contents()); |
| |
| base::RunLoop ukm_loop; |
| ukm_recorder()->SetOnAddEntryCallback(Entry::kEntryName, |
| ukm_loop.QuitClosure()); |
| |
| service.ExpectRequestUserConsent(); |
| EXPECT_CALL(*service.provider(), Retrieve(_, _)) |
| .WillOnce(Invoke([&service]() { |
| service.NotifyReceive(GURL(kTestUrl), "hi", UserConsent::kNotObtained); |
| service.ActivateTimer(); |
| })); |
| |
| service.MakeRequest(base::DoNothing()); |
| |
| ukm_loop.Run(); |
| |
| ExpectOutcomeUKM(url, blink::WebOTPServiceOutcome::kTimeout); |
| } |
| |
| TEST_F(WebOTPServiceTest, RecordTimeoutAsOutcomeWithTimerActivation) { |
| RecordFailureOutcomeWithTimerActivation( |
| FailureType::kPromptTimeout, blink::WebOTPServiceOutcome::kTimeout); |
| } |
| |
| TEST_F(WebOTPServiceTest, NotRecordTimeoutAsOutcomeWithoutTimerActivation) { |
| NotRecordFailureOutcomeWithoutTimerActivation(FailureType::kPromptTimeout); |
| } |
| |
| TEST_F(WebOTPServiceTest, RecordTimeoutAsOutcomeUponPreviousRequestCancelled) { |
| RecordFailureOutcomeUponPreviousRequestCancelled( |
| FailureType::kPromptTimeout, blink::WebOTPServiceOutcome::kTimeout); |
| } |
| |
| TEST_F(WebOTPServiceTest, RecordTimeoutAsOutcomeUponDestruction) { |
| RecordFailureOutcomeUponDestruction(FailureType::kPromptTimeout, |
| blink::WebOTPServiceOutcome::kTimeout); |
| } |
| |
| TEST_F(WebOTPServiceTest, RecordUserCancelledAsOutcome) { |
| RecordFailureOutcomeWithTimerActivation( |
| FailureType::kPromptCancelled, |
| blink::WebOTPServiceOutcome::kUserCancelled); |
| ExpectTimingUKM("TimeUserCancelMs"); |
| histogram_tester().ExpectTotalCount("Blink.Sms.Receive.TimeUserCancel", 1); |
| } |
| |
| TEST_F(WebOTPServiceTest, |
| NotRecordUserCancelledAsOutcomeWithoutTimerActivation) { |
| NotRecordFailureOutcomeWithoutTimerActivation(FailureType::kPromptCancelled); |
| } |
| |
| TEST_F(WebOTPServiceTest, |
| RecordUserCancelledAsOutcomeUponPreviousRequestCancelled) { |
| RecordFailureOutcomeUponPreviousRequestCancelled( |
| FailureType::kPromptCancelled, |
| blink::WebOTPServiceOutcome::kUserCancelled); |
| } |
| |
| TEST_F(WebOTPServiceTest, RecordUserCancelledAsOutcomeUponDestruction) { |
| RecordFailureOutcomeUponDestruction( |
| FailureType::kPromptCancelled, |
| blink::WebOTPServiceOutcome::kUserCancelled); |
| } |
| |
| TEST_F(WebOTPServiceTest, RecordUserDismissPrompt) { |
| GURL url = GURL(kTestUrl); |
| NavigateAndCommit(url); |
| |
| ServiceWithPrompt service(web_contents()); |
| |
| base::RunLoop ukm_loop; |
| ukm_recorder()->SetOnAddEntryCallback(Entry::kEntryName, |
| ukm_loop.QuitClosure()); |
| |
| service.ExpectRequestUserConsent(); |
| EXPECT_CALL(*service.provider(), Retrieve(_, _)) |
| .WillOnce(Invoke([&service]() { |
| service.NotifyReceive(GURL(kTestUrl), "hi", UserConsent::kNotObtained); |
| service.DismissPrompt(); |
| })); |
| |
| service.MakeRequest(base::DoNothing()); |
| |
| ukm_loop.Run(); |
| |
| ExpectOutcomeUKM(url, blink::WebOTPServiceOutcome::kUserCancelled); |
| ExpectTimingUKM("TimeUserCancelMs"); |
| histogram_tester().ExpectTotalCount("Blink.Sms.Receive.TimeUserCancel", 1); |
| } |
| |
| TEST_F(WebOTPServiceTest, RecordUnhandledRequestOnNavigation) { |
| web_contents()->GetController().GetBackForwardCache().DisableForTesting( |
| content::BackForwardCache::TEST_REQUIRES_NO_CACHING); |
| NavigateAndCommit(GURL(kTestUrl)); |
| NiceMock<MockSmsWebContentsDelegate> delegate; |
| WebContentsImpl* web_contents_impl = |
| static_cast<WebContentsImpl*>(web_contents()); |
| web_contents_impl->SetDelegate(&delegate); |
| |
| NiceMock<MockSmsProvider> provider; |
| SmsFetcherImpl fetcher(&provider); |
| mojo::Remote<blink::mojom::WebOTPService> service; |
| EXPECT_TRUE(WebOTPService::Create(&fetcher, main_rfh(), |
| service.BindNewPipeAndPassReceiver())); |
| base::RunLoop ukm_loop; |
| ukm_recorder()->SetOnAddEntryCallback(Entry::kEntryName, |
| ukm_loop.QuitClosure()); |
| |
| base::RunLoop navigate; |
| |
| EXPECT_CALL(provider, Retrieve(_, _)).WillOnce(Invoke([&navigate]() { |
| navigate.Quit(); |
| })); |
| |
| base::RunLoop reload; |
| |
| service->Receive(base::BindLambdaForTesting( |
| [&reload](SmsStatus status, const optional<string>& otp) { |
| EXPECT_EQ(SmsStatus::kUnhandledRequest, status); |
| EXPECT_EQ(absl::nullopt, otp); |
| reload.Quit(); |
| })); |
| |
| navigate.Run(); |
| |
| // Simulates the user navigating to a new page. |
| NavigateAndCommit(GURL("https://www.example.com")); |
| |
| reload.Run(); |
| ukm_loop.Run(); |
| |
| ExpectOutcomeUKM(GURL(kTestUrl), |
| blink::WebOTPServiceOutcome::kUnhandledRequest); |
| } |
| |
| TEST_F(WebOTPServiceTest, NotRecordUnhandledRequestWhenThereIsNoRequest) { |
| GURL url = GURL(kTestUrl); |
| NavigateAndCommit(url); |
| |
| { |
| ServiceWithPrompt service(web_contents()); |
| ASSERT_FALSE(service.fetcher()->HasSubscribers()); |
| } |
| |
| ExpectNoOutcomeUKM(); |
| } |
| |
| TEST_F(WebOTPServiceTest, NotRecordUnhandledRequestWhenRequestIsHandled) { |
| GURL url = GURL(kTestUrl); |
| NavigateAndCommit(url); |
| |
| { |
| ServiceWithPrompt service(web_contents()); |
| |
| base::RunLoop ukm_loop; |
| ukm_recorder()->SetOnAddEntryCallback(Entry::kEntryName, |
| ukm_loop.QuitClosure()); |
| |
| service.ExpectRequestUserConsent(); |
| EXPECT_CALL(*service.provider(), Retrieve(_, _)) |
| .WillOnce(Invoke([&service]() { |
| service.NotifyReceive(GURL(kTestUrl), "hi", |
| UserConsent::kNotObtained); |
| service.DismissPrompt(); |
| })); |
| |
| service.MakeRequest(base::DoNothing()); |
| |
| ukm_loop.Run(); |
| } |
| |
| ExpectOutcomeUKM(url, blink::WebOTPServiceOutcome::kUserCancelled); |
| } |
| |
| TEST_F(WebOTPServiceTest, RecordWebContentsVisibilityForUserConsentAPI) { |
| NavigateAndCommit(GURL(kTestUrl)); |
| base::HistogramTester histogram_tester; |
| |
| // Sets the WebContents to visible |
| WebContentsImpl* web_contents_impl = |
| static_cast<WebContentsImpl*>(web_contents()); |
| web_contents_impl->UpdateWebContentsVisibility(Visibility::VISIBLE); |
| ASSERT_EQ(web_contents_impl->GetVisibility(), Visibility::VISIBLE); |
| Service service1(web_contents_impl); |
| |
| base::RunLoop loop1; |
| |
| EXPECT_CALL(*service1.provider(), Retrieve(_, _)) |
| .WillOnce(Invoke([&service1]() { |
| service1.NotifyReceive(GURL(kTestUrl), "ABC", UserConsent::kObtained); |
| })); |
| |
| service1.MakeRequest(BindLambdaForTesting( |
| [&loop1](SmsStatus status, const optional<string>& otp) { |
| loop1.Quit(); |
| })); |
| |
| loop1.Run(); |
| |
| histogram_tester.ExpectBucketCount("Blink.Sms.WebContentsVisibleOnReceive", 1, |
| 1); |
| histogram_tester.ExpectTotalCount("Blink.Sms.WebContentsVisibleOnReceive", 1); |
| |
| // Sets the WebContents to invisible |
| web_contents_impl->UpdateWebContentsVisibility(Visibility::HIDDEN); |
| ASSERT_NE(web_contents_impl->GetVisibility(), Visibility::VISIBLE); |
| |
| Service service2(web_contents_impl); |
| |
| base::RunLoop loop2; |
| EXPECT_CALL(*service2.provider(), Retrieve(_, _)) |
| .WillOnce(Invoke([&service2]() { |
| service2.NotifyReceive(GURL(kTestUrl), "ABC", UserConsent::kObtained); |
| })); |
| |
| service2.MakeRequest(BindLambdaForTesting( |
| [&loop2](SmsStatus status, const optional<string>& otp) { |
| loop2.Quit(); |
| })); |
| |
| loop2.Run(); |
| |
| histogram_tester.ExpectBucketCount("Blink.Sms.WebContentsVisibleOnReceive", 0, |
| 1); |
| histogram_tester.ExpectTotalCount("Blink.Sms.WebContentsVisibleOnReceive", 2); |
| } |
| |
| TEST_F(WebOTPServiceTest, RecordCancelledAsOutcome) { |
| GURL url = GURL(kTestUrl); |
| NavigateAndCommit(url); |
| |
| ServiceWithPrompt service(web_contents()); |
| |
| base::RunLoop sms1_loop, sms2_loop; |
| base::RunLoop ukm_loop; |
| ukm_recorder()->SetOnAddEntryCallback(Entry::kEntryName, |
| ukm_loop.QuitClosure()); |
| |
| EXPECT_CALL(*service.provider(), Retrieve(_, _)) |
| .WillOnce(Return()) |
| .WillOnce(Invoke([&sms2_loop]() { sms2_loop.Quit(); })); |
| |
| service.MakeRequest(BindLambdaForTesting( |
| [&sms1_loop](SmsStatus status, const optional<string>& otp) { |
| sms1_loop.Quit(); |
| })); |
| |
| // The 2nd request will cancel the 1st one. |
| service.MakeRequest(base::DoNothing()); |
| |
| sms1_loop.Run(); |
| sms2_loop.Run(); |
| ukm_loop.Run(); |
| |
| ExpectOutcomeUKM(url, blink::WebOTPServiceOutcome::kCancelled); |
| } |
| |
| TEST_F(WebOTPServiceTest, |
| RecordCrossDeviceFailureAsOutcomeUponPreviousRequestCancelled) { |
| RecordFailureOutcomeUponPreviousRequestCancelled( |
| FailureType::kCrossDeviceFailure, |
| blink::WebOTPServiceOutcome::kCrossDeviceFailure); |
| } |
| |
| TEST_F(WebOTPServiceTest, RecordCrossDeviceFailureAsOutcomeUponDestruction) { |
| RecordFailureOutcomeUponDestruction( |
| FailureType::kCrossDeviceFailure, |
| blink::WebOTPServiceOutcome::kCrossDeviceFailure); |
| } |
| |
| } // namespace content |