chromium/chrome/browser/ash/proxy_config_service_impl_unittest.cc

// 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.

#ifdef UNSAFE_BUFFERS_BUILD
// TODO(crbug.com/40285824): Remove this and convert code to safer constructs.
#pragma allow_unsafe_buffers
#endif

#include <stddef.h>

#include <memory>
#include <utility>
#include <vector>

#include "base/format_macros.h"
#include "base/json/json_writer.h"
#include "base/notreached.h"
#include "base/run_loop.h"
#include "base/strings/stringprintf.h"
#include "base/values.h"
#include "chrome/browser/ash/settings/scoped_cros_settings_test_helper.h"
#include "chromeos/ash/components/network/network_handler.h"
#include "chromeos/ash/components/network/network_handler_test_helper.h"
#include "chromeos/ash/components/network/network_profile_handler.h"
#include "chromeos/ash/components/network/network_state.h"
#include "chromeos/ash/components/network/network_state_handler.h"
#include "chromeos/ash/components/network/proxy/proxy_config_handler.h"
#include "chromeos/ash/components/network/proxy/proxy_config_service_impl.h"
#include "chromeos/components/onc/onc_utils.h"
#include "components/onc/onc_pref_names.h"
#include "components/prefs/testing_pref_service.h"
#include "components/proxy_config/proxy_config_pref_names.h"
#include "components/sync_preferences/testing_pref_service_syncable.h"
#include "content/public/browser/browser_task_traits.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/test/browser_task_environment.h"
#include "net/proxy_resolution/proxy_config.h"
#include "net/proxy_resolution/proxy_config_service_common_unittest.h"
#include "net/traffic_annotation/network_traffic_annotation_test_helper.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/cros_system_api/dbus/service_constants.h"

// TODO(stevenjb): Refactor and move this to
// src/chromeos/ash/components/network/proxy or rename. This is really more of
// an integration test than a unit test at this point and currently relies on
// some chrome specific components.

namespace ash {

namespace {

enum class Mode {
  kDirect,
  kAutoDetect,
  kPac,
  kSingleProxy,
  kPerSchemeProxy,
};

struct Input {
  Mode mode;
  std::string pac_url;
  std::string server;
  std::string bypass_rules;
};

// Builds an identifier for each test in an array.
#define TEST_DESC(desc) base::StringPrintf("at line %d <%s>", __LINE__, desc)

// Inspired from net/proxy_resolution/proxy_config_service_linux_unittest.cc.
const struct TestParams {
  // Short description to identify the test
  std::string description;

  Input input;

  // Expected outputs from fields of net::ProxyConfig (via IO).
  bool auto_detect;
  const char* pac_url;
  net::ProxyRulesExpectation proxy_rules;
} tests[] = {
    {
        // 0
        TEST_DESC("No proxying"),

        {
            // Input.
            Mode::kDirect,  // mode
        },

        // Expected result.
        false,                                // auto_detect
        "",                                   // pac_url
        net::ProxyRulesExpectation::Empty(),  // proxy_rules
    },

    {
        // 1
        TEST_DESC("Auto detect"),

        {
            // Input.
            Mode::kAutoDetect,  // mode
        },

        // Expected result.
        true,                                 // auto_detect
        "",                                   // pac_url
        net::ProxyRulesExpectation::Empty(),  // proxy_rules
    },

    {
        // 2
        TEST_DESC("Valid PAC URL"),

        {
            // Input.
            Mode::kPac,              // mode
            "http://wpad/wpad.dat",  // pac_url
        },

        // Expected result.
        false,                                // auto_detect
        "http://wpad/wpad.dat",               // pac_url
        net::ProxyRulesExpectation::Empty(),  // proxy_rules
    },

    {
        // 3
        TEST_DESC("Invalid PAC URL"),

        {
            // Input.
            Mode::kPac,  // mode
            "wpad.dat",  // pac_url
        },

        // Expected result.
        false,                                // auto_detect
        "",                                   // pac_url
        net::ProxyRulesExpectation::Empty(),  // proxy_rules
    },

    {
        // 4
        TEST_DESC("Single-host in proxy list"),

        {
            // Input.
            Mode::kSingleProxy,  // mode
            "",                  // pac_url
            "www.google.com",    // server
        },

        // Expected result.
        false,                               // auto_detect
        "",                                  // pac_url
        net::ProxyRulesExpectation::Single(  // proxy_rules
            "www.google.com:80",             // single proxy
            "<local>"),                      // bypass rules
    },

    {
        // 5
        TEST_DESC("Single-host, different port"),

        {
            // Input.
            Mode::kSingleProxy,   // mode
            "",                   // pac_url
            "www.google.com:99",  // server
        },

        // Expected result.
        false,                               // auto_detect
        "",                                  // pac_url
        net::ProxyRulesExpectation::Single(  // proxy_rules
            "www.google.com:99",             // single
            "<local>"),                      // bypass rules
    },

    {
        // 6
        TEST_DESC("Tolerate a scheme"),

        {
            // Input.
            Mode::kSingleProxy,          // mode
            "",                          // pac_url
            "http://www.google.com:99",  // server
        },

        // Expected result.
        false,                               // auto_detect
        "",                                  // pac_url
        net::ProxyRulesExpectation::Single(  // proxy_rules
            "www.google.com:99",             // single proxy
            "<local>"),                      // bypass rules
    },

    {
        // 7
        TEST_DESC("Per-scheme proxy rules"),

        {
            // Input.
            Mode::kPerSchemeProxy,  // mode
            "",                     // pac_url
            "http=www.google.com:80;https=https://www.foo.com:110;"
            "ftp=ftp.foo.com:121;socks=socks5://socks.com:888",  // server
        },

        // Expected result.
        false,                                           // auto_detect
        "",                                              // pac_url
        net::ProxyRulesExpectation::PerSchemeWithSocks(  // proxy_rules
            "www.google.com:80",                         // http
            "https://www.foo.com:110",                   // https
            "ftp.foo.com:121",                           // ftp
            "socks5://socks.com:888",                    // fallback proxy
            "<local>"),                                  // bypass rules
    },

    {
        // 8
        TEST_DESC("Bypass rules"),

        {
            // Input.
            Mode::kSingleProxy,  // mode
            "",                  // pac_url
            "www.google.com",    // server
            "*.google.com, *foo.com:99, 1.2.3.4:22, 127.0.0.1/8",
            // bypass_rules
        },

        // Expected result.
        false,                               // auto_detect
        "",                                  // pac_url
        net::ProxyRulesExpectation::Single(  // proxy_rules
            "www.google.com:80",             // single proxy
                                             // bypass_rules
            "<local>,*.google.com,*foo.com:99,1.2.3.4:22,127.0.0.1/8"),
    },
};  // tests

const char kEthernetPolicy[] =
    "    { \"GUID\": \"{485d6076-dd44-6b6d-69787465725f5040}\","
    "      \"Type\": \"Ethernet\","
    "      \"Name\": \"MyEthernet\","
    "      \"Ethernet\": {"
    "        \"Authentication\": \"None\" },"
    "      \"ProxySettings\": {"
    "        \"PAC\": \"http://domain.com/x\","
    "        \"Type\": \"PAC\" }"
    "    }";

const char kUserProfilePath[] = "user_profile";

}  // namespace

class ProxyConfigServiceImplTest : public testing::Test {
 protected:
  ProxyConfigServiceImplTest() = default;

  void SetUp() override {
    PrefProxyConfigTrackerImpl::RegisterPrefs(pref_service_.registry());
    ::onc::RegisterPrefs(pref_service_.registry());
    PrefProxyConfigTrackerImpl::RegisterProfilePrefs(profile_prefs_.registry());
    ::onc::RegisterProfilePrefs(profile_prefs_.registry());
  }

  void SetUpProxyConfigService(PrefService* profile_prefs) {
    config_service_impl_ = std::make_unique<ProxyConfigServiceImpl>(
        profile_prefs, &pref_service_, content::GetIOThreadTaskRunner({}));
    proxy_config_service_ =
        config_service_impl_->CreateTrackingProxyConfigService(
            std::unique_ptr<net::ProxyConfigService>());

    // CreateTrackingProxyConfigService triggers update of initial prefs proxy
    // config by tracker to chrome proxy config service, so flush all pending
    // tasks so that tests start fresh.
    base::RunLoop().RunUntilIdle();
  }

  void SetUpPrivateWiFi() {
    ShillProfileClient::TestInterface* profile_test =
        network_handler_test_helper_.profile_test();
    ShillServiceClient::TestInterface* service_test =
        network_handler_test_helper_.service_test();

    // Process any pending notifications before clearing services.
    base::RunLoop().RunUntilIdle();
    service_test->ClearServices();

    // Sends a notification about the added profile.
    profile_test->AddProfile(kUserProfilePath, "user_hash");

    service_test->AddService("/service/stub_wifi2", "stub_wifi2" /* guid */,
                             "wifi2_PSK", shill::kTypeWifi, shill::kStateOnline,
                             true /* visible */);
    profile_test->AddService(kUserProfilePath, "/service/stub_wifi2");

    base::RunLoop().RunUntilIdle();
  }

  void SetUpSharedEthernet() {
    ShillProfileClient::TestInterface* profile_test =
        network_handler_test_helper_.profile_test();
    ShillServiceClient::TestInterface* service_test =
        network_handler_test_helper_.service_test();

    // Process any pending notifications before clearing services.
    base::RunLoop().RunUntilIdle();
    service_test->ClearServices();

    // Sends a notification about the added profile.
    profile_test->AddProfile(kUserProfilePath, "user_hash");

    service_test->AddService("/service/ethernet", "stub_ethernet" /* guid */,
                             "eth0", shill::kTypeEthernet, shill::kStateOnline,
                             true /* visible */);
    profile_test->AddService(NetworkProfileHandler::GetSharedProfilePath(),
                             "/service/ethernet");

    base::RunLoop().RunUntilIdle();
  }

  void TearDown() override {
    config_service_impl_->DetachFromPrefService();
    base::RunLoop().RunUntilIdle();
    config_service_impl_.reset();
    proxy_config_service_.reset();
  }

  base::Value::Dict InitConfigWithTestInput(const Input& input) {
    switch (input.mode) {
      case Mode::kDirect:
        return ProxyConfigDictionary::CreateDirect();
      case Mode::kAutoDetect:
        return ProxyConfigDictionary::CreateAutoDetect();
      case Mode::kPac:
        return ProxyConfigDictionary::CreatePacScript(input.pac_url, false);
      case Mode::kSingleProxy:
      case Mode::kPerSchemeProxy:
        return ProxyConfigDictionary::CreateFixedServers(input.server,
                                                         input.bypass_rules);
    }
    NOTREACHED_IN_MIGRATION();
    return base::Value::Dict();
  }

  void SetUserConfigInShill(const base::Value::Dict* pref_proxy_config_dict) {
    std::string proxy_config;
    if (pref_proxy_config_dict)
      base::JSONWriter::Write(*pref_proxy_config_dict, &proxy_config);

    NetworkStateHandler* network_state_handler =
        NetworkHandler::Get()->network_state_handler();
    const NetworkState* network = network_state_handler->DefaultNetwork();
    ASSERT_TRUE(network);
    network_handler_test_helper_.service_test()->SetServiceProperty(
        network->path(), shill::kProxyConfigProperty,
        base::Value(proxy_config));
  }

  // Synchronously gets the latest proxy config.
  void SyncGetLatestProxyConfig(net::ProxyConfigWithAnnotation* config) {
    *config = net::ProxyConfigWithAnnotation();
    // Let message loop process all messages. This will run
    // ChromeProxyConfigService::UpdateProxyConfig, which is posted on IO from
    // PrefProxyConfigTrackerImpl::OnProxyConfigChanged.
    base::RunLoop().RunUntilIdle();
    net::ProxyConfigService::ConfigAvailability availability =
        proxy_config_service_->GetLatestProxyConfig(config);

    EXPECT_EQ(net::ProxyConfigService::CONFIG_VALID, availability);
  }

  content::BrowserTaskEnvironment task_environment_;
  NetworkHandlerTestHelper network_handler_test_helper_;
  std::unique_ptr<net::ProxyConfigService> proxy_config_service_;
  std::unique_ptr<ProxyConfigServiceImpl> config_service_impl_;
  TestingPrefServiceSimple pref_service_;
  sync_preferences::TestingPrefServiceSyncable profile_prefs_;

 private:
  ScopedCrosSettingsTestHelper cros_settings_test_helper_;
};

TEST_F(ProxyConfigServiceImplTest, NetworkProxy) {
  SetUpPrivateWiFi();
  // Create a ProxyConfigServiceImpl like for the system request context.
  SetUpProxyConfigService(nullptr /* no profile prefs */);
  for (size_t i = 0; i < std::size(tests); ++i) {
    SCOPED_TRACE(base::StringPrintf("Test[%" PRIuS "] %s", i,
                                    tests[i].description.c_str()));

    base::Value::Dict test_config = InitConfigWithTestInput(tests[i].input);
    SetUserConfigInShill(&test_config);

    net::ProxyConfigWithAnnotation config;
    SyncGetLatestProxyConfig(&config);

    EXPECT_EQ(tests[i].auto_detect, config.value().auto_detect());
    EXPECT_EQ(GURL(tests[i].pac_url), config.value().pac_url());
    EXPECT_TRUE(tests[i].proxy_rules.Matches(config.value().proxy_rules()));
  }
}

TEST_F(ProxyConfigServiceImplTest, DynamicPrefsOverride) {
  SetUpPrivateWiFi();
  // Create a ProxyConfigServiceImpl like for the system request context.
  SetUpProxyConfigService(nullptr /* no profile prefs */);
  // Groupings of 3 test inputs to use for managed, recommended and network
  // proxies respectively.  Only valid and non-direct test inputs are used.
  const size_t proxies[][3] = {
      // clang-format off
    { 1, 2, 4, },
    { 1, 4, 2, },
    { 4, 2, 1, },
    { 2, 1, 4, },
    { 2, 4, 5, },
    { 2, 5, 4, },
    { 5, 4, 2, },
    { 4, 2, 5, },
    { 4, 5, 6, },
    { 4, 6, 5, },
    { 6, 5, 4, },
    { 5, 4, 6, },
    { 5, 6, 7, },
    { 5, 7, 6, },
    { 7, 6, 5, },
    { 6, 5, 7, },
    { 6, 7, 8, },
    { 6, 8, 7, },
    { 8, 7, 6, },
    { 7, 6, 8, },
      // clang-format on
  };
  for (size_t i = 0; i < std::size(proxies); ++i) {
    const TestParams& managed_params = tests[proxies[i][0]];
    const TestParams& recommended_params = tests[proxies[i][1]];
    const TestParams& network_params = tests[proxies[i][2]];

    SCOPED_TRACE(base::StringPrintf(
        "Test[%" PRIuS "] managed=[%s], recommended=[%s], network=[%s]", i,
        managed_params.description.c_str(),
        recommended_params.description.c_str(),
        network_params.description.c_str()));

    base::Value managed_config(InitConfigWithTestInput(managed_params.input));
    base::Value recommended_config(
        InitConfigWithTestInput(recommended_params.input));

    // Managed proxy pref should take effect over recommended proxy and
    // non-existent network proxy.
    SetUserConfigInShill(nullptr);
    pref_service_.SetManagedPref(
        ::proxy_config::prefs::kProxy,
        base::Value::ToUniquePtrValue(managed_config.Clone()));
    pref_service_.SetRecommendedPref(
        ::proxy_config::prefs::kProxy,
        base::Value::ToUniquePtrValue(recommended_config.Clone()));
    net::ProxyConfigWithAnnotation actual_config;
    SyncGetLatestProxyConfig(&actual_config);
    EXPECT_EQ(managed_params.auto_detect, actual_config.value().auto_detect());
    EXPECT_EQ(GURL(managed_params.pac_url), actual_config.value().pac_url());
    EXPECT_TRUE(managed_params.proxy_rules.Matches(
        actual_config.value().proxy_rules()));

    // Recommended proxy pref should take effect when managed proxy pref is
    // removed.
    pref_service_.RemoveManagedPref(::proxy_config::prefs::kProxy);
    SyncGetLatestProxyConfig(&actual_config);
    EXPECT_EQ(recommended_params.auto_detect,
              actual_config.value().auto_detect());
    EXPECT_EQ(GURL(recommended_params.pac_url),
              actual_config.value().pac_url());
    EXPECT_TRUE(recommended_params.proxy_rules.Matches(
        actual_config.value().proxy_rules()));

    base::Value::Dict network_config =
        InitConfigWithTestInput(network_params.input);
    // Network proxy should take take effect over recommended proxy pref.
    SetUserConfigInShill(&network_config);
    SyncGetLatestProxyConfig(&actual_config);
    EXPECT_EQ(network_params.auto_detect, actual_config.value().auto_detect());
    EXPECT_EQ(GURL(network_params.pac_url), actual_config.value().pac_url());
    EXPECT_TRUE(network_params.proxy_rules.Matches(
        actual_config.value().proxy_rules()));

    // Managed proxy pref should take effect over network proxy.
    pref_service_.SetManagedPref(
        ::proxy_config::prefs::kProxy,
        base::Value::ToUniquePtrValue(managed_config.Clone()));
    SyncGetLatestProxyConfig(&actual_config);
    EXPECT_EQ(managed_params.auto_detect, actual_config.value().auto_detect());
    EXPECT_EQ(GURL(managed_params.pac_url), actual_config.value().pac_url());
    EXPECT_TRUE(managed_params.proxy_rules.Matches(
        actual_config.value().proxy_rules()));

    // Network proxy should take effect over recommended proxy pref when managed
    // proxy pref is removed.
    pref_service_.RemoveManagedPref(::proxy_config::prefs::kProxy);
    SyncGetLatestProxyConfig(&actual_config);
    EXPECT_EQ(network_params.auto_detect, actual_config.value().auto_detect());
    EXPECT_EQ(GURL(network_params.pac_url), actual_config.value().pac_url());
    EXPECT_TRUE(network_params.proxy_rules.Matches(
        actual_config.value().proxy_rules()));

    // Removing recommended proxy pref should have no effect on network proxy.
    pref_service_.RemoveRecommendedPref(::proxy_config::prefs::kProxy);
    SyncGetLatestProxyConfig(&actual_config);
    EXPECT_EQ(network_params.auto_detect, actual_config.value().auto_detect());
    EXPECT_EQ(GURL(network_params.pac_url), actual_config.value().pac_url());
    EXPECT_TRUE(network_params.proxy_rules.Matches(
        actual_config.value().proxy_rules()));
  }
}

// Tests whether the proxy settings from user policy are used for ethernet even
// if 'UseSharedProxies' is set to false.
// See https://crbug.com/454966 .
TEST_F(ProxyConfigServiceImplTest, SharedEthernetAndUserPolicy) {
  SetUpSharedEthernet();
  SetUpProxyConfigService(&profile_prefs_);

  std::optional<base::Value::Dict> ethernet_policy =
      chromeos::onc::ReadDictionaryFromJson(kEthernetPolicy);
  ASSERT_TRUE(ethernet_policy.has_value());

  base::Value::List network_configs;
  network_configs.Append(std::move(*ethernet_policy));

  profile_prefs_.SetUserPref(::proxy_config::prefs::kUseSharedProxies,
                             std::make_unique<base::Value>(false));
  profile_prefs_.SetManagedPref(
      ::onc::prefs::kOpenNetworkConfiguration,
      std::make_unique<base::Value>(std::move(network_configs)));

  net::ProxyConfigWithAnnotation actual_config;
  SyncGetLatestProxyConfig(&actual_config);
  net::ProxyConfigWithAnnotation expected_config(
      net::ProxyConfig::CreateFromCustomPacURL(GURL("http://domain.com/x")),
      TRAFFIC_ANNOTATION_FOR_TESTS);
  EXPECT_TRUE(expected_config.value().Equals(actual_config.value()));
}

}  // namespace ash