chromium/chromeos/ash/components/phonehub/tether_controller_impl_unittest.cc

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

#include "chromeos/ash/components/phonehub/tether_controller_impl.h"

#include <memory>

#include "base/memory/weak_ptr.h"
#include "base/strings/stringprintf.h"
#include "base/strings/utf_string_conversions.h"
#include "base/test/task_environment.h"
#include "chromeos/ash/components/network/network_state_handler.h"
#include "chromeos/ash/components/network/network_state_test_helper.h"
#include "chromeos/ash/components/phonehub/fake_user_action_recorder.h"
#include "chromeos/ash/components/phonehub/mutable_phone_model.h"
#include "chromeos/ash/components/phonehub/phone_model_test_util.h"
#include "chromeos/ash/components/phonehub/phone_status_model.h"
#include "chromeos/ash/services/multidevice_setup/public/cpp/fake_multidevice_setup_client.h"
#include "chromeos/ash/services/network_config/in_process_instance.h"
#include "chromeos/ash/services/network_config/public/cpp/cros_network_config_test_helper.h"
#include "testing/gtest/include/gtest/gtest.h"

namespace ash {
namespace phonehub {

namespace {

using ::chromeos::network_config::mojom::ConnectionStateType;
using ::chromeos::network_config::mojom::NetworkStatePropertiesPtr;
using ::chromeos::network_config::mojom::StartConnectResult;
using multidevice_setup::mojom::Feature;
using multidevice_setup::mojom::FeatureState;

constexpr char kWifiGuid[] = "WifiGuid";
constexpr char kTetherGuid[] = "TetherGuid";
constexpr char kTetherNetworkName[] = "TetherNetworkName";
constexpr char kTetherNetworkCarrier[] = "TetherNetworkCarrier";
constexpr int kBatteryPercentage = 100;
constexpr int kSignalStrength = 100;
constexpr bool kHasConnectedToHost = true;

class FakeObserver : public TetherController::Observer {
 public:
  FakeObserver() = default;
  ~FakeObserver() override = default;

  size_t num_status_changes() const { return num_status_changes_; }
  size_t num_scan_failed() const { return num_scan_failed_; }

  // TetherController::Observer:
  void OnTetherStatusChanged() override { ++num_status_changes_; }
  void OnAttemptConnectionScanFailed() override { ++num_scan_failed_; }

 private:
  size_t num_status_changes_ = 0;
  size_t num_scan_failed_ = 0;
};

PhoneStatusModel CreateTestPhoneStatusModel(
    PhoneStatusModel::MobileStatus mobile_status =
        PhoneStatusModel::MobileStatus::kSimWithReception) {
  PhoneStatusModel::MobileConnectionMetadata metadata;
  metadata.signal_strength = PhoneStatusModel::SignalStrength::kFourBars;
  metadata.mobile_provider =
      mobile_status == PhoneStatusModel::MobileStatus::kSimWithReception
          ? std::u16string(kFakeMobileProviderName)
          : std::u16string();
  return PhoneStatusModel(mobile_status, metadata,
                          PhoneStatusModel::ChargingState::kNotCharging,
                          PhoneStatusModel::BatterySaverState::kOff,
                          /*battery_percentage=*/100u);
}

}  // namespace

class TetherControllerImplTest : public testing::Test {
 protected:
  friend class TetherControllerImpl;

  TetherControllerImplTest() = default;
  TetherControllerImplTest(const TetherControllerImplTest&) = delete;
  TetherControllerImplTest& operator=(const TetherControllerImplTest&) = delete;
  ~TetherControllerImplTest() override = default;

