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

#include "net/proxy_resolution/proxy_list.h"

#include <vector>

#include "net/base/net_errors.h"
#include "net/base/proxy_server.h"
#include "net/base/proxy_string_util.h"
#include "net/log/net_log_with_source.h"
#include "net/proxy_resolution/proxy_retry_info.h"
#include "net/test/gtest_util.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"

using net::test::IsOk;

namespace net {

namespace {

// Test parsing from a PAC string.
TEST(ProxyListTest, SetFromPacString) {
  const struct {
    const char* pac_input;
    const char* debug_output;
  } tests[] = {
    // Valid inputs:
    {  "PROXY foopy:10",
       "PROXY foopy:10",
    },
    {  " DIRECT",  // leading space.
       "DIRECT",
    },
    {  "PROXY foopy1 ; proxy foopy2;\t DIRECT",
       "PROXY foopy1:80;PROXY foopy2:80;DIRECT",
    },
    {  "proxy foopy1 ; SOCKS foopy2",
       "PROXY foopy1:80;SOCKS foopy2:1080",
    },
    // Try putting DIRECT first.
    {  "DIRECT ; proxy foopy1 ; DIRECT ; SOCKS5 foopy2;DIRECT ",
       "DIRECT;PROXY foopy1:80;DIRECT;SOCKS5 foopy2:1080;DIRECT",
    },
    // Try putting DIRECT consecutively.
    {  "DIRECT ; proxy foopy1:80; DIRECT ; DIRECT",
       "DIRECT;PROXY foopy1:80;DIRECT;DIRECT",
    },

    // Invalid inputs (parts which aren't understood get
    // silently discarded):
    //
    // If the proxy list string parsed to empty, automatically fall-back to
    // DIRECT.
    {  "PROXY-foopy:10",
       "DIRECT",
    },
    {  "PROXY",
       "DIRECT",
    },
    {  "PROXY foopy1 ; JUNK ; JUNK ; SOCKS5 foopy2 ; ;",
       "PROXY foopy1:80;SOCKS5 foopy2:1080",
    },
  };

  for (const auto& test : tests) {
    ProxyList list;
    list.SetFromPacString(test.pac_input);
    EXPECT_EQ(test.debug_output, list.ToDebugString());
    EXPECT_FALSE(list.IsEmpty());
  }
}

TEST(ProxyListTest, RemoveProxiesWithoutScheme) {
  const struct {
    const char* pac_input;
    int filter;
    const char* filtered_debug_output;
  } tests[] = {
      {
          "PROXY foopy:10 ; SOCKS5 foopy2 ; SOCKS foopy11 ; PROXY foopy3 ; "
          "DIRECT",
          // Remove anything that isn't HTTP.
          ProxyServer::SCHEME_HTTP,
          "PROXY foopy:10;PROXY foopy3:80;DIRECT",
      },
      {
          "PROXY foopy:10 ; SOCKS5 foopy2",
          // Remove anything that isn't HTTP or SOCKS5.
          ProxyServer::SCHEME_SOCKS4,
          "",
      },
  };

  for (const auto& test : tests) {
    ProxyList list;
    list.SetFromPacString(test.pac_input);
    list.RemoveProxiesWithoutScheme(test.filter);
    EXPECT_EQ(test.filtered_debug_output, list.ToDebugString());
  }
}

TEST(ProxyListTest, RemoveProxiesWithoutSchemeWithProxyChains) {
  const ProxyChain kProxyChainFooHttps({
      ProxyServer::FromSchemeHostAndPort(ProxyServer::Scheme::SCHEME_HTTPS,
                                         "foo-a", 443),
      ProxyServer::FromSchemeHostAndPort(ProxyServer::Scheme::SCHEME_HTTPS,
                                         "foo-b", 443),
  });
  const ProxyChain kProxyChainBarMixed({
      ProxyServer::FromSchemeHostAndPort(ProxyServer::Scheme::SCHEME_SOCKS5,
                                         "bar-a", 443),
      ProxyServer::FromSchemeHostAndPort(ProxyServer::Scheme::SCHEME_HTTPS,
                                         "bar-b", 443),
  });
  const ProxyChain kProxyChainGraultSocks = ProxyChain::FromSchemeHostAndPort(
      ProxyServer::Scheme::SCHEME_SOCKS4, "grault", 443);

  ProxyList list;
  list.AddProxyChain(kProxyChainFooHttps);
  list.AddProxyChain(kProxyChainBarMixed);
  list.AddProxyChain(kProxyChainGraultSocks);
  list.AddProxyChain(ProxyChain::Direct());

  // Remove anything that isn't entirely HTTPS.
  list.RemoveProxiesWithoutScheme(ProxyServer::SCHEME_HTTPS);

  std::vector<net::ProxyChain> expected = {
      kProxyChainFooHttps,
      ProxyChain::Direct(),
  };
  EXPECT_EQ(list.AllChains(), expected);
}

TEST(ProxyListTest, DeprioritizeBadProxyChains) {
  // Retry info that marks a proxy as being bad for a *very* long time (to avoid
  // the test depending on the current time.)
  ProxyRetryInfo proxy_retry_info;
  proxy_retry_info.bad_until = base::TimeTicks::Now() + base::Days(1);

  // Call DeprioritizeBadProxyChains with an empty map -- should have no effect.
  {
    ProxyList list;
    list.SetFromPacString("PROXY foopy1:80;PROXY foopy2:80;PROXY foopy3:80");

    ProxyRetryInfoMap retry_info_map;
    list.DeprioritizeBadProxyChains(retry_info_map);
    EXPECT_EQ("PROXY foopy1:80;PROXY foopy2:80;PROXY foopy3:80",
              list.ToDebugString());
  }

  // Call DeprioritizeBadProxyChains with 2 of the three chains marked as bad.
  // These proxies should be retried last.
  {
    ProxyList list;
    list.SetFromPacString("PROXY foopy1:80;PROXY foopy2:80;PROXY foopy3:80");

    ProxyRetryInfoMap retry_info_map;
    retry_info_map[ProxyUriToProxyChain(
        "foopy1:80", ProxyServer::SCHEME_HTTP)] = proxy_retry_info;
    retry_info_map[ProxyUriToProxyChain(
        "foopy3:80", ProxyServer::SCHEME_HTTP)] = proxy_retry_info;
    retry_info_map[ProxyUriToProxyChain("socks5://localhost:1080",
                                        ProxyServer::SCHEME_HTTP)] =
        proxy_retry_info;

    list.DeprioritizeBadProxyChains(retry_info_map);

    EXPECT_EQ("PROXY foopy2:80;PROXY foopy1:80;PROXY foopy3:80",
              list.ToDebugString());
  }

  // Call DeprioritizeBadProxyChains where ALL of the chains are marked as bad.
  // This should have no effect on the order.
  {
    ProxyList list;
    list.SetFromPacString("PROXY foopy1:80;PROXY foopy2:80;PROXY foopy3:80");

    ProxyRetryInfoMap retry_info_map;
    retry_info_map[ProxyUriToProxyChain(
        "foopy1:80", ProxyServer::SCHEME_HTTP)] = proxy_retry_info;
    retry_info_map[ProxyUriToProxyChain(
        "foopy2:80", ProxyServer::SCHEME_HTTP)] = proxy_retry_info;
    retry_info_map[ProxyUriToProxyChain(
        "foopy3:80", ProxyServer::SCHEME_HTTP)] = proxy_retry_info;

    list.DeprioritizeBadProxyChains(retry_info_map);

    EXPECT_EQ("PROXY foopy1:80;PROXY foopy2:80;PROXY foopy3:80",
              list.ToDebugString());
  }

  // Call DeprioritizeBadProxyChains with 2 of the three chains marked as bad.
  // Of the 2 bad proxies, one is to be reconsidered and should be retried last.
  // The other is not to be reconsidered and should be removed from the list.
  {
    ProxyList list;
    list.SetFromPacString("PROXY foopy1:80;PROXY foopy2:80;PROXY foopy3:80");

    ProxyRetryInfoMap retry_info_map;
    // |proxy_retry_info.reconsider defaults to true.
    retry_info_map[ProxyUriToProxyChain(
        "foopy1:80", ProxyServer::SCHEME_HTTP)] = proxy_retry_info;
    proxy_retry_info.try_while_bad = false;
    retry_info_map[ProxyUriToProxyChain(
        "foopy3:80", ProxyServer::SCHEME_HTTP)] = proxy_retry_info;
    proxy_retry_info.try_while_bad = true;
    retry_info_map[ProxyUriToProxyChain("socks5://localhost:1080",
                                        ProxyServer::SCHEME_SOCKS5)] =
        proxy_retry_info;

    list.DeprioritizeBadProxyChains(retry_info_map);

    EXPECT_EQ("PROXY foopy2:80;PROXY foopy1:80", list.ToDebugString());
  }
}

TEST(ProxyListTest, UpdateRetryInfoOnFallback) {
  // Retrying should put the first proxy on the retry list.
  {
    ProxyList list;
    ProxyRetryInfoMap retry_info_map;
    NetLogWithSource net_log;
    ProxyChain proxy_chain(
        ProxyUriToProxyChain("foopy1:80", ProxyServer::SCHEME_HTTP));
    std::vector<ProxyChain> bad_proxies;
    bad_proxies.push_back(proxy_chain);
    list.SetFromPacString("PROXY foopy1:80;PROXY foopy2:80;PROXY foopy3:80");
    list.UpdateRetryInfoOnFallback(&retry_info_map, base::Seconds(60), true,
                                   bad_proxies, ERR_PROXY_CONNECTION_FAILED,
                                   net_log);
    EXPECT_TRUE(retry_info_map.end() != retry_info_map.find(proxy_chain));
    EXPECT_EQ(ERR_PROXY_CONNECTION_FAILED,
              retry_info_map[proxy_chain].net_error);
    EXPECT_TRUE(retry_info_map.end() ==
                retry_info_map.find(ProxyUriToProxyChain(
                    "foopy2:80", ProxyServer::SCHEME_HTTP)));
    EXPECT_TRUE(retry_info_map.end() ==
                retry_info_map.find(ProxyUriToProxyChain(
                    "foopy3:80", ProxyServer::SCHEME_HTTP)));
  }
  // Retrying should put the first proxy on the retry list, even if there
  // was no network error.
  {
    ProxyList list;
    ProxyRetryInfoMap retry_info_map;
    NetLogWithSource net_log;
    ProxyChain proxy_chain(
        ProxyUriToProxyChain("foopy1:80", ProxyServer::SCHEME_HTTP));
    std::vector<ProxyChain> bad_proxies;
    bad_proxies.push_back(proxy_chain);
    list.SetFromPacString("PROXY foopy1:80;PROXY foopy2:80;PROXY foopy3:80");
    list.UpdateRetryInfoOnFallback(&retry_info_map, base::Seconds(60), true,
                                   bad_proxies, OK, net_log);
    EXPECT_TRUE(retry_info_map.end() != retry_info_map.find(proxy_chain));
    EXPECT_THAT(retry_info_map[proxy_chain].net_error, IsOk());
    EXPECT_TRUE(retry_info_map.end() ==
                retry_info_map.find(ProxyUriToProxyChain(
                    "foopy2:80", ProxyServer::SCHEME_HTTP)));
    EXPECT_TRUE(retry_info_map.end() ==
                retry_info_map.find(ProxyUriToProxyChain(
                    "foopy3:80", ProxyServer::SCHEME_HTTP)));
  }
  // Including another bad proxy should put both the first and the specified
  // proxy on the retry list.
  {
    ProxyList list;
    ProxyRetryInfoMap retry_info_map;
    NetLogWithSource net_log;
    ProxyChain proxy_chain(
        ProxyUriToProxyChain("foopy3:80", ProxyServer::SCHEME_HTTP));
    std::vector<ProxyChain> bad_proxies;
    bad_proxies.push_back(proxy_chain);
    list.SetFromPacString("PROXY foopy1:80;PROXY foopy2:80;PROXY foopy3:80");
    list.UpdateRetryInfoOnFallback(&retry_info_map, base::Seconds(60), true,
                                   bad_proxies, ERR_NAME_RESOLUTION_FAILED,
                                   net_log);
    EXPECT_TRUE(retry_info_map.end() !=
                retry_info_map.find(ProxyUriToProxyChain(
                    "foopy1:80", ProxyServer::SCHEME_HTTP)));
    EXPECT_EQ(ERR_NAME_RESOLUTION_FAILED,
              retry_info_map[proxy_chain].net_error);
    EXPECT_TRUE(retry_info_map.end() ==
                retry_info_map.find(ProxyUriToProxyChain(
                    "foopy2:80", ProxyServer::SCHEME_HTTP)));
    EXPECT_TRUE(retry_info_map.end() != retry_info_map.find(proxy_chain));
  }
  // If the first proxy is DIRECT, nothing is added to the retry list, even
  // if another bad proxy is specified.
  {
    ProxyList list;
    ProxyRetryInfoMap retry_info_map;
    NetLogWithSource net_log;
    ProxyChain proxy_chain(
        ProxyUriToProxyChain("foopy2:80", ProxyServer::SCHEME_HTTP));
    std::vector<ProxyChain> bad_proxies;
    bad_proxies.push_back(proxy_chain);
    list.SetFromPacString("DIRECT;PROXY foopy2:80;PROXY foopy3:80");
    list.UpdateRetryInfoOnFallback(&retry_info_map, base::Seconds(60), true,
                                   bad_proxies, OK, net_log);
    EXPECT_TRUE(retry_info_map.end() == retry_info_map.find(proxy_chain));
    EXPECT_TRUE(retry_info_map.end() ==
                retry_info_map.find(ProxyUriToProxyChain(
                    "foopy3:80", ProxyServer::SCHEME_HTTP)));
  }
  // If the bad proxy is already on the retry list, and the old retry info would
  // cause the proxy to be retried later than the newly specified retry info,
  // then the old retry info should be kept.
  {
    ProxyList list;
    ProxyRetryInfoMap retry_info_map;
    NetLogWithSource net_log;
    list.SetFromPacString("PROXY foopy1:80;PROXY foopy2:80;PROXY foopy3:80");

    // First, mark the proxy as bad for 60 seconds.
    list.UpdateRetryInfoOnFallback(&retry_info_map, base::Seconds(60), true,
                                   std::vector<ProxyChain>(),
                                   ERR_PROXY_CONNECTION_FAILED, net_log);
    // Next, mark the same proxy as bad for 1 second. This call should have no
    // effect, since this would cause the bad proxy to be retried sooner than
    // the existing retry info.
    list.UpdateRetryInfoOnFallback(&retry_info_map, base::Seconds(1), false,
                                   std::vector<ProxyChain>(), OK, net_log);
    ProxyChain proxy_chain(
        ProxyUriToProxyChain("foopy1:80", ProxyServer::SCHEME_HTTP));
    EXPECT_TRUE(retry_info_map.end() != retry_info_map.find(proxy_chain));
    EXPECT_EQ(ERR_PROXY_CONNECTION_FAILED,
              retry_info_map[proxy_chain].net_error);
    EXPECT_TRUE(retry_info_map[proxy_chain].try_while_bad);
    EXPECT_EQ(base::Seconds(60), retry_info_map[proxy_chain].current_delay);
    EXPECT_GT(retry_info_map[proxy_chain].bad_until,
              base::TimeTicks::Now() + base::Seconds(30));
    EXPECT_TRUE(retry_info_map.end() ==
                retry_info_map.find(ProxyUriToProxyChain(
                    "foopy2:80", ProxyServer::SCHEME_HTTP)));
    EXPECT_TRUE(retry_info_map.end() ==
                retry_info_map.find(ProxyUriToProxyChain(
                    "foopy3:80", ProxyServer::SCHEME_HTTP)));
  }
  // If the bad proxy is already on the retry list, and the newly specified
  // retry info would cause the proxy to be retried later than the old retry
  // info, then the old retry info should be replaced with the new retry info.
  {
    ProxyList list;
    ProxyRetryInfoMap retry_info_map;
    NetLogWithSource net_log;
    list.SetFromPacString("PROXY foopy1:80;PROXY foopy2:80;PROXY foopy3:80");

    // First, mark the proxy as bad for 1 second.
    list.UpdateRetryInfoOnFallback(&retry_info_map, base::Seconds(1), false,
                                   std::vector<ProxyChain>(), OK, net_log);
    // Next, mark the same proxy as bad for 60 seconds. This call should replace
    // the existing retry info with the new 60 second retry info.
    list.UpdateRetryInfoOnFallback(&retry_info_map, base::Seconds(60), true,
                                   std::vector<ProxyChain>(),
                                   ERR_PROXY_CONNECTION_FAILED, net_log);
    ProxyChain proxy_chain(
        ProxyUriToProxyChain("foopy1:80", ProxyServer::SCHEME_HTTP));
    EXPECT_TRUE(retry_info_map.end() != retry_info_map.find(proxy_chain));
    EXPECT_EQ(ERR_PROXY_CONNECTION_FAILED,
              retry_info_map[proxy_chain].net_error);
    EXPECT_TRUE(retry_info_map[proxy_chain].try_while_bad);
    EXPECT_EQ(base::Seconds(60), retry_info_map[proxy_chain].current_delay);
    EXPECT_GT(retry_info_map[proxy_chain].bad_until,
              base::TimeTicks::Now() + base::Seconds(30));
    EXPECT_TRUE(retry_info_map.end() ==
                retry_info_map.find(ProxyUriToProxyChain(
                    "foopy2:80", ProxyServer::SCHEME_HTTP)));
    EXPECT_TRUE(retry_info_map.end() ==
                retry_info_map.find(ProxyUriToProxyChain(
                    "foopy3:80", ProxyServer::SCHEME_HTTP)));
  }
}

TEST(ProxyListTest, ToPacString) {
  ProxyList list;
  list.AddProxyChain(ProxyChain::FromSchemeHostAndPort(
      ProxyServer::Scheme::SCHEME_HTTPS, "foo", 443));
  EXPECT_EQ(list.ToPacString(), "HTTPS foo:443");

  // ToPacString should fail for proxy chains.
  list.AddProxyChain(ProxyChain({
      ProxyServer::FromSchemeHostAndPort(ProxyServer::Scheme::SCHEME_HTTPS,
                                         "foo-a", 443),
      ProxyServer::FromSchemeHostAndPort(ProxyServer::Scheme::SCHEME_HTTPS,
                                         "foo-b", 443),
  }));
  EXPECT_DEATH_IF_SUPPORTED(list.ToPacString(), "");
}

TEST(ProxyListTest, ToDebugString) {
  ProxyList list;
  list.AddProxyChain(ProxyChain::FromSchemeHostAndPort(
      ProxyServer::Scheme::SCHEME_HTTPS, "foo", 443));
  list.AddProxyChain(ProxyChain({
      ProxyServer::FromSchemeHostAndPort(ProxyServer::Scheme::SCHEME_HTTPS,
                                         "foo-a", 443),
      ProxyServer::FromSchemeHostAndPort(ProxyServer::Scheme::SCHEME_HTTPS,
                                         "foo-b", 443),
  }));

  EXPECT_EQ(list.ToDebugString(),
            "HTTPS foo:443;[https://foo-a:443, https://foo-b:443]");
}

TEST(ProxyListTest, ToValue) {
  ProxyList list;
  list.AddProxyChain(ProxyChain::FromSchemeHostAndPort(
      ProxyServer::Scheme::SCHEME_HTTPS, "foo", 443));
  list.AddProxyChain(ProxyChain({
      ProxyServer::FromSchemeHostAndPort(ProxyServer::Scheme::SCHEME_HTTPS,
                                         "foo-a", 443),
      ProxyServer::FromSchemeHostAndPort(ProxyServer::Scheme::SCHEME_HTTPS,
                                         "foo-b", 443),
  }));

  base::Value expected(base::Value::Type::LIST);
  base::Value::List& exp_list = expected.GetList();
  exp_list.Append("[https://foo:443]");
  exp_list.Append("[https://foo-a:443, https://foo-b:443]");

  EXPECT_EQ(list.ToValue(), expected);
}

}  // anonymous namespace

}  // namespace net
