// Copyright 2014 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "components/variations/net/variations_http_headers.h"

#include <string>

#include "base/containers/flat_map.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/scoped_command_line.h"
#include "base/test/task_environment.h"
#include "build/build_config.h"
#include "components/variations/net/variations_flags.h"
#include "components/variations/variations.mojom.h"
#include "net/base/isolation_info.h"
#include "net/base/network_handle.h"
#include "net/cookies/site_for_cookies.h"
#include "net/traffic_annotation/network_traffic_annotation_test_helper.h"
#include "net/url_request/url_request.h"
#include "net/url_request/url_request_context.h"
#include "net/url_request/url_request_context_builder.h"
#include "net/url_request/url_request_test_util.h"
#include "services/network/public/cpp/resource_request.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "url/gurl.h"
#include "url/origin.h"

namespace variations {
namespace {

// Returns a ResourceRequest created from the given values.
network::ResourceRequest CreateResourceRequest(
    const std::string& request_initiator_url,
    bool is_outermost_main_frame,
    bool has_trusted_params,
    const std::string& isolation_info_top_frame_origin_url,
    const std::string& isolation_info_frame_origin_url) {
  network::ResourceRequest request;
  if (request_initiator_url.empty())
    return request;

  request.request_initiator = url::Origin::Create(GURL(request_initiator_url));
  request.is_outermost_main_frame = is_outermost_main_frame;
  if (!has_trusted_params)
    return request;

  request.trusted_params = network::ResourceRequest::TrustedParams();
  if (isolation_info_top_frame_origin_url.empty())
    return request;

  request.trusted_params->isolation_info = net::IsolationInfo::Create(
      net::IsolationInfo::RequestType::kOther,
      url::Origin::Create(GURL(isolation_info_top_frame_origin_url)),
      url::Origin::Create(GURL(isolation_info_frame_origin_url)),
      net::SiteForCookies());
  return request;
}

// Wraps AppendVariationsHeaderWithCustomValue().
void AppendVariationsHeader(const GURL& destination,
                            Owner owner,
                            network::ResourceRequest* request) {
  base::flat_map<variations::mojom::GoogleWebVisibility, std::string> headers =
      {{variations::mojom::GoogleWebVisibility::FIRST_PARTY, "abc123"},
       {variations::mojom::GoogleWebVisibility::ANY, "xyz456"}};

  AppendVariationsHeaderWithCustomValue(
      destination, InIncognito::kNo,
      variations::mojom::VariationsHeaders::New(headers).get(), owner, request);
}

// Returns a URLRequest created from the given values.
std::unique_ptr<net::URLRequest> CreateURLRequest(
    net::URLRequestContext* context,
    const std::string& request_initiator_url,
    const std::string& isolation_info_top_frame_origin_url,
    const std::string& isolation_info_frame_origin_url) {
  std::unique_ptr<net::URLRequest> request = context->CreateRequest(
      GURL("https://foo.google.com"), net::DEFAULT_PRIORITY, nullptr,
      TRAFFIC_ANNOTATION_FOR_TESTS, net::handles::kInvalidNetworkHandle);

  if (!request_initiator_url.empty()) {
    request->set_initiator(url::Origin::Create(GURL(request_initiator_url)));
  }

  if (!isolation_info_top_frame_origin_url.empty()) {
    request->set_isolation_info(net::IsolationInfo::Create(
        net::IsolationInfo::RequestType::kOther,
        url::Origin::Create(GURL(isolation_info_top_frame_origin_url)),
        url::Origin::Create(GURL(isolation_info_frame_origin_url)),
        net::SiteForCookies()));
  }

  return request;
}

// Wraps AppendVariationsHeaderWithCustomValue().
void AppendVariationsHeader(const GURL& destination, net::URLRequest* request) {
  base::flat_map<variations::mojom::GoogleWebVisibility, std::string> headers =
      {{variations::mojom::GoogleWebVisibility::FIRST_PARTY, "abc123"},
       {variations::mojom::GoogleWebVisibility::ANY, "xyz456"}};

  AppendVariationsHeaderWithCustomValue(
      destination, InIncognito::kNo,
      variations::mojom::VariationsHeaders::New(headers).get(), request);
}

}  // namespace

TEST(VariationsHttpHeadersTest, ShouldAppendVariationsHeader) {
  struct {
    const char* url;
    bool should_append_headers;
  } cases[] = {
      {"http://google.com", false},
      {"https://google.com", true},
      {"http://www.google.com", false},
      {"https://www.google.com", true},
      {"http://m.google.com", false},
      {"https://m.google.com", true},
      {"http://google.ca", false},
      {"https://google.ca", true},
      {"http://google.co.uk", false},
      {"https://google.co.uk", true},
      {"http://google.co.uk:8080/", false},
      {"https://google.co.uk:8080/", true},
      {"http://www.google.co.uk:8080/", false},
      {"https://www.google.co.uk:8080/", true},
      {"https://google", false},

      {"http://youtube.com", false},
      {"https://youtube.com", true},
      {"http://www.youtube.com", false},
      {"https://www.youtube.com", true},
      {"http://www.youtube.ca", false},
      {"https://www.youtube.ca", true},
      {"http://www.youtube.co.uk:8080/", false},
      {"https://www.youtube.co.uk:8080/", true},
      {"https://youtube", false},

      {"https://www.yahoo.com", false},

      {"http://ad.doubleclick.net", false},
      {"https://ad.doubleclick.net", true},
      {"https://a.b.c.doubleclick.net", true},
      {"https://a.b.c.doubleclick.net:8081", true},
      {"http://www.doubleclick.com", false},
      {"https://www.doubleclick.com", true},
      {"https://www.doubleclick.org", false},
      {"http://www.doubleclick.net.com", false},
      {"https://www.doubleclick.net.com", false},

      {"http://ad.googlesyndication.com", false},
      {"https://ad.googlesyndication.com", true},
      {"https://a.b.c.googlesyndication.com", true},
      {"https://a.b.c.googlesyndication.com:8080", true},
      {"http://www.doubleclick.edu", false},
      {"http://www.googlesyndication.com.edu", false},
      {"https://www.googlesyndication.com.com", false},

      {"http://www.googleadservices.com", false},
      {"https://www.googleadservices.com", true},
      {"http://www.googleadservices.com:8080", false},
      {"https://www.googleadservices.com:8080", true},
      {"https://www.internal.googleadservices.com", true},
      {"https://www2.googleadservices.com", true},
      {"https://www.googleadservices.org", false},
      {"https://www.googleadservices.com.co.uk", false},

      {"http://WWW.ANDROID.COM", false},
      {"https://WWW.ANDROID.COM", true},
      {"http://www.android.com", false},
      {"https://www.android.com", true},
      {"http://www.doubleclick.com", false},
      {"https://www.doubleclick.com", true},
      {"http://www.doubleclick.net", false},
      {"https://www.doubleclick.net", true},
      {"http://www.ggpht.com", false},
      {"https://www.ggpht.com", true},
      {"http://www.googleadservices.com", false},
      {"https://www.googleadservices.com", true},
      {"http://www.googleapis.com", false},
      {"https://www.googleapis.com", true},
      {"http://www.googlesyndication.com", false},
      {"https://www.googlesyndication.com", true},
      {"http://www.googleusercontent.com", false},
      {"https://www.googleusercontent.com", true},
      {"http://www.googlevideo.com", false},
      {"https://www.googlevideo.com", true},
      {"http://ssl.gstatic.com", false},
      {"https://ssl.gstatic.com", true},
      {"http://www.gstatic.com", false},
      {"https://www.gstatic.com", true},
      {"http://www.ytimg.com", false},
      {"https://www.ytimg.com", true},
      {"https://wwwytimg.com", false},
      {"https://ytimg.com", false},

      {"https://www.android.org", false},
      {"https://www.doubleclick.org", false},
      {"http://www.doubleclick.net", false},
      {"https://www.doubleclick.net", true},
      {"https://www.ggpht.org", false},
      {"https://www.googleadservices.org", false},
      {"https://www.googleapis.org", false},
      {"https://www.googlesyndication.org", false},
      {"https://www.googleusercontent.org", false},
      {"https://www.googlevideo.org", false},
      {"https://ssl.gstatic.org", false},
      {"https://www.gstatic.org", false},
      {"https://www.ytimg.org", false},

      {"http://a.b.android.com", false},
      {"https://a.b.android.com", true},
      {"http://a.b.doubleclick.com", false},
      {"https://a.b.doubleclick.com", true},
      {"http://a.b.doubleclick.net", false},
      {"https://a.b.doubleclick.net", true},
      {"http://a.b.ggpht.com", false},
      {"https://a.b.ggpht.com", true},
      {"http://a.b.googleadservices.com", false},
      {"https://a.b.googleadservices.com", true},
      {"http://a.b.googleapis.com", false},
      {"https://a.b.googleapis.com", true},
      {"http://a.b.googlesyndication.com", false},
      {"https://a.b.googlesyndication.com", true},
      {"http://a.b.googleusercontent.com", false},
      {"https://a.b.googleusercontent.com", true},
      {"http://a.b.googlevideo.com", false},
      {"https://a.b.googlevideo.com", true},
      {"http://ssl.gstatic.com", false},
      {"https://ssl.gstatic.com", true},
      {"http://a.b.gstatic.com", false},
      {"https://a.b.gstatic.com", true},
      {"http://a.b.ytimg.com", false},
      {"https://a.b.ytimg.com", true},
      {"http://googleweblight.com", false},
      {"https://googleweblight.com", true},
      {"http://wwwgoogleweblight.com", false},
      {"https://www.googleweblight.com", false},
      {"https://a.b.googleweblight.com", false},

      {"http://a.b.litepages.googlezip.net", false},
      {"https://litepages.googlezip.net", false},
      {"https://a.litepages.googlezip.net", true},
      {"https://a.b.litepages.googlezip.net", true},

      {"https://127.0.0.1", false},
      {"http://127.0.0.1", false},
      {"https://127.0.0.1:12345", false},
      {"http://127.0.0.1:12345", false},
  };

  for (const auto& c : cases) {
    const GURL url(c.url);
    EXPECT_EQ(c.should_append_headers,
              ShouldAppendVariationsHeaderForTesting(url, InIncognito::kNo))
        << url;
  }
}

#if BUILDFLAG(IS_IOS)
TEST(VariationsHttpHeadersTest, ShouldAppendVariationsHeaderLocalhost) {
  base::test::ScopedCommandLine scoped_command_line;
  scoped_command_line.GetProcessCommandLine()->AppendSwitch(
      variations::kAppendVariationsHeadersToLocalhostForTesting);
  struct {
    const char* url;
    bool should_append_headers;
  } cases[] = {
      {"https://127.0.0.1", true},
      {"http://127.0.0.1", true},
      {"https://127.0.0.1:12345", true},
      {"http://127.0.0.1:12345", true},
  };

  for (const auto& c : cases) {
    const GURL url(c.url);
    EXPECT_EQ(c.should_append_headers,
              ShouldAppendVariationsHeaderForTesting(url, InIncognito::kNo))
        << url;
  }
}
#endif  // BUILDFLAG(IS_IOS)

struct PopulateRequestContextHistogramData {
  const char* request_initiator_url;
  bool is_outermost_main_frame;
  bool has_trusted_params;
  const char* isolation_info_top_frame_origin_url;
  const char* isolation_info_frame_origin_url;
  bool is_top_level_google_owned;
  int bucket;
  const char* name;
};

class PopulateRequestContextHistogramTest
    : public testing::TestWithParam<PopulateRequestContextHistogramData> {
 public:
  static const PopulateRequestContextHistogramData kCases[];

  // Required to use ObserverListThreadSafe::AddObserver() from:
  //    base::FieldTrialList::AddObserver
  //    variations::VariationsIdsProvider::InitVariationIDsCacheIfNeeded
  //    variations::VariationsIdsProvider::GetClientDataHeader
  //    variations::VariationsHeaderHelper::VariationsHeaderHelper
  //    variations::AppendVariationsHeaderUnknownSignedIn
  base::test::SingleThreadTaskEnvironment task_environment_;
};

const PopulateRequestContextHistogramData
    PopulateRequestContextHistogramTest::kCases[] = {
        {"", false, false, "", "", false, 0, "kBrowserInitiated"},
        {"chrome://newtab/", false, false, "", "", false, 1,
         "kInternalChromePageInitiated"},
        {"chrome-search://most-visited/title.html", false, false, "", "", false,
         1, "kInternalChromePageInitiated"},
        {"https://www.youtube.com/", true, false, "", "", false, 2,
         "kGooglePageInitiated"},
        {"https://docs.google.com/", false, true, "https://drive.google.com/",
         "https://docs.google.com/", false, 3,
         "kGoogleSubFrameOnGooglePageInitiated with TrustedParams"},
        {"https://docs.google.com/", false, false, "", "", true, 3,
         "kGoogleSubFrameOnGooglePageInitiated without TrustedParams"},
        {"https://www.un.org/", false, false, "", "", false, 4,
         "kNonGooglePageInitiated"},
        // Bucket 5, kNoTrustedParams, is deprecated.
        {"https://foo.google.com/", false, true, "", "", false, 6,
         "kNoIsolationInfo"},
        {"https://foo.gstatic.com/", false, true, "https://www.lexico.com/", "",
         false, 7,
         "kGoogleSubFrameOnNonGooglePageInitiated with TrustedParams"},
        {"https://foo.gstatic.com/", false, false, "", "", false, 7,
         "kGoogleSubFrameOnNonGooglePageInitiated without TrustedParams"},
        // Bucket 8, kNonGooglePageInitiatedFromFrameOrigin, is deprecated.
};

TEST(VariationsHttpHeadersTest, PopulateDomainOwnerHistogram) {
  const GURL destination("https://fonts.googleapis.com/foo");
  network::ResourceRequest request = CreateResourceRequest(
      /*request_initiator_url=*/"https://docs.google.com/",
      /*is_outermost_main_frame=*/false,
      /*has_trusted_params=*/false,
      /*isolation_info_top_frame_origin_url=*/"",
      /*isolation_info_frame_origin_url=*/"");

  base::HistogramTester tester;
  AppendVariationsHeader(destination, Owner::kUnknownFromRenderer, &request);
  AppendVariationsHeader(destination, Owner::kUnknown, &request);
  AppendVariationsHeader(destination, Owner::kNotGoogle, &request);
  AppendVariationsHeader(destination, Owner::kGoogle, &request);
  EXPECT_THAT(tester.GetAllSamples("Variations.Headers.DomainOwner"),
              testing::ElementsAre(base::Bucket(0, 1), base::Bucket(1, 1),
                                   base::Bucket(2, 1), base::Bucket(3, 1)));
}

INSTANTIATE_TEST_SUITE_P(
    VariationsHttpHeadersTest,
    PopulateRequestContextHistogramTest,
    testing::ValuesIn(PopulateRequestContextHistogramTest::kCases));

TEST_P(PopulateRequestContextHistogramTest, PopulateRequestContextHistogram) {
  PopulateRequestContextHistogramData data = GetParam();
  SCOPED_TRACE(data.name);

  network::ResourceRequest request = CreateResourceRequest(
      data.request_initiator_url, data.is_outermost_main_frame,
      data.has_trusted_params, data.isolation_info_top_frame_origin_url,
      data.isolation_info_frame_origin_url);

  base::HistogramTester tester;
  AppendVariationsHeader(
      GURL("https://foo.google.com"),
      data.is_top_level_google_owned ? Owner::kGoogle : Owner::kNotGoogle,
      &request);

  // Verify that the histogram has a single sample corresponding to the request
  // context category.
  const std::string histogram = "Variations.Headers.RequestContextCategory";
  tester.ExpectUniqueSample(histogram, data.bucket, 1);
}

struct PopulateRequestContextHistogramURLRequestData {
  const char* request_initiator_url;
  const char* isolation_info_top_frame_origin_url;
  const char* isolation_info_frame_origin_url;
  int bucket;
  const char* name;
};

class PopulateRequestContextHistogramURLRequestTest
    : public testing::TestWithParam<
          PopulateRequestContextHistogramURLRequestData> {
 public:
  static const PopulateRequestContextHistogramURLRequestData kCases[];

  base::test::SingleThreadTaskEnvironment task_environment_;
};

const PopulateRequestContextHistogramURLRequestData
    PopulateRequestContextHistogramURLRequestTest::kCases[] = {
        {"", "", "", 0, "kBrowserInitiated"},
        {"chrome://newtab/", "", "", 1, "kInternalChromePageInitiated"},
        {"https://docs.google.com/", "https://drive.google.com/",
         "https://docs.google.com/", 3, "kGoogleSubFrameOnGooglePageInitiated"},
        {"https://www.un.org/", "", "", 4, "kNonGooglePageInitiated"},
        {"https://foo.google.com/", "", "", 6, "kNoIsolationInfo"},
        {"https://foo.gstatic.com/", "https://www.lexico.com/", "", 7,
         "kGoogleSubFrameOnNonGooglePageInitiated"},
};

INSTANTIATE_TEST_SUITE_P(
    VariationsHttpHeadersTest,
    PopulateRequestContextHistogramURLRequestTest,
    testing::ValuesIn(PopulateRequestContextHistogramURLRequestTest::kCases));

TEST_P(PopulateRequestContextHistogramURLRequestTest,
       PopulateRequestContextHistogram) {
  PopulateRequestContextHistogramURLRequestData data = GetParam();
  SCOPED_TRACE(data.name);

  auto builder = net::CreateTestURLRequestContextBuilder();
  auto context = builder->Build();
  std::unique_ptr<net::URLRequest> request =
      CreateURLRequest(context.get(), data.request_initiator_url,
                       data.isolation_info_top_frame_origin_url,
                       data.isolation_info_frame_origin_url);

  base::HistogramTester tester;
  AppendVariationsHeader(GURL("https://foo.google.com"), request.get());

  // Verify that the histogram has a single sample corresponding to the request
  // context category.
  const std::string histogram = "Variations.Headers.RequestContextCategory";
  tester.ExpectUniqueSample(histogram, data.bucket, 1);
}

}  // namespace variations