  class FakeTetherNetworkConnector
      : public TetherControllerImpl::TetherNetworkConnector {
   public:
    FakeTetherNetworkConnector() {
      network_config::BindToInProcessInstance(
          cros_network_config_.BindNewPipeAndPassReceiver());
    }
    ~FakeTetherNetworkConnector() override = default;

    void StartConnect(const std::string& guid,
                      StartConnectCallback callback) override {
      start_connect_callback_ = std::move(callback);
    }

    void StartDisconnect(const std::string& guid,
                         StartDisconnectCallback callback) override {
      start_disconnect_callback_ = std::move(callback);
    }

    void GetNetworkStateList(
        chromeos::network_config::mojom::NetworkFilterPtr filter,
        GetNetworkStateListCallback callback) override {
      cros_network_config_->GetNetworkStateList(
          std::move(filter),
          base::BindOnce(
              &FakeTetherNetworkConnector::OnVisibleTetherNetworkFetched,
              weak_ptr_factory_.GetWeakPtr(), std::move(callback)));
    }

    void SetNextConnectionStateType(ConnectionStateType connection_state) {
      connection_state_ = connection_state;
    }

    void OnVisibleTetherNetworkFetched(
        GetNetworkStateListCallback callback,
        std::vector<NetworkStatePropertiesPtr> networks) {
      if (connection_state_.has_value() && networks.size() == 1) {
        networks[0]->connection_state = *connection_state_;
        connection_state_ = std::nullopt;
      }

      std::move(callback).Run(std::move(networks));
    }

    bool DoesPendingStartConnectCallbackExist() {
      return !start_connect_callback_.is_null();
    }

    bool DoesPendingDisconnectCallbackExist() {
      return !start_disconnect_callback_.is_null();
    }

    void InvokeStartConnectCallbackWithFakeResult(
        StartConnectResult result = StartConnectResult::kSuccess,
        const std::string& message = "") {
      std::move(start_connect_callback_).Run(result, message);
    }

    void InvokeDisconnectCallbackWithRealResults(std::string service_path) {
      cros_network_config_->StartDisconnect(
          service_path, std::move(start_disconnect_callback_));
    }

    void InvokeDisconnectCallbackWithFakeParams(bool success = true) {
      std::move(start_disconnect_callback_).Run(success);
    }

   private:
    mojo::Remote<chromeos::network_config::mojom::CrosNetworkConfig>
        cros_network_config_;
    std::optional<ConnectionStateType> connection_state_;
    StartConnectCallback start_connect_callback_;
    StartDisconnectCallback start_disconnect_callback_;
    base::WeakPtrFactory<FakeTetherNetworkConnector> weak_ptr_factory_{this};
  };

  // testing::Test:
  void SetUp() override {
    NetworkHandler::Initialize();
    base::RunLoop().RunUntilIdle();

    service_path_ =
        cros_network_config_helper_.network_state_helper().ConfigureService(
            base::StringPrintf(
                R"({"GUID": "%s", "Type": "wifi",
             "State": "ready", "Strength": 100,
            "Connectable": true})",
                kWifiGuid));

    fake_phone_model_.SetPhoneStatusModel(CreateTestPhoneStatusModel());

    controller_ =
        base::WrapUnique<TetherControllerImpl>(new TetherControllerImpl(
            &fake_phone_model_, &fake_user_action_recorder_,
            &fake_multidevice_setup_client_,
            std::make_unique<FakeTetherNetworkConnector>()));
    controller_->AddObserver(&fake_observer_);
  }

  void TearDown() override {
    controller_->RemoveObserver(&fake_observer_);
    NetworkHandler::Shutdown();
    testing::Test::TearDown();
  }

  NetworkStateHandler* network_state_handler() {
    return cros_network_config_helper_.network_state_helper()
        .network_state_handler();
  }

  FakeTetherNetworkConnector* fake_tether_network_connector() {
    return static_cast<FakeTetherNetworkConnector*>(
        controller_->connector_.get());
  }

  void EnableTetherDevice() {
    network_state_handler()->SetTetherTechnologyState(
        NetworkStateHandler::TechnologyState::TECHNOLOGY_ENABLED);
    base::RunLoop().RunUntilIdle();
  }

  void DisconnectTetherDevice() {
    network_state_handler()->SetTetherTechnologyState(
        NetworkStateHandler::TechnologyState::TECHNOLOGY_AVAILABLE);
    base::RunLoop().RunUntilIdle();
  }

  void AddVisibleTetherNetwork() {
    network_state_handler()->AddTetherNetworkState(
        kTetherGuid, kTetherNetworkName, kTetherNetworkCarrier,
        kBatteryPercentage, kSignalStrength, kHasConnectedToHost);
    network_state_handler()->AssociateTetherNetworkStateWithWifiNetwork(
        kTetherGuid, kWifiGuid);
    base::RunLoop().RunUntilIdle();
  }

  void RemoveVisibleTetherNetwork() {
    network_state_handler()->RemoveTetherNetworkState(kTetherGuid);
    base::RunLoop().RunUntilIdle();
  }

  void SetTetherNetworkStateConnected() {
    network_state_handler()->SetTetherNetworkStateConnected(kTetherGuid);
    base::RunLoop().RunUntilIdle();
  }

  void SetTetherNetworkStateConnecting() {
    network_state_handler()->SetTetherNetworkStateConnecting(kTetherGuid);
    base::RunLoop().RunUntilIdle();
  }

  void SetTetherNetworkStateDisconnected() {
    network_state_handler()->SetTetherNetworkStateDisconnected(kTetherGuid);
    base::RunLoop().RunUntilIdle();
  }

  void SetTetherScanState(bool is_scanning) {
    network_state_handler()->SetTetherScanState(is_scanning);
    base::RunLoop().RunUntilIdle();
  }

  TetherController::Status GetStatus() const {
    return controller_->GetStatus();
  }

  void SetMultideviceSetupFeatureState(FeatureState feature_state) {
    fake_multidevice_setup_client_.SetFeatureState(Feature::kInstantTethering,
                                                   feature_state);
  }

  void InvokeDisconnectCallbackWithRealResults() {
    fake_tether_network_connector()->InvokeDisconnectCallbackWithRealResults(
        service_path_);
    base::RunLoop().RunUntilIdle();
  }

  void InvokePendingSetFeatureEnabledStateCallback(
      bool success,
      bool expected_enabled = true) {
    if (success)
      SetMultideviceSetupFeatureState(FeatureState::kEnabledByUser);

    fake_multidevice_setup_client_.InvokePendingSetFeatureEnabledStateCallback(
        Feature::kInstantTethering,
        /*expected_enabled=*/expected_enabled, std::nullopt, success);
  }

  void AttemptConnection() {
    size_t num_recorded_connection_attempts_before_call =
        fake_user_action_recorder_.num_tether_attempts();
    controller_->AttemptConnection();
    EXPECT_EQ(num_recorded_connection_attempts_before_call + 1,
              fake_user_action_recorder_.num_tether_attempts());
  }

  void Disconnect() { controller_->Disconnect(); }

  size_t GetNumObserverStatusChanged() const {
    return fake_observer_.num_status_changes();
  }

  size_t GetNumObserverScanFailed() const {
    return fake_observer_.num_scan_failed();
  }

  MutablePhoneModel* phone_model() { return &fake_phone_model_; }

 private:
  base::test::TaskEnvironment task_environment_;
  network_config::CrosNetworkConfigTestHelper cros_network_config_helper_;
  std::string service_path_;
  multidevice_setup::FakeMultiDeviceSetupClient fake_multidevice_setup_client_;
  MutablePhoneModel fake_phone_model_;
  FakeUserActionRecorder fake_user_action_recorder_;
  FakeObserver fake_observer_;
  std::unique_ptr<TetherControllerImpl> controller_;
};

TEST_F(TetherControllerImplTest,
       DisconnectCompletesAfterOnActiveNetworksChanged) {
  SetMultideviceSetupFeatureState(FeatureState::kEnabledByUser);

  EnableTetherDevice();
  AddVisibleTetherNetwork();

  // Disconnect from a connecting state.
  AttemptConnection();
  EXPECT_EQ(GetStatus(), TetherController::Status::kConnecting);

  SetTetherNetworkStateConnecting();
  Disconnect();

  // Simulate OnActiveNetworksChanged() being called after Disconnect()
  // is requested when Bluetooth is on but hotspot is off, yielding a
  // ConnectionStateType::kConnecting tether network instead of a
  // ConnectionStateType::kDisconnected network.
  fake_tether_network_connector()->SetNextConnectionStateType(
      ConnectionStateType::kConnecting);
  SetTetherNetworkStateDisconnected();
  EXPECT_EQ(GetStatus(), TetherController::Status::kConnecting);

  // Upon invoking the Disconnect callback, a refetch occurs.
  EXPECT_TRUE(
      fake_tether_network_connector()->DoesPendingDisconnectCallbackExist());
  InvokeDisconnectCallbackWithRealResults();
  EXPECT_EQ(GetStatus(), TetherController::Status::kConnectionAvailable);
}

