| // Copyright 2024 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_match_resolver.h" |
| |
| #include "testing/gmock/include/gmock/gmock.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| #include "url/gurl.h" |
| |
| namespace content { |
| namespace { |
| |
| // Mock `PrefetchContainer` to test `CollectMatchCandidatesGeneric()`. |
| class MockContainer { |
| public: |
| struct Args { |
| blink::DocumentToken document_token; |
| GURL url; |
| PrefetchServableState servable_state; |
| std::optional<net::HttpNoVarySearchData> no_vary_search_hint; |
| std::optional<net::HttpNoVarySearchData> no_vary_search_data; |
| }; |
| |
| explicit MockContainer(MockContainer::Args args) |
| : key_(PrefetchKey(args.document_token, args.url)), |
| servable_state_(args.servable_state), |
| no_vary_search_hint_(args.no_vary_search_hint), |
| no_vary_search_data_(args.no_vary_search_data), |
| prefetch_status_(PrefetchStatus::kPrefetchSuccessful) {} |
| ~MockContainer() = default; |
| |
| const GURL& GetURL() const { return key_.url(); } |
| |
| PrefetchServableState GetServableState( |
| base::TimeDelta cacheable_duration) const { |
| return servable_state_; |
| } |
| |
| bool HasPrefetchStatus() const { return prefetch_status_.has_value(); } |
| PrefetchStatus GetPrefetchStatus() const { |
| DCHECK(prefetch_status_); |
| return prefetch_status_.value(); |
| } |
| |
| bool IsNoVarySearchHeaderMatch(const GURL& url) const { |
| const std::optional<net::HttpNoVarySearchData>& no_vary_search_data = |
| GetNoVarySearchData(); |
| return no_vary_search_data && |
| no_vary_search_data->AreEquivalent(url, GetURL()); |
| } |
| |
| bool ShouldWaitForNoVarySearchHeader(const GURL& url) const { |
| const std::optional<net::HttpNoVarySearchData>& no_vary_search_hint = |
| GetNoVarySearchHint(); |
| // It's not trivial to implement `PrefetchContainer::GetNonRedirectHead()`. |
| // Use `servable_state_` instead. |
| bool simulate_get_non_redirect_head_is_null = |
| (servable_state_ != PrefetchServableState::kServable); |
| return simulate_get_non_redirect_head_is_null && no_vary_search_hint && |
| no_vary_search_hint->AreEquivalent(url, GetURL()); |
| } |
| |
| // We don't test on this property. |
| bool IsDecoy() const { return false; } |
| |
| void SetServingPageMetrics(base::WeakPtr<PrefetchServingPageMetricsContainer> |
| serving_page_metrics_container) {} |
| void UpdateServingPageMetrics() {} |
| |
| const PrefetchKey& key() const { return key_; } |
| const std::optional<net::HttpNoVarySearchData>& GetNoVarySearchHint() const { |
| return no_vary_search_hint_; |
| } |
| const std::optional<net::HttpNoVarySearchData>& GetNoVarySearchData() const { |
| return no_vary_search_data_; |
| } |
| |
| private: |
| PrefetchKey key_; |
| PrefetchServableState servable_state_; |
| std::optional<net::HttpNoVarySearchData> no_vary_search_hint_; |
| std::optional<net::HttpNoVarySearchData> no_vary_search_data_; |
| std::optional<PrefetchStatus> prefetch_status_; |
| }; |
| |
| std::ostream& operator<<(std::ostream& ostream, const MockContainer&) { |
| return ostream; |
| } |
| |
| class CollectMatchCandidatesTestHelper { |
| public: |
| CollectMatchCandidatesTestHelper() = default; |
| ~CollectMatchCandidatesTestHelper() = default; |
| |
| void Add(MockContainer::Args args) { |
| auto container = std::make_unique<MockContainer>(std::move(args)); |
| owned_prefetches_[container->key()] = std::move(container); |
| } |
| |
| std::vector<PrefetchKey> KeysOfCollectMatchCandidatesGeneric( |
| const PrefetchKey& navigated_key, |
| bool is_nav_prerender) { |
| std::vector<PrefetchKey> candidate_keys; |
| // We must bind the following value instead of using `std::get()` in `for` |
| // due to a lifetime issue before C++23: |
| // https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2022/p2718r0.html |
| auto [candidates, _] = CollectMatchCandidatesGeneric( |
| owned_prefetches_, navigated_key, is_nav_prerender, |
| /*serving_page_metrics_container=*/nullptr); |
| for (const auto* container : candidates) { |
| candidate_keys.push_back(container->key()); |
| } |
| return candidate_keys; |
| } |
| |
| void Assert(const base::Location& location, |
| const PrefetchKey& navigated_key, |
| bool is_nav_prerender, |
| const std::vector<PrefetchKey>& candidate_keys) { |
| SCOPED_TRACE(::testing::Message() |
| << "from \033[31m" << location.ToString() << "\033[39m"); |
| |
| EXPECT_THAT( |
| KeysOfCollectMatchCandidatesGeneric(navigated_key, is_nav_prerender), |
| testing::UnorderedElementsAreArray(candidate_keys)); |
| } |
| |
| private: |
| std::map<PrefetchKey, std::unique_ptr<MockContainer>> owned_prefetches_; |
| }; |
| |
| TEST(CollectMatchCandidates, DistinguishesDocumentToken) { |
| CollectMatchCandidatesTestHelper helper; |
| blink::DocumentToken document_token1; |
| blink::DocumentToken document_token2; |
| |
| helper.Add({ |
| .document_token = document_token1, |
| .url = GURL("https://a.example.com/"), |
| .servable_state = PrefetchServableState::kServable, |
| }); |
| helper.Add({ |
| .document_token = document_token2, |
| .url = GURL("https://a.example.com/"), |
| .servable_state = PrefetchServableState::kServable, |
| }); |
| |
| helper.Assert(FROM_HERE, |
| PrefetchKey(document_token1, GURL("https://a.example.com/")), |
| /*is_nav_prerender=*/false, |
| {PrefetchKey(document_token1, GURL("https://a.example.com/"))}); |
| |
| helper.Assert(FROM_HERE, |
| PrefetchKey(std::nullopt, GURL("https://a.example.com/")), |
| /*is_nav_prerender=*/false, {}); |
| } |
| |
| TEST(CollectMatchCandidates, DistingushesUrl) { |
| CollectMatchCandidatesTestHelper helper; |
| blink::DocumentToken document_token; |
| |
| helper.Add({ |
| .document_token = document_token, |
| .url = GURL("https://a.example.com/"), |
| .servable_state = PrefetchServableState::kServable, |
| }); |
| helper.Add({ |
| .document_token = document_token, |
| .url = GURL("https://b.example.com/"), |
| .servable_state = PrefetchServableState::kServable, |
| }); |
| |
| helper.Assert(FROM_HERE, |
| PrefetchKey(document_token, GURL("https://a.example.com/")), |
| /*is_nav_prerender=*/false, |
| {PrefetchKey(document_token, GURL("https://a.example.com/"))}); |
| |
| helper.Assert(FROM_HERE, |
| PrefetchKey(document_token, GURL("https://c.example.com/")), |
| /*is_nav_prerender=*/false, {}); |
| } |
| |
| TEST(CollectMatchCandidates, RejectsNotServable) { |
| CollectMatchCandidatesTestHelper helper; |
| blink::DocumentToken document_token; |
| |
| helper.Add({ |
| .document_token = document_token, |
| .url = GURL("https://servable.example.com/"), |
| .servable_state = PrefetchServableState::kServable, |
| }); |
| helper.Add({ |
| .document_token = document_token, |
| .url = GURL("https://not-servable.example.com/"), |
| .servable_state = PrefetchServableState::kNotServable, |
| }); |
| helper.Add({ |
| .document_token = document_token, |
| .url = GURL("https://should-block-until-head-received.example.com/"), |
| .servable_state = PrefetchServableState::kShouldBlockUntilHeadReceived, |
| }); |
| |
| helper.Assert( |
| FROM_HERE, |
| PrefetchKey(document_token, GURL("https://servable.example.com/")), |
| /*is_nav_prerender=*/false, |
| {PrefetchKey(document_token, GURL("https://servable.example.com/"))}); |
| |
| helper.Assert( |
| FROM_HERE, |
| PrefetchKey(document_token, GURL("https://not-servable.example.com/")), |
| /*is_nav_prerender=*/false, {}); |
| |
| helper.Assert( |
| FROM_HERE, |
| PrefetchKey( |
| document_token, |
| GURL("https://should-block-until-head-received.example.com/")), |
| /*is_nav_prerender=*/false, |
| {PrefetchKey( |
| document_token, |
| GURL("https://should-block-until-head-received.example.com/"))}); |
| } |
| |
| TEST(CollectMatchCandidates, |
| IncludesShouldBlockUntilEligibilityGotIfIsLikelyAheadOfPrerender) { |
| CollectMatchCandidatesTestHelper helper; |
| blink::DocumentToken document_token; |
| |
| helper.Add({ |
| .document_token = document_token, |
| .url = GURL("https://prerender.example.com/"), |
| .servable_state = PrefetchServableState::kShouldBlockUntilEligibilityGot, |
| }); |
| helper.Add({ |
| .document_token = document_token, |
| .url = GURL("https://not-prerender.example.com/"), |
| .servable_state = PrefetchServableState::kShouldBlockUntilEligibilityGot, |
| }); |
| |
| helper.Assert( |
| FROM_HERE, |
| PrefetchKey(document_token, GURL("https://prerender.example.com/")), |
| /*is_nav_prerender=*/true, |
| {PrefetchKey(document_token, GURL("https://prerender.example.com/"))}); |
| |
| helper.Assert( |
| FROM_HERE, |
| PrefetchKey(document_token, GURL("https://not-prerender.example.com/")), |
| /*is_nav_prerender=*/false, {}); |
| } |
| |
| TEST(CollectMatchCandidates, ChecksNoVarySearchHintAndHeader) { |
| CollectMatchCandidatesTestHelper helper; |
| blink::DocumentToken document_token; |
| |
| helper.Add({ |
| .document_token = document_token, |
| .url = GURL("https://a.example.com/"), |
| .servable_state = PrefetchServableState::kServable, |
| .no_vary_search_hint = std::nullopt, |
| .no_vary_search_data = std::nullopt, |
| }); |
| helper.Add({ |
| .document_token = document_token, |
| .url = GURL("https://a.example.com/?distinguish=true"), |
| .servable_state = PrefetchServableState::kServable, |
| .no_vary_search_hint = std::nullopt, |
| .no_vary_search_data = std::nullopt, |
| }); |
| helper.Add({ |
| .document_token = document_token, |
| .url = GURL("https://a.example.com/?ignore=onlyHeader"), |
| .servable_state = PrefetchServableState::kServable, |
| .no_vary_search_hint = std::nullopt, |
| .no_vary_search_data = |
| net::HttpNoVarySearchData::CreateFromNoVaryParams({"ignore"}, true), |
| }); |
| helper.Add({ |
| .document_token = document_token, |
| .url = GURL("https://a.example.com/?ignore=onlyHint"), |
| .servable_state = PrefetchServableState::kShouldBlockUntilHeadReceived, |
| .no_vary_search_hint = |
| net::HttpNoVarySearchData::CreateFromNoVaryParams({"ignore"}, true), |
| .no_vary_search_data = std::nullopt, |
| }); |
| helper.Add({ |
| .document_token = document_token, |
| .url = GURL("https://a.example.com/?ignore=bothHintAndHeader"), |
| .servable_state = PrefetchServableState::kShouldBlockUntilHeadReceived, |
| .no_vary_search_hint = |
| net::HttpNoVarySearchData::CreateFromNoVaryParams({"ignore"}, true), |
| .no_vary_search_data = |
| net::HttpNoVarySearchData::CreateFromNoVaryParams({"ignore"}, true), |
| }); |
| helper.Add({ |
| .document_token = document_token, |
| .url = GURL("https://a.example.com/?distinguish=hintButContradictHeader"), |
| .servable_state = PrefetchServableState::kServable, |
| .no_vary_search_hint = net::HttpNoVarySearchData::CreateFromNoVaryParams( |
| {"distinguish"}, true), |
| .no_vary_search_data = std::nullopt, |
| }); |
| |
| helper.Assert( |
| FROM_HERE, PrefetchKey(document_token, GURL("https://a.example.com/")), |
| /*is_nav_prerender=*/false, |
| { |
| PrefetchKey(document_token, GURL("https://a.example.com/")), |
| PrefetchKey(document_token, |
| GURL("https://a.example.com/?ignore=onlyHeader")), |
| PrefetchKey(document_token, |
| GURL("https://a.example.com/?ignore=onlyHint")), |
| PrefetchKey(document_token, |
| GURL("https://a.example.com/?ignore=bothHintAndHeader")), |
| }); |
| } |
| |
| } // namespace |
| } // namespace content |