| // Copyright 2022 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "content/browser/preloading/prefetch/prefetch_container.h" |
| |
| #include "base/test/metrics/histogram_tester.h" |
| #include "components/ukm/test_ukm_recorder.h" |
| #include "content/browser/preloading/prefetch/prefetch_probe_result.h" |
| #include "content/browser/preloading/prefetch/prefetch_status.h" |
| #include "content/browser/preloading/prefetch/prefetch_type.h" |
| #include "content/browser/preloading/prefetch/prefetched_mainframe_response_container.h" |
| #include "content/public/browser/browser_context.h" |
| #include "content/public/browser/global_routing_id.h" |
| #include "content/public/browser/storage_partition.h" |
| #include "content/public/test/test_renderer_host.h" |
| #include "mojo/public/cpp/bindings/remote.h" |
| #include "net/base/isolation_info.h" |
| #include "services/metrics/public/cpp/metrics_utils.h" |
| #include "services/metrics/public/cpp/ukm_builders.h" |
| #include "services/network/public/mojom/cookie_manager.mojom.h" |
| #include "services/network/public/mojom/network_context.mojom.h" |
| #include "services/network/public/mojom/url_response_head.mojom.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| |
| namespace content { |
| |
| class PrefetchContainerTest : public RenderViewHostTestHarness { |
| public: |
| PrefetchContainerTest() |
| : RenderViewHostTestHarness( |
| base::test::TaskEnvironment::TimeSource::MOCK_TIME) {} |
| |
| void SetUp() override { |
| RenderViewHostTestHarness::SetUp(); |
| |
| browser_context() |
| ->GetDefaultStoragePartition() |
| ->GetNetworkContext() |
| ->GetCookieManager(cookie_manager_.BindNewPipeAndPassReceiver()); |
| } |
| |
| network::mojom::CookieManager* cookie_manager() { |
| return cookie_manager_.get(); |
| } |
| |
| bool SetCookie(const GURL& url, const std::string& value) { |
| std::unique_ptr<net::CanonicalCookie> cookie(net::CanonicalCookie::Create( |
| url, value, base::Time::Now(), /*server_time=*/absl::nullopt, |
| /*cookie_partition_key=*/absl::nullopt)); |
| |
| EXPECT_TRUE(cookie.get()); |
| |
| bool result = false; |
| base::RunLoop run_loop; |
| |
| net::CookieOptions options; |
| options.set_include_httponly(); |
| options.set_same_site_cookie_context( |
| net::CookieOptions::SameSiteCookieContext::MakeInclusive()); |
| |
| cookie_manager_->SetCanonicalCookie( |
| *cookie.get(), url, options, |
| base::BindOnce( |
| [](bool* result, base::RunLoop* run_loop, |
| net::CookieAccessResult set_cookie_access_result) { |
| *result = set_cookie_access_result.status.IsInclude(); |
| run_loop->Quit(); |
| }, |
| &result, &run_loop)); |
| |
| // This will run until the cookie is set. |
| run_loop.Run(); |
| |
| // This will run until the cookie listener is updated. |
| base::RunLoop().RunUntilIdle(); |
| |
| return result; |
| } |
| |
| void UpdatePrefetchRequestMetrics( |
| PrefetchContainer* prefetch_container, |
| const absl::optional<network::URLLoaderCompletionStatus>& |
| completion_status, |
| const network::mojom::URLResponseHead* head) { |
| prefetch_container->UpdatePrefetchRequestMetrics(completion_status, head); |
| } |
| |
| private: |
| mojo::Remote<network::mojom::CookieManager> cookie_manager_; |
| }; |
| |
| TEST_F(PrefetchContainerTest, CreatePrefetchContainer) { |
| PrefetchContainer prefetch_container( |
| GlobalRenderFrameHostId(1234, 5678), GURL("https://test.com"), |
| PrefetchType(/*use_isolated_network_context=*/true, |
| /*use_prefetch_proxy=*/true), |
| nullptr); |
| |
| EXPECT_EQ(prefetch_container.GetReferringRenderFrameHostId(), |
| GlobalRenderFrameHostId(1234, 5678)); |
| EXPECT_EQ(prefetch_container.GetURL(), GURL("https://test.com")); |
| EXPECT_EQ(prefetch_container.GetPrefetchType(), |
| PrefetchType(/*use_isolated_network_context=*/true, |
| /*use_prefetch_proxy=*/true)); |
| |
| EXPECT_EQ(prefetch_container.GetPrefetchContainerKey(), |
| std::make_pair(GlobalRenderFrameHostId(1234, 5678), |
| GURL("https://test.com"))); |
| } |
| |
| TEST_F(PrefetchContainerTest, PrefetchStatus) { |
| PrefetchContainer prefetch_container( |
| GlobalRenderFrameHostId(1234, 5678), GURL("https://test.com"), |
| PrefetchType(/*use_isolated_network_context=*/true, |
| /*use_prefetch_proxy=*/true), |
| nullptr); |
| |
| EXPECT_FALSE(prefetch_container.HasPrefetchStatus()); |
| |
| prefetch_container.SetPrefetchStatus(PrefetchStatus::kPrefetchNotStarted); |
| |
| EXPECT_TRUE(prefetch_container.HasPrefetchStatus()); |
| EXPECT_EQ(prefetch_container.GetPrefetchStatus(), |
| PrefetchStatus::kPrefetchNotStarted); |
| } |
| |
| TEST_F(PrefetchContainerTest, IsDecoy) { |
| PrefetchContainer prefetch_container( |
| GlobalRenderFrameHostId(1234, 5678), GURL("https://test.com"), |
| PrefetchType(/*use_isolated_network_context=*/true, |
| /*use_prefetch_proxy=*/true), |
| nullptr); |
| |
| EXPECT_FALSE(prefetch_container.IsDecoy()); |
| |
| prefetch_container.SetIsDecoy(true); |
| EXPECT_TRUE(prefetch_container.IsDecoy()); |
| } |
| |
| TEST_F(PrefetchContainerTest, ValidResponse) { |
| PrefetchContainer prefetch_container( |
| GlobalRenderFrameHostId(1234, 5678), GURL("https://test.com"), |
| PrefetchType(/*use_isolated_network_context=*/true, |
| /*use_prefetch_proxy=*/true), |
| nullptr); |
| |
| prefetch_container.TakePrefetchedResponse( |
| std::make_unique<PrefetchedMainframeResponseContainer>( |
| net::IsolationInfo(), network::mojom::URLResponseHead::New(), |
| std::make_unique<std::string>("test body"))); |
| |
| task_environment()->FastForwardBy(base::Minutes(2)); |
| |
| EXPECT_FALSE(prefetch_container.HasValidPrefetchedResponse(base::Minutes(1))); |
| EXPECT_TRUE(prefetch_container.HasValidPrefetchedResponse(base::Minutes(3))); |
| } |
| |
| TEST_F(PrefetchContainerTest, CookieListener) { |
| PrefetchContainer prefetch_container( |
| GlobalRenderFrameHostId(1234, 5678), GURL("https://test.com"), |
| PrefetchType(/*use_isolated_network_context=*/true, |
| /*use_prefetch_proxy=*/true), |
| nullptr); |
| |
| EXPECT_FALSE(prefetch_container.HaveDefaultContextCookiesChanged()); |
| |
| prefetch_container.RegisterCookieListener(cookie_manager()); |
| |
| EXPECT_FALSE(prefetch_container.HaveDefaultContextCookiesChanged()); |
| |
| ASSERT_TRUE(SetCookie(GURL("https://test.com"), "test-cookie")); |
| |
| EXPECT_TRUE(prefetch_container.HaveDefaultContextCookiesChanged()); |
| } |
| |
| TEST_F(PrefetchContainerTest, CookieCopy) { |
| base::HistogramTester histogram_tester; |
| |
| PrefetchContainer prefetch_container( |
| GlobalRenderFrameHostId(1234, 5678), GURL("https://test.com"), |
| PrefetchType(/*use_isolated_network_context=*/true, |
| /*use_prefetch_proxy=*/true), |
| nullptr); |
| prefetch_container.RegisterCookieListener(cookie_manager()); |
| |
| EXPECT_FALSE(prefetch_container.IsIsolatedCookieCopyInProgress()); |
| |
| prefetch_container.OnIsolatedCookieCopyStart(); |
| |
| EXPECT_TRUE(prefetch_container.IsIsolatedCookieCopyInProgress()); |
| |
| // Once the cookie copy process has started, we should stop the cookie |
| // listener. |
| ASSERT_TRUE(SetCookie(GURL("https://test.com"), "test-cookie")); |
| EXPECT_FALSE(prefetch_container.HaveDefaultContextCookiesChanged()); |
| |
| task_environment()->FastForwardBy(base::Milliseconds(10)); |
| prefetch_container.OnIsolatedCookiesReadCompleteAndWriteStart(); |
| task_environment()->FastForwardBy(base::Milliseconds(20)); |
| |
| bool callback_called = false; |
| prefetch_container.SetOnCookieCopyCompleteCallback( |
| base::BindOnce([](bool* callback_called) { *callback_called = true; }, |
| &callback_called)); |
| |
| prefetch_container.OnIsolatedCookieCopyComplete(); |
| |
| EXPECT_FALSE(prefetch_container.IsIsolatedCookieCopyInProgress()); |
| EXPECT_TRUE(callback_called); |
| |
| histogram_tester.ExpectUniqueTimeSample( |
| "PrefetchProxy.AfterClick.Mainframe.CookieReadTime", |
| base::Milliseconds(10), 1); |
| histogram_tester.ExpectUniqueTimeSample( |
| "PrefetchProxy.AfterClick.Mainframe.CookieWriteTime", |
| base::Milliseconds(20), 1); |
| histogram_tester.ExpectUniqueTimeSample( |
| "PrefetchProxy.AfterClick.Mainframe.CookieCopyTime", |
| base::Milliseconds(30), 1); |
| } |
| |
| TEST_F(PrefetchContainerTest, PrefetchProxyPrefetchedResourceUkm) { |
| ukm::TestAutoSetUkmRecorder ukm_recorder; |
| |
| std::unique_ptr<PrefetchContainer> prefetch_container = |
| std::make_unique<PrefetchContainer>( |
| GlobalRenderFrameHostId(1234, 5678), GURL("https://test.com"), |
| PrefetchType(/*use_isolated_network_context=*/true, |
| /*use_prefetch_proxy=*/true), |
| nullptr); |
| |
| network::URLLoaderCompletionStatus completion_status; |
| completion_status.encoded_data_length = 100; |
| completion_status.completion_time = |
| base::TimeTicks() + base::Milliseconds(200); |
| |
| network::mojom::URLResponseHeadPtr head = |
| network::mojom::URLResponseHead::New(); |
| head->load_timing.request_start = base::TimeTicks(); |
| |
| UpdatePrefetchRequestMetrics(prefetch_container.get(), completion_status, |
| head.get()); |
| |
| prefetch_container->TakePrefetchedResponse( |
| std::make_unique<PrefetchedMainframeResponseContainer>( |
| net::IsolationInfo(), std::move(head), |
| std::make_unique<std::string>("test body"))); |
| |
| // Simulates the URL of the prefetch being navigated to and the prefetch being |
| // considered for serving. |
| prefetch_container->OnNavigationToPrefetch(); |
| |
| // Simulate a successful DNS probe for this prefetch. Not this will also |
| // update the status of the prefetch to |
| // |PrefetchStatus::kPrefetchUsedProbeSuccess|. |
| prefetch_container->OnPrefetchProbeResult( |
| PrefetchProbeResult::kDNSProbeSuccess); |
| |
| // Deleting the prefetch container will trigger the recording of the |
| // PrefetchProxy_PrefetchedResource UKM event. |
| prefetch_container.reset(); |
| |
| auto ukm_entries = ukm_recorder.GetEntries( |
| ukm::builders::PrefetchProxy_PrefetchedResource::kEntryName, |
| { |
| ukm::builders::PrefetchProxy_PrefetchedResource::kResourceTypeName, |
| ukm::builders::PrefetchProxy_PrefetchedResource::kStatusName, |
| ukm::builders::PrefetchProxy_PrefetchedResource::kLinkClickedName, |
| ukm::builders::PrefetchProxy_PrefetchedResource::kDataLengthName, |
| ukm::builders::PrefetchProxy_PrefetchedResource::kFetchDurationMSName, |
| ukm::builders::PrefetchProxy_PrefetchedResource:: |
| kISPFilteringStatusName, |
| ukm::builders::PrefetchProxy_PrefetchedResource:: |
| kNavigationStartToFetchStartMSName, |
| ukm::builders::PrefetchProxy_PrefetchedResource::kLinkPositionName, |
| }); |
| |
| ASSERT_EQ(ukm_entries.size(), 1U); |
| EXPECT_EQ(ukm_entries[0].source_id, ukm::kInvalidSourceId); |
| |
| const auto& ukm_metrics = ukm_entries[0].metrics; |
| |
| ASSERT_TRUE( |
| ukm_metrics.find( |
| ukm::builders::PrefetchProxy_PrefetchedResource::kResourceTypeName) != |
| ukm_metrics.end()); |
| EXPECT_EQ( |
| ukm_metrics.at( |
| ukm::builders::PrefetchProxy_PrefetchedResource::kResourceTypeName), |
| /*mainfrmae*/ 1); |
| |
| ASSERT_TRUE( |
| ukm_metrics.find( |
| ukm::builders::PrefetchProxy_PrefetchedResource::kStatusName) != |
| ukm_metrics.end()); |
| EXPECT_EQ(ukm_metrics.at( |
| ukm::builders::PrefetchProxy_PrefetchedResource::kStatusName), |
| static_cast<int>(PrefetchStatus::kPrefetchUsedProbeSuccess)); |
| |
| ASSERT_TRUE( |
| ukm_metrics.find( |
| ukm::builders::PrefetchProxy_PrefetchedResource::kLinkClickedName) != |
| ukm_metrics.end()); |
| EXPECT_EQ( |
| ukm_metrics.at( |
| ukm::builders::PrefetchProxy_PrefetchedResource::kLinkClickedName), |
| 1); |
| |
| ASSERT_TRUE( |
| ukm_metrics.find( |
| ukm::builders::PrefetchProxy_PrefetchedResource::kDataLengthName) != |
| ukm_metrics.end()); |
| EXPECT_EQ( |
| ukm_metrics.at( |
| ukm::builders::PrefetchProxy_PrefetchedResource::kDataLengthName), |
| ukm::GetExponentialBucketMinForBytes(100)); |
| |
| ASSERT_TRUE(ukm_metrics.find(ukm::builders::PrefetchProxy_PrefetchedResource:: |
| kFetchDurationMSName) != ukm_metrics.end()); |
| EXPECT_EQ(ukm_metrics.at(ukm::builders::PrefetchProxy_PrefetchedResource:: |
| kFetchDurationMSName), |
| 200); |
| |
| ASSERT_TRUE(ukm_metrics.find(ukm::builders::PrefetchProxy_PrefetchedResource:: |
| kISPFilteringStatusName) != |
| ukm_metrics.end()); |
| EXPECT_EQ(ukm_metrics.at(ukm::builders::PrefetchProxy_PrefetchedResource:: |
| kISPFilteringStatusName), |
| static_cast<int>(PrefetchProbeResult::kDNSProbeSuccess)); |
| |
| // These fields are not set and should not be in the UKM event. |
| EXPECT_TRUE(ukm_metrics.find(ukm::builders::PrefetchProxy_PrefetchedResource:: |
| kNavigationStartToFetchStartMSName) == |
| ukm_metrics.end()); |
| EXPECT_TRUE( |
| ukm_metrics.find( |
| ukm::builders::PrefetchProxy_PrefetchedResource::kLinkPositionName) == |
| ukm_metrics.end()); |
| } |
| |
| TEST_F(PrefetchContainerTest, PrefetchProxyPrefetchedResourceUkm_NothingSet) { |
| ukm::TestAutoSetUkmRecorder ukm_recorder; |
| |
| std::unique_ptr<PrefetchContainer> prefetch_container = |
| std::make_unique<PrefetchContainer>( |
| GlobalRenderFrameHostId(1234, 5678), GURL("https://test.com"), |
| PrefetchType(/*use_isolated_network_context=*/true, |
| /*use_prefetch_proxy=*/true), |
| nullptr); |
| prefetch_container.reset(); |
| |
| auto ukm_entries = ukm_recorder.GetEntries( |
| ukm::builders::PrefetchProxy_PrefetchedResource::kEntryName, |
| { |
| ukm::builders::PrefetchProxy_PrefetchedResource::kResourceTypeName, |
| ukm::builders::PrefetchProxy_PrefetchedResource::kStatusName, |
| ukm::builders::PrefetchProxy_PrefetchedResource::kLinkClickedName, |
| ukm::builders::PrefetchProxy_PrefetchedResource::kDataLengthName, |
| ukm::builders::PrefetchProxy_PrefetchedResource::kFetchDurationMSName, |
| ukm::builders::PrefetchProxy_PrefetchedResource:: |
| kISPFilteringStatusName, |
| }); |
| |
| ASSERT_EQ(ukm_entries.size(), 1U); |
| EXPECT_EQ(ukm_entries[0].source_id, ukm::kInvalidSourceId); |
| |
| const auto& ukm_metrics = ukm_entries[0].metrics; |
| ASSERT_TRUE( |
| ukm_metrics.find( |
| ukm::builders::PrefetchProxy_PrefetchedResource::kResourceTypeName) != |
| ukm_metrics.end()); |
| EXPECT_EQ( |
| ukm_metrics.at( |
| ukm::builders::PrefetchProxy_PrefetchedResource::kResourceTypeName), |
| /*mainfrmae*/ 1); |
| |
| ASSERT_TRUE( |
| ukm_metrics.find( |
| ukm::builders::PrefetchProxy_PrefetchedResource::kStatusName) != |
| ukm_metrics.end()); |
| EXPECT_EQ(ukm_metrics.at( |
| ukm::builders::PrefetchProxy_PrefetchedResource::kStatusName), |
| static_cast<int>(PrefetchStatus::kPrefetchNotStarted)); |
| |
| ASSERT_TRUE( |
| ukm_metrics.find( |
| ukm::builders::PrefetchProxy_PrefetchedResource::kLinkClickedName) != |
| ukm_metrics.end()); |
| EXPECT_EQ( |
| ukm_metrics.at( |
| ukm::builders::PrefetchProxy_PrefetchedResource::kLinkClickedName), |
| 0); |
| |
| EXPECT_TRUE( |
| ukm_metrics.find( |
| ukm::builders::PrefetchProxy_PrefetchedResource::kDataLengthName) == |
| ukm_metrics.end()); |
| EXPECT_TRUE(ukm_metrics.find(ukm::builders::PrefetchProxy_PrefetchedResource:: |
| kFetchDurationMSName) == ukm_metrics.end()); |
| EXPECT_TRUE(ukm_metrics.find(ukm::builders::PrefetchProxy_PrefetchedResource:: |
| kISPFilteringStatusName) == |
| ukm_metrics.end()); |
| } |
| |
| } // namespace content |