chromium/chrome/browser/ash/net/network_portal_detector_impl_browsertest.cc

// 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 "chrome/browser/ash/net/network_portal_detector_impl.h"

#include <memory>

#include "base/command_line.h"
#include "base/compiler_specific.h"
#include "base/functional/bind.h"
#include "base/functional/callback_helpers.h"
#include "base/location.h"
#include "base/memory/raw_ptr.h"
#include "base/run_loop.h"
#include "base/task/single_thread_task_runner.h"
#include "chrome/browser/ash/login/login_manager_test.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/notifications/notification_display_service_tester.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/profiles/profile_manager.h"
#include "chrome/browser/ui/ash/network/network_portal_notification_controller.h"
#include "chrome/grit/generated_resources.h"
#include "chromeos/ash/components/dbus/shill/shill_service_client.h"
#include "chromeos/ash/components/network/network_handler.h"
#include "chromeos/ash/components/network/network_state.h"
#include "chromeos/ash/components/network/network_state_handler.h"
#include "chromeos/ash/components/network/portal_detector/network_portal_detector.h"
#include "chromeos/constants/pref_names.h"
#include "components/account_id/account_id.h"
#include "components/captive_portal/core/captive_portal_testing_utils.h"
#include "components/prefs/pref_service.h"
#include "components/sync_preferences/pref_service_syncable.h"
#include "content/public/test/browser_test.h"
#include "dbus/object_path.h"
#include "net/base/net_errors.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/cros_system_api/dbus/service_constants.h"
#include "ui/base/l10n/l10n_util.h"

namespace ash {

class NetworkPortalWebDialog;

namespace {

const char* const kNotificationId =
    NetworkPortalNotificationController::kNotificationId;

constexpr char kTestUser[] = "[email protected]";
constexpr char kTestUserGaiaId[] = "1234567890";
constexpr char kWifiServicePath[] = "/service/wifi";
constexpr char kWifiGuid[] = "wifi";

void ErrorCallbackFunction(const std::string& error_name,
                           const std::string& error_message) {
  LOG(FATAL) << "Shill Error: " << error_name << " : " << error_message;
}

void SetConnected(const std::string& service_path) {
  ShillServiceClient::Get()->Connect(dbus::ObjectPath(service_path),
                                     base::DoNothing(),
                                     base::BindOnce(&ErrorCallbackFunction));
  base::RunLoop().RunUntilIdle();
}

void SetDisconnected(const std::string& service_path) {
  ShillServiceClient::Get()->Disconnect(dbus::ObjectPath(service_path),
                                        base::DoNothing(),
                                        base::BindOnce(&ErrorCallbackFunction));
  base::RunLoop().RunUntilIdle();
}

void SetState(const char* state) {
  ShillServiceClient::Get()->SetProperty(
      dbus::ObjectPath(kWifiServicePath), shill::kStateProperty,
      base::Value(state), base::DoNothing(),
      base::BindOnce(&ErrorCallbackFunction));
  base::RunLoop().RunUntilIdle();
}

}  // namespace

class NetworkPortalDetectorImplBrowserTest
    : public LoginManagerTest,
      public captive_portal::CaptivePortalDetectorTestBase {
 public:
  NetworkPortalDetectorImplBrowserTest()
      : test_account_id_(
            AccountId::FromUserEmailGaiaId(kTestUser, kTestUserGaiaId)) {}

  NetworkPortalDetectorImplBrowserTest(
      const NetworkPortalDetectorImplBrowserTest&) = delete;
  NetworkPortalDetectorImplBrowserTest& operator=(
      const NetworkPortalDetectorImplBrowserTest&) = delete;

  ~NetworkPortalDetectorImplBrowserTest() override = default;

  // Tests that the Shill state sets the expected NetworkState::PortalState
  // and generates the expected notification. Note: This does not run any
  // Chrome portal detection, that is covered by unit tests.
  void TestPortalStateAndNotification(
      const char* shill_state,
      NetworkState::PortalState portal_state,
      const std::u16string& expected_title,
      const std::u16string& expected_message,
      const std::u16string& expected_button_title) {
    LoginUser(test_account_id_);
    base::RunLoop().RunUntilIdle();

    EXPECT_FALSE(display_service_->GetNotification(kNotificationId));

    // Set connected should not trigger portal detection.
    SetConnected(kWifiServicePath);

    NetworkStateHandler* network_state_handler =
        NetworkHandler::Get()->network_state_handler();
    const NetworkState* default_network =
        network_state_handler->DefaultNetwork();
    ASSERT_TRUE(default_network);
    EXPECT_EQ(default_network->GetPortalState(),
              NetworkState::PortalState::kOnline);
    EXPECT_FALSE(display_service_->GetNotification(kNotificationId));

    // Setting a shill portal state should set portal detection and display a
    // notification
    SetState(shill_state);

    default_network = network_state_handler->DefaultNetwork();
    ASSERT_TRUE(default_network);
    EXPECT_EQ(default_network->GetPortalState(), portal_state);
    EXPECT_TRUE(display_service_->GetNotification(kNotificationId));
    EXPECT_EQ(GetNotificationTitle(), expected_title);
    EXPECT_EQ(GetNotificationMessage(), expected_message);
    if (expected_button_title.empty()) {
      EXPECT_EQ(
          display_service_->GetNotification(kNotificationId)->buttons().size(),
          0u);
    } else {
      EXPECT_EQ(GetNotificationButtonTitle(), expected_button_title);
    }

    // Explicitly close the notification.
    display_service_->RemoveNotification(NotificationHandler::Type::TRANSIENT,
                                         kNotificationId, /*by_user=*/true);
  }

  void SetUpOnMainThread() override {
    LoginManagerTest::SetUpOnMainThread();

    ShillServiceClient::TestInterface* service_test =
        ShillServiceClient::Get()->GetTestInterface();
    service_test->ClearServices();
    service_test->AddService(kWifiServicePath, kWifiGuid, "wifi",
                             shill::kTypeWifi, shill::kStateIdle,
                             /*visible=*/true);

    display_service_ = std::make_unique<NotificationDisplayServiceTester>(
        /*profile=*/nullptr);

    network_portal_detector_ =
        new NetworkPortalDetectorImpl(test_loader_factory());
    // Takes ownership of |network_portal_detector_|:
    network_portal_detector::InitializeForTesting(network_portal_detector_);
    network_portal_detector_->enabled_ = true;
    set_detector(network_portal_detector_->captive_portal_detector_.get());
    network_portal_notification_controller_ =
        std::make_unique<NetworkPortalNotificationController>();
    base::RunLoop().RunUntilIdle();
  }

  void TearDownOnMainThread() override {
    network_portal_notification_controller_.reset();
  }

  void SetIgnoreNoNetworkForTesting() {
    network_portal_notification_controller_->SetIgnoreNoNetworkForTesting();
  }

  const std::u16string GetNotificationTitle() {
    return display_service_->GetNotification(kNotificationId)->title();
  }

  const std::u16string GetNotificationMessage() {
    return display_service_->GetNotification(kNotificationId)->message();
  }

  const std::u16string GetNotificationButtonTitle() {
    return display_service_->GetNotification(kNotificationId)
        ->buttons()
        .front()
        .title;
  }

 protected:
  AccountId test_account_id_;
  std::unique_ptr<NotificationDisplayServiceTester> display_service_;
  raw_ptr<NetworkPortalDetectorImpl, DanglingUntriaged>
      network_portal_detector_ = nullptr;
  std::unique_ptr<NetworkPortalNotificationController>
      network_portal_notification_controller_;
};

IN_PROC_BROWSER_TEST_F(NetworkPortalDetectorImplBrowserTest,
                       InSessionDetectionRedirectFoundState) {
  TestPortalStateAndNotification(
      shill::kStateRedirectFound, NetworkState::PortalState::kPortal,
      l10n_util::GetStringUTF16(
          IDS_NEW_PORTAL_DETECTION_NOTIFICATION_TITLE_WIFI),
      l10n_util::GetStringFUTF16(IDS_NEW_PORTAL_DETECTION_NOTIFICATION_MESSAGE,
                                 u"wifi"),
      l10n_util::GetStringUTF16(IDS_NEW_PORTAL_DETECTION_NOTIFICATION_BUTTON));
}

IN_PROC_BROWSER_TEST_F(NetworkPortalDetectorImplBrowserTest,
                       InSessionDetectionPortalSuspectedState) {
  TestPortalStateAndNotification(
      shill::kStatePortalSuspected, NetworkState::PortalState::kPortalSuspected,
      l10n_util::GetStringUTF16(
          IDS_NEW_PORTAL_DETECTION_NOTIFICATION_TITLE_WIFI),
      l10n_util::GetStringFUTF16(IDS_NEW_PORTAL_DETECTION_NOTIFICATION_MESSAGE,
                                 u"wifi"),
      l10n_util::GetStringUTF16(IDS_NEW_PORTAL_DETECTION_NOTIFICATION_BUTTON));
}

IN_PROC_BROWSER_TEST_F(NetworkPortalDetectorImplBrowserTest,
                       PortalStateChangedBetweenPortalStates) {
  LoginUser(test_account_id_);
  base::RunLoop().RunUntilIdle();

  // User connects to portalled wifi.
  SetConnected(kWifiServicePath);
  SetState(shill::kStateRedirectFound);

  // Verify notification properties.
  NetworkStateHandler* network_state_handler =
      NetworkHandler::Get()->network_state_handler();
  const NetworkState* default_network = network_state_handler->DefaultNetwork();
  ASSERT_TRUE(default_network);
  EXPECT_EQ(default_network->GetPortalState(),
            NetworkState::PortalState::kPortal);
  EXPECT_TRUE(display_service_->GetNotification(kNotificationId));
  EXPECT_EQ(GetNotificationTitle(),
            l10n_util::GetStringUTF16(
                IDS_NEW_PORTAL_DETECTION_NOTIFICATION_TITLE_WIFI));
  EXPECT_EQ(GetNotificationMessage(),
            l10n_util::GetStringFUTF16(
                IDS_NEW_PORTAL_DETECTION_NOTIFICATION_MESSAGE, u"wifi"));
  EXPECT_EQ(
      GetNotificationButtonTitle(),
      l10n_util::GetStringUTF16(IDS_NEW_PORTAL_DETECTION_NOTIFICATION_BUTTON));

  // State changes to portal-suspected. Notification properties shouldn't
  // change.
  SetState(shill::kStatePortalSuspected);
  ASSERT_TRUE(default_network);
  EXPECT_TRUE(display_service_->GetNotification(kNotificationId));
  EXPECT_EQ(GetNotificationTitle(),
            l10n_util::GetStringUTF16(
                IDS_NEW_PORTAL_DETECTION_NOTIFICATION_TITLE_WIFI));
  EXPECT_EQ(GetNotificationMessage(),
            l10n_util::GetStringFUTF16(
                IDS_NEW_PORTAL_DETECTION_NOTIFICATION_MESSAGE, u"wifi"));
  EXPECT_EQ(
      GetNotificationButtonTitle(),
      l10n_util::GetStringUTF16(IDS_NEW_PORTAL_DETECTION_NOTIFICATION_BUTTON));

  // Explicitly close the notification.
  display_service_->RemoveNotification(NotificationHandler::Type::TRANSIENT,
                                       kNotificationId, /*by_user=*/true);
}

IN_PROC_BROWSER_TEST_F(NetworkPortalDetectorImplBrowserTest,
                       ReconnectionNewNotification) {
  LoginUser(test_account_id_);
  base::RunLoop().RunUntilIdle();

  // User connects to portalled wifi.
  SetConnected(kWifiServicePath);
  SetState(shill::kStateRedirectFound);
  EXPECT_TRUE(display_service_->GetNotification(kNotificationId));

  // Disconnect from portalled wifi.
  SetDisconnected(kWifiServicePath);
  EXPECT_FALSE(display_service_->GetNotification(kNotificationId));

  // Verify notification when reconnecting to same portalled wifi.
  SetConnected(kWifiServicePath);
  SetState(shill::kStateRedirectFound);
  EXPECT_TRUE(display_service_->GetNotification(kNotificationId));
}

IN_PROC_BROWSER_TEST_F(NetworkPortalDetectorImplBrowserTest,
                       UserDismissedNotificationNoNewNotification) {
  LoginUser(test_account_id_);
  base::RunLoop().RunUntilIdle();

  // User connects to portalled wifi.
  SetConnected(kWifiServicePath);
  SetState(shill::kStateRedirectFound);
  EXPECT_TRUE(display_service_->GetNotification(kNotificationId));

  // Close Notification.
  display_service_->RemoveNotification(NotificationHandler::Type::TRANSIENT,
                                       kNotificationId, /*by_user=*/true);
  // Change state to redirect-found.
  SetState(shill::kStateRedirectFound);
  // Verify notification does not exist after user closed.
  EXPECT_FALSE(display_service_->GetNotification(kNotificationId));
}

class NetworkPortalDetectorImplBrowserTestIgnoreProxy
    : public NetworkPortalDetectorImplBrowserTest,
      public testing::WithParamInterface<bool> {
 public:
  NetworkPortalDetectorImplBrowserTestIgnoreProxy() = default;

  NetworkPortalDetectorImplBrowserTestIgnoreProxy(
      const NetworkPortalDetectorImplBrowserTestIgnoreProxy&) = delete;
  NetworkPortalDetectorImplBrowserTestIgnoreProxy& operator=(
      const NetworkPortalDetectorImplBrowserTestIgnoreProxy&) = delete;

 protected:
  void TestImpl(const bool preference_value);
};

void NetworkPortalDetectorImplBrowserTestIgnoreProxy::TestImpl(
    const bool preference_value) {
  LoginUser(test_account_id_);
  base::RunLoop().RunUntilIdle();

  SetIgnoreNoNetworkForTesting();

  ProfileManager::GetActiveUserProfile()->GetPrefs()->SetBoolean(
      chromeos::prefs::kCaptivePortalAuthenticationIgnoresProxy,
      preference_value);

  // User connects to portalled wifi.
  SetConnected(kWifiServicePath);
  SetState(shill::kStateRedirectFound);

  NetworkStateHandler* network_state_handler =
      NetworkHandler::Get()->network_state_handler();
  const NetworkState* default_network = network_state_handler->DefaultNetwork();
  ASSERT_TRUE(default_network);

  // Check that the network is behind a portal and a notification is displayed.
  EXPECT_TRUE(display_service_->GetNotification(kNotificationId));
  EXPECT_EQ(default_network->GetPortalState(),
            NetworkState::PortalState::kPortal);

  display_service_->GetNotification(kNotificationId)
      ->delegate()
      ->Click(std::nullopt, std::nullopt);

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

IN_PROC_BROWSER_TEST_P(NetworkPortalDetectorImplBrowserTestIgnoreProxy,
                       TestWithPreference) {
  TestImpl(GetParam());
}

INSTANTIATE_TEST_SUITE_P(CaptivePortalAuthenticationIgnoresProxy,
                         NetworkPortalDetectorImplBrowserTestIgnoreProxy,
                         testing::Bool());

}  // namespace ash