TEST_F(TetherControllerImplTest, ExternalTetherChangesReflectToStatus) {
  EXPECT_EQ(GetStatus(), TetherController::Status::kIneligibleForFeature);
  SetMultideviceSetupFeatureState(FeatureState::kEnabledByUser);
  EXPECT_EQ(GetNumObserverStatusChanged(), 1U);
  EXPECT_EQ(GetStatus(), TetherController::Status::kConnectionUnavailable);

  // Tether device and network must be enabled for status changes other than
  // kIneligibleForFeature or kConnectionUnavailable to occur.
  EnableTetherDevice();
  AddVisibleTetherNetwork();
  EXPECT_EQ(GetStatus(), TetherController::Status::kConnectionAvailable);
  EXPECT_EQ(GetNumObserverStatusChanged(), 2U);

  // Starts connecting to tether network.
  SetTetherNetworkStateConnecting();
  EXPECT_EQ(GetStatus(), TetherController::Status::kConnecting);
  EXPECT_EQ(GetNumObserverStatusChanged(), 3U);

  // Connected to tether network.
  SetTetherNetworkStateConnected();
  EXPECT_EQ(GetStatus(), TetherController::Status::kConnected);
  EXPECT_EQ(GetNumObserverStatusChanged(), 4U);

  // Tether network disconnects on it's own.
  SetTetherNetworkStateDisconnected();
  EXPECT_EQ(GetStatus(), TetherController::Status::kConnectionAvailable);
  EXPECT_EQ(GetNumObserverStatusChanged(), 5U);

  // Tether network becomes unavailable.
  RemoveVisibleTetherNetwork();
  EXPECT_EQ(GetStatus(), TetherController::Status::kConnectionUnavailable);
  EXPECT_EQ(GetNumObserverStatusChanged(), 6U);

  // Phone status changed to no reception.
  phone_model()->SetPhoneStatusModel(CreateTestPhoneStatusModel(
      PhoneStatusModel::MobileStatus::kSimButNoReception));
  EXPECT_EQ(GetStatus(), TetherController::Status::kNoReception);
  EXPECT_EQ(GetNumObserverStatusChanged(), 7U);

  // Phone status changed to no SIM.
  phone_model()->SetPhoneStatusModel(
      CreateTestPhoneStatusModel(PhoneStatusModel::MobileStatus::kNoSim));
  EXPECT_EQ(GetStatus(), TetherController::Status::kNoReception);
  EXPECT_EQ(GetNumObserverStatusChanged(), 7U);

  // Tether feature becomes unsupported,
  SetMultideviceSetupFeatureState(FeatureState::kNotSupportedByPhone);
  EXPECT_EQ(GetStatus(), TetherController::Status::kIneligibleForFeature);
  EXPECT_EQ(GetNumObserverStatusChanged(), 8U);

  // Tether feature becomes supported, the status becomes kNoReception again.
  SetMultideviceSetupFeatureState(FeatureState::kEnabledByUser);
  EXPECT_EQ(GetStatus(), TetherController::Status::kNoReception);
  EXPECT_EQ(GetNumObserverStatusChanged(), 9U);

  // Phone status changed to having reception. Connection is still unavailable.
  phone_model()->SetPhoneStatusModel(CreateTestPhoneStatusModel(
      PhoneStatusModel::MobileStatus::kSimWithReception));
  EXPECT_EQ(GetStatus(), TetherController::Status::kConnectionUnavailable);
  EXPECT_EQ(GetNumObserverStatusChanged(), 10U);

  // Phone Model is lost, connection is still unavailable.
  phone_model()->SetPhoneStatusModel(std::nullopt);
  EXPECT_EQ(GetStatus(), TetherController::Status::kConnectionUnavailable);
  EXPECT_EQ(GetNumObserverStatusChanged(), 10U);

  // Even though there is no Phone Model, adding a visible tether network
  // will cause the controller to indicate a connection is available.
  AddVisibleTetherNetwork();
  EXPECT_EQ(GetStatus(), TetherController::Status::kConnectionAvailable);
  EXPECT_EQ(GetNumObserverStatusChanged(), 11U);

  // Even though there is no Phone Model, connecting to a visible tether network
  // externally (e.g via OS Settings) will cause the controller to indicate a
  // connecting tether state.
  SetTetherNetworkStateConnecting();
  EXPECT_EQ(GetStatus(), TetherController::Status::kConnecting);
  EXPECT_EQ(GetNumObserverStatusChanged(), 12U);

  // Even though there is no Phone Model, a connection to a visible tether
  // network externally (e.g via OS Settings) will cause the controller to
  // indicate a connected tether state.
  SetTetherNetworkStateConnected();
  EXPECT_EQ(GetStatus(), TetherController::Status::kConnected);
  EXPECT_EQ(GetNumObserverStatusChanged(), 13U);
}

TEST_F(TetherControllerImplTest, AttemptConnectDisconnect) {
  SetMultideviceSetupFeatureState(FeatureState::kEnabledByUser);

  EnableTetherDevice();
  AddVisibleTetherNetwork();

  AttemptConnection();
  EXPECT_TRUE(
      fake_tether_network_connector()->DoesPendingStartConnectCallbackExist());

  // Upon completing the connection, the status should no longer be connecting.
  EXPECT_EQ(GetStatus(), TetherController::Status::kConnecting);
  fake_tether_network_connector()->InvokeStartConnectCallbackWithFakeResult();
  EXPECT_NE(GetStatus(), TetherController::Status::kConnecting);

  // Disconnect from a connected state.
  SetTetherNetworkStateConnected();
  Disconnect();
  EXPECT_TRUE(
      fake_tether_network_connector()->DoesPendingDisconnectCallbackExist());
  fake_tether_network_connector()->InvokeDisconnectCallbackWithFakeParams();
  SetTetherNetworkStateDisconnected();

  // Disconnect from a connecting state.
  AttemptConnection();
  EXPECT_EQ(GetStatus(), TetherController::Status::kConnecting);
  Disconnect();
  EXPECT_TRUE(
      fake_tether_network_connector()->DoesPendingDisconnectCallbackExist());
  fake_tether_network_connector()->InvokeDisconnectCallbackWithFakeParams();

  AttemptConnection();
  EXPECT_EQ(GetStatus(), TetherController::Status::kConnecting);
  Disconnect();
  EXPECT_EQ(GetStatus(), TetherController::Status::kConnectionAvailable);
  EXPECT_TRUE(
      fake_tether_network_connector()->DoesPendingDisconnectCallbackExist());
  AttemptConnection();
  EXPECT_EQ(GetStatus(), TetherController::Status::kConnecting);
  fake_tether_network_connector()->InvokeDisconnectCallbackWithFakeParams();
  EXPECT_EQ(GetStatus(), TetherController::Status::kConnecting);

  // Disconnect from a disconnected state.
  RemoveVisibleTetherNetwork();
  AttemptConnection();
  EXPECT_EQ(GetStatus(), TetherController::Status::kConnecting);
  Disconnect();
  EXPECT_FALSE(
      fake_tether_network_connector()->DoesPendingDisconnectCallbackExist());
}

TEST_F(TetherControllerImplTest, AttemptConnectFeatureOffNetworkExists) {
  SetMultideviceSetupFeatureState(FeatureState::kDisabledByUser);
  EXPECT_EQ(GetStatus(), TetherController::Status::kConnectionUnavailable);

  // Test enable flow when a tether device initially exists.
  EnableTetherDevice();
  AddVisibleTetherNetwork();
  AttemptConnection();

  // Should be set connecting even before feature is enabled.
  EXPECT_EQ(GetStatus(), TetherController::Status::kConnecting);

  // Should still be connecting when feature becomes enabled.
  InvokePendingSetFeatureEnabledStateCallback(/*success=*/true);
  EXPECT_EQ(GetStatus(), TetherController::Status::kConnecting);

  // Connecting to tether device.
  SetTetherNetworkStateConnecting();
  EXPECT_EQ(GetStatus(), TetherController::Status::kConnecting);

  // Connected to tether network.
  AddVisibleTetherNetwork();
  SetTetherNetworkStateConnected();
  EXPECT_EQ(GetStatus(), TetherController::Status::kConnected);
  EXPECT_EQ(GetNumObserverStatusChanged(), 3U);

  // Tether network is lost.
  RemoveVisibleTetherNetwork();
  EXPECT_EQ(GetStatus(), TetherController::Status::kConnectionUnavailable);
}

TEST_F(TetherControllerImplTest, AttemptConnectFeatureFailedToEnable) {
  EnableTetherDevice();

  // Test enable flow when feature fails to turn on.
  SetMultideviceSetupFeatureState(FeatureState::kDisabledByUser);
  EXPECT_EQ(GetStatus(), TetherController::Status::kConnectionUnavailable);
  AttemptConnection();

  // Should be set connecting even before feature is enabled.
  EXPECT_EQ(GetStatus(), TetherController::Status::kConnecting);

  // Should fail to connect if feature does not successfully turn on.
  InvokePendingSetFeatureEnabledStateCallback(/*success=*/false);
  EXPECT_EQ(GetStatus(), TetherController::Status::kConnectionUnavailable);

  // Test when feature is enabled externally and visible network is added
  // before callback runs.
  AttemptConnection();

  // Should be set connecting even before feature is enabled.
  EXPECT_EQ(GetStatus(), TetherController::Status::kConnecting);

  // Feature enabled externally
  SetMultideviceSetupFeatureState(FeatureState::kEnabledByUser);
  AddVisibleTetherNetwork();

  EXPECT_EQ(GetStatus(), TetherController::Status::kConnecting);

  // Should fail to connect if feature does not successfully turn on.
  InvokePendingSetFeatureEnabledStateCallback(/*success=*/false);
  EXPECT_EQ(GetStatus(), TetherController::Status::kConnectionAvailable);

  RemoveVisibleTetherNetwork();

  // Test when Mulitdevice suite disabled before callback can return.
  AttemptConnection();

  // Should be set connecting even before feature is enabled.
  EXPECT_EQ(GetStatus(), TetherController::Status::kConnecting);

  // Disable suite externally.
  SetMultideviceSetupFeatureState(FeatureState::kDisabledByUser);
  AddVisibleTetherNetwork();

  EXPECT_EQ(GetStatus(), TetherController::Status::kConnectionUnavailable);
}

TEST_F(TetherControllerImplTest, AttemptConnectFeatureOffNoNetwork) {
  // Test enable flow when a tether device initially does not exist.
  DisconnectTetherDevice();
  SetMultideviceSetupFeatureState(FeatureState::kDisabledByUser);
  EXPECT_EQ(GetStatus(), TetherController::Status::kConnectionUnavailable);
  AttemptConnection();

  // Should be set connecting even before feature is enabled.
  EXPECT_EQ(GetStatus(), TetherController::Status::kConnecting);

  // Should still be connecting when feature becomes enabled.
  InvokePendingSetFeatureEnabledStateCallback(/*success=*/true);
  EXPECT_EQ(GetStatus(), TetherController::Status::kConnecting);

  // Tether is scanning, connection should be connecting still.
  SetTetherScanState(true);
  EXPECT_EQ(GetStatus(), TetherController::Status::kConnecting);
  DisconnectTetherDevice();

  // Tether stops scanning, attempt ends and connection should become
  // unavailable.
  SetTetherScanState(false);
  EXPECT_EQ(GetNumObserverScanFailed(), 1U);
  EXPECT_EQ(GetStatus(), TetherController::Status::kConnectionUnavailable);

  // Tether starts scanning after connection attempt ended.
  SetTetherScanState(true);
  EXPECT_EQ(GetNumObserverScanFailed(), 1U);
  EXPECT_EQ(GetStatus(), TetherController::Status::kConnectionUnavailable);
}

}  // namespace phonehub
}  // namespace ash