910e62b5创建于 1月15日历史提交
// 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