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

// Unit tests for helper functions for the Chrome Extensions Proxy Settings API.

#include "extensions/browser/api/proxy/proxy_api_helpers.h"

#include <memory>
#include <utility>

#include "base/values.h"
#include "components/proxy_config/proxy_config_dictionary.h"
#include "components/proxy_config/proxy_prefs.h"
#include "extensions/browser/api/proxy/proxy_api_constants.h"
#include "extensions/buildflags/buildflags.h"
#include "net/base/proxy_server.h"
#include "net/base/proxy_string_util.h"
#include "testing/gtest/include/gtest/gtest.h"

static_assert(BUILDFLAG(ENABLE_EXTENSIONS_CORE));

namespace extensions {

namespace keys = proxy_api_constants;

namespace {

const char kSamplePacScript[] = "test";
const char kSamplePacScriptAsDataUrl[] =
    "data:application/x-ns-proxy-autoconfig;base64,dGVzdA==";
const char kSamplePacScriptAsDataUrl2[] = "data:;base64,dGVzdA==";
const char kSamplePacScriptUrl[] = "http://wpad/wpad.dat";

// Helper function to create a ProxyServer dictionary as defined in the
// extension API.
base::DictValue CreateTestProxyServerDict(const std::string& host) {
  base::DictValue dict;
  dict.Set(keys::kProxyConfigRuleHost, host);
  return dict;
}

// Helper function to create a ProxyServer dictionary as defined in the
// extension API.
base::DictValue CreateTestProxyServerDict(const std::string& schema,
                                          const std::string& host,
                                          int port) {
  base::DictValue dict;
  dict.Set(keys::kProxyConfigRuleScheme, schema);
  dict.Set(keys::kProxyConfigRuleHost, host);
  dict.Set(keys::kProxyConfigRulePort, port);
  return dict;
}

}  // namespace

namespace proxy_api_helpers {

TEST(ExtensionProxyApiHelpers, CreateDataURLFromPACScript) {
  EXPECT_EQ(kSamplePacScriptAsDataUrl,
            CreateDataURLFromPACScript(kSamplePacScript));
}

TEST(ExtensionProxyApiHelpers, CreatePACScriptFromDataURL) {
  std::string out;
  // Verify deserialization of a PAC data:// URL that we created ourselves.
  ASSERT_TRUE(CreatePACScriptFromDataURL(kSamplePacScriptAsDataUrl, &out));
  EXPECT_EQ(kSamplePacScript, out);

  // Check that we don't require a mime-type.
  out.clear();
  ASSERT_TRUE(CreatePACScriptFromDataURL(kSamplePacScriptAsDataUrl2, &out));
  EXPECT_EQ(kSamplePacScript, out);

  out.clear();
  EXPECT_FALSE(CreatePACScriptFromDataURL("http://www.google.com", &out));
}

TEST(ExtensionProxyApiHelpers, GetProxyModeFromExtensionPref) {
  base::DictValue proxy_config;
  ProxyPrefs::ProxyMode mode;
  std::string error;
  bool bad_message = false;

  // Test positive case.
  proxy_config.Set(keys::kProxyConfigMode,
                   ProxyPrefs::ProxyModeToString(ProxyPrefs::MODE_DIRECT));
  ASSERT_TRUE(
      GetProxyModeFromExtensionPref(proxy_config, &mode, &error, &bad_message));
  EXPECT_EQ(ProxyPrefs::MODE_DIRECT, mode);
  EXPECT_EQ(std::string(), error);
  EXPECT_FALSE(bad_message);

  // Test negative case.
  proxy_config.Set(keys::kProxyConfigMode, "foobar");
  EXPECT_FALSE(
      GetProxyModeFromExtensionPref(proxy_config, &mode, &error, &bad_message));
  EXPECT_TRUE(bad_message);

  // Do not test |error|, as an invalid enumeration value is considered an
  // internal error. It should be filtered by the extensions API.
}

TEST(ExtensionProxyApiHelpers, GetPacUrlFromExtensionPref) {
  std::string out;
  std::string error;
  bool bad_message = false;

  base::DictValue proxy_config;
  proxy_config.Set(keys::kProxyConfigMode,
                   ProxyPrefs::ProxyModeToString(ProxyPrefs::MODE_PAC_SCRIPT));

  // Currently we are still missing a PAC script entry.
  // This is silently ignored.
  ASSERT_TRUE(
      GetPacUrlFromExtensionPref(proxy_config, &out, &error, &bad_message));
  EXPECT_EQ(std::string(), out);
  EXPECT_EQ(std::string(), error);
  EXPECT_FALSE(bad_message);

  // Set up a pac script.
  base::DictValue pacScriptDict;
  pacScriptDict.Set(keys::kProxyConfigPacScriptUrl, kSamplePacScriptUrl);
  proxy_config.Set(keys::kProxyConfigPacScript, std::move(pacScriptDict));

  ASSERT_TRUE(
      GetPacUrlFromExtensionPref(proxy_config, &out, &error, &bad_message));
  EXPECT_EQ(kSamplePacScriptUrl, out);
  EXPECT_EQ(std::string(), error);
  EXPECT_FALSE(bad_message);
}

TEST(ExtensionProxyApiHelpers, GetPacDataFromExtensionPref) {
  std::string out;
  std::string error;
  bool bad_message = false;

  base::DictValue proxy_config;
  proxy_config.Set(keys::kProxyConfigMode,
                   ProxyPrefs::ProxyModeToString(ProxyPrefs::MODE_PAC_SCRIPT));

  // Currently we are still missing a PAC data entry. This is silently ignored.
  ASSERT_TRUE(
      GetPacDataFromExtensionPref(proxy_config, &out, &error, &bad_message));
  EXPECT_EQ(std::string(), out);
  EXPECT_EQ(std::string(), error);
  EXPECT_FALSE(bad_message);

  // Set up a PAC script.
  base::DictValue pacScriptDict;
  pacScriptDict.Set(keys::kProxyConfigPacScriptData, kSamplePacScript);
  proxy_config.Set(keys::kProxyConfigPacScript, std::move(pacScriptDict));

  ASSERT_TRUE(
      GetPacDataFromExtensionPref(proxy_config, &out, &error, &bad_message));
  EXPECT_EQ(kSamplePacScript, out);
  EXPECT_EQ(std::string(), error);
  EXPECT_FALSE(bad_message);
}

TEST(ExtensionProxyApiHelpers, GetProxyRulesStringFromExtensionPref) {
  std::string out;
  std::string error;
  bool bad_message = false;

  base::DictValue proxy_config;
  proxy_config.Set(keys::kProxyConfigMode, ProxyPrefs::ProxyModeToString(
                                               ProxyPrefs::MODE_FIXED_SERVERS));

  // Currently we are still missing a proxy config entry.
  // This is silently ignored.
  ASSERT_TRUE(GetProxyRulesStringFromExtensionPref(proxy_config, &out, &error,
                                                   &bad_message));
  EXPECT_EQ(std::string(), out);
  EXPECT_EQ(std::string(), error);

  base::DictValue proxy_rules;
  proxy_rules.Set(proxy_api_helpers::kFieldNames[1],
                  CreateTestProxyServerDict("proxy1"));
  proxy_rules.Set(proxy_api_helpers::kFieldNames[2],
                  CreateTestProxyServerDict("proxy2"));
  proxy_config.Set(keys::kProxyConfigRules, std::move(proxy_rules));

  ASSERT_TRUE(GetProxyRulesStringFromExtensionPref(proxy_config, &out, &error,
                                                   &bad_message));
  EXPECT_EQ("http=proxy1:80;https=proxy2:80", out);
  EXPECT_EQ(std::string(), error);
  EXPECT_FALSE(bad_message);
}

TEST(ExtensionProxyApiHelpers, GetProxyRulesStringFromExtensionPrefInvalid) {
  static constexpr char kSingleProxy[] = "singleProxy";
  std::string out;
  std::string error;
  bool bad_message = false;

  base::DictValue proxy_config;
  proxy_config.Set(keys::kProxyConfigMode, ProxyPrefs::ProxyModeToString(
                                               ProxyPrefs::MODE_FIXED_SERVERS));
  base::DictValue invalidManualProxy;
  invalidManualProxy.Set(keys::kProxyConfigRuleHost, std::string());
  base::DictValue singleProxy;
  singleProxy.Set(kSingleProxy, std::move(invalidManualProxy));
  proxy_config.Set(keys::kProxyConfigRules, std::move(singleProxy));

  ASSERT_FALSE(GetProxyRulesStringFromExtensionPref(proxy_config, &out, &error,
                                                    &bad_message));
  EXPECT_EQ(std::string(), out);
  EXPECT_EQ("Invalid 'rules.???.host' entry. Hostname cannot be empty.", error);
  EXPECT_FALSE(bad_message);
}

TEST(ExtensionProxyApiHelpers, GetBypassListFromExtensionPref) {
  std::string out;
  std::string error;
  bool bad_message = false;

  base::DictValue proxy_config;
  proxy_config.Set(keys::kProxyConfigMode, ProxyPrefs::ProxyModeToString(
                                               ProxyPrefs::MODE_FIXED_SERVERS));

  // Currently we are still missing a proxy config entry.
  // This is silently ignored.
  ASSERT_TRUE(
      GetBypassListFromExtensionPref(proxy_config, &out, &error, &bad_message));
  EXPECT_EQ(std::string(), out);
  EXPECT_EQ(std::string(), error);
  EXPECT_FALSE(bad_message);

  base::ListValue bypass_list;
  bypass_list.Append("host1");
  bypass_list.Append("host2");
  base::DictValue proxy_rules;
  proxy_rules.Set(keys::kProxyConfigBypassList, std::move(bypass_list));
  proxy_config.Set(keys::kProxyConfigRules, std::move(proxy_rules));

  ASSERT_TRUE(
      GetBypassListFromExtensionPref(proxy_config, &out, &error, &bad_message));
  EXPECT_EQ("host1,host2", out);
  EXPECT_EQ(std::string(), error);
  EXPECT_FALSE(bad_message);
}

TEST(ExtensionProxyApiHelpers, CreateProxyConfigDict) {
  std::string error;
  base::DictValue exp_direct = ProxyConfigDictionary::CreateDirect();
  std::optional<base::DictValue> out_direct(CreateProxyConfigDict(
      ProxyPrefs::MODE_DIRECT, false, std::string(), std::string(),
      std::string(), std::string(), &error));
  EXPECT_EQ(exp_direct, *out_direct);

  base::DictValue exp_auto = ProxyConfigDictionary::CreateAutoDetect();
  std::optional<base::DictValue> out_auto(CreateProxyConfigDict(
      ProxyPrefs::MODE_AUTO_DETECT, false, std::string(), std::string(),
      std::string(), std::string(), &error));
  EXPECT_EQ(exp_auto, *out_auto);

  base::DictValue exp_pac_url =
      ProxyConfigDictionary::CreatePacScript(kSamplePacScriptUrl, false);
  std::optional<base::DictValue> out_pac_url(CreateProxyConfigDict(
      ProxyPrefs::MODE_PAC_SCRIPT, false, kSamplePacScriptUrl, std::string(),
      std::string(), std::string(), &error));
  EXPECT_EQ(exp_pac_url, *out_pac_url);

  base::DictValue exp_pac_data =
      ProxyConfigDictionary::CreatePacScript(kSamplePacScriptAsDataUrl, false);
  std::optional<base::DictValue> out_pac_data(CreateProxyConfigDict(
      ProxyPrefs::MODE_PAC_SCRIPT, false, std::string(), kSamplePacScript,
      std::string(), std::string(), &error));
  EXPECT_EQ(exp_pac_data, *out_pac_data);

  base::DictValue exp_fixed =
      ProxyConfigDictionary::CreateFixedServers("foo:80", "localhost");
  std::optional<base::DictValue> out_fixed(CreateProxyConfigDict(
      ProxyPrefs::MODE_FIXED_SERVERS, false, std::string(), std::string(),
      "foo:80", "localhost", &error));
  EXPECT_EQ(exp_fixed, *out_fixed);

  base::DictValue exp_system = ProxyConfigDictionary::CreateSystem();
  std::optional<base::DictValue> out_system(CreateProxyConfigDict(
      ProxyPrefs::MODE_SYSTEM, false, std::string(), std::string(),
      std::string(), std::string(), &error));
  EXPECT_EQ(exp_system, *out_system);

  // Neither of them should have set an error.
  EXPECT_EQ(std::string(), error);
}

TEST(ExtensionProxyApiHelpers, GetProxyServer) {
  base::DictValue proxy_server_dict;
  net::ProxyServer created;
  std::string error;
  bool bad_message = false;

  // Test simplest case, no schema nor port specified --> defaults are used.
  proxy_server_dict.Set(keys::kProxyConfigRuleHost, "proxy_server");
  ASSERT_TRUE(GetProxyServer(proxy_server_dict, net::ProxyServer::SCHEME_HTTP,
                             &created, &error, &bad_message));
  EXPECT_EQ("PROXY proxy_server:80",
            net::ProxyServerToPacResultElement(created));
  EXPECT_FALSE(bad_message);

  // Test complete case.
  proxy_server_dict.Set(keys::kProxyConfigRuleScheme, "socks4");
  proxy_server_dict.Set(keys::kProxyConfigRulePort, 1234);
  ASSERT_TRUE(GetProxyServer(proxy_server_dict, net::ProxyServer::SCHEME_HTTP,
                             &created, &error, &bad_message));
  EXPECT_EQ("SOCKS proxy_server:1234",
            net::ProxyServerToPacResultElement(created));
  EXPECT_FALSE(bad_message);
}

TEST(ExtensionProxyApiHelpers, JoinUrlList) {
  bool bad_message = false;
  base::ListValue list;
  list.Append("s1");
  list.Append("s2");
  list.Append("s3");

  std::string out;
  std::string error;
  ASSERT_TRUE(JoinUrlList(list, ";", &out, &error, &bad_message));
  EXPECT_EQ("s1;s2;s3", out);
  EXPECT_FALSE(bad_message);
}

// This tests CreateProxyServerDict as well.
TEST(ExtensionProxyApiHelpers, CreateProxyRulesDict) {
  ProxyConfigDictionary config(ProxyConfigDictionary::CreateFixedServers(
      "http=proxy1:80;https=proxy2:80;ftp=proxy3:80;socks=proxy4:80",
      "localhost"));
  std::optional<base::DictValue> extension_pref = CreateProxyRulesDict(config);
  ASSERT_TRUE(extension_pref);

  base::DictValue expected;
  expected.Set("proxyForHttp", CreateTestProxyServerDict("http", "proxy1", 80));
  expected.Set("proxyForHttps",
               CreateTestProxyServerDict("http", "proxy2", 80));
  expected.Set("proxyForFtp", CreateTestProxyServerDict("http", "proxy3", 80));
  expected.Set("fallbackProxy",
               CreateTestProxyServerDict("socks4", "proxy4", 80));
  base::ListValue bypass_list;
  bypass_list.Append("localhost");
  expected.Set(keys::kProxyConfigBypassList, std::move(bypass_list));

  EXPECT_EQ(expected, *extension_pref);
}

// Test multiple proxies per scheme -- expect that only the first is returned.
TEST(ExtensionProxyApiHelpers, CreateProxyRulesDictMultipleProxies) {
  ProxyConfigDictionary config(ProxyConfigDictionary::CreateFixedServers(
      "http=proxy1:80,default://;https=proxy2:80,proxy1:80;ftp=proxy3:80,"
      "https://proxy5:443;socks=proxy4:80,proxy1:80",
      "localhost"));
  std::optional<base::DictValue> extension_pref = CreateProxyRulesDict(config);
  ASSERT_TRUE(extension_pref);

  base::DictValue expected;
  expected.Set("proxyForHttp", CreateTestProxyServerDict("http", "proxy1", 80));
  expected.Set("proxyForHttps",
               CreateTestProxyServerDict("http", "proxy2", 80));
  expected.Set("proxyForFtp", CreateTestProxyServerDict("http", "proxy3", 80));
  expected.Set("fallbackProxy",
               CreateTestProxyServerDict("socks4", "proxy4", 80));
  base::ListValue bypass_list;
  bypass_list.Append("localhost");
  expected.Set(keys::kProxyConfigBypassList, std::move(bypass_list));

  EXPECT_EQ(expected, *extension_pref);
}

// Test if a PAC script URL is specified.
TEST(ExtensionProxyApiHelpers, CreatePacScriptDictWithUrl) {
  ProxyConfigDictionary config(
      ProxyConfigDictionary::CreatePacScript(kSamplePacScriptUrl, false));
  std::optional<base::DictValue> extension_pref = CreatePacScriptDict(config);
  ASSERT_TRUE(extension_pref);

  base::DictValue expected;
  expected.Set(keys::kProxyConfigPacScriptUrl, kSamplePacScriptUrl);
  expected.Set(keys::kProxyConfigPacScriptMandatory, false);

  EXPECT_EQ(expected, *extension_pref);
}

// Test if a PAC script is encoded in a data URL.
TEST(ExtensionProxyApiHelpers, CreatePacScriptDictWidthData) {
  ProxyConfigDictionary config(
      ProxyConfigDictionary::CreatePacScript(kSamplePacScriptAsDataUrl, false));
  std::optional<base::DictValue> extension_pref = CreatePacScriptDict(config);
  ASSERT_TRUE(extension_pref);

  base::DictValue expected;
  expected.Set(keys::kProxyConfigPacScriptData, kSamplePacScript);
  expected.Set(keys::kProxyConfigPacScriptMandatory, false);

  EXPECT_EQ(expected, *extension_pref);
}

TEST(ExtensionProxyApiHelpers, TokenizeToStringList) {
  base::ListValue expected;
  expected.Append("s1");
  expected.Append("s2");
  expected.Append("s3");

  base::ListValue out = TokenizeToStringList("s1;s2;s3", ";");
  EXPECT_EQ(expected, out);
}

}  // namespace proxy_api_helpers
}  // namespace extensions
