chromium/chromeos/ash/components/dbus/shill/fake_shill_service_client.cc

// Copyright 2013 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/dbus/shill/fake_shill_service_client.h"

#include <memory>
#include <optional>
#include <string_view>
#include <utility>

#include "base/containers/contains.h"
#include "base/functional/bind.h"
#include "base/functional/callback_helpers.h"
#include "base/location.h"
#include "base/logging.h"
#include "base/memory/ptr_util.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_util.h"
#include "base/task/single_thread_task_runner.h"
#include "base/task/task_runner.h"
#include "base/time/time.h"
#include "chromeos/ash/components/dbus/shill/shill_device_client.h"
#include "chromeos/ash/components/dbus/shill/shill_manager_client.h"
#include "chromeos/ash/components/dbus/shill/shill_profile_client.h"
#include "chromeos/ash/components/dbus/shill/shill_property_changed_observer.h"
#include "dbus/bus.h"
#include "dbus/message.h"
#include "dbus/object_path.h"
#include "third_party/cros_system_api/dbus/service_constants.h"

namespace ash {

namespace {

void CallSortManagerServices() {
  ShillManagerClient::Get()->GetTestInterface()->SortManagerServices(true);
}

base::TimeDelta GetInteractiveDelay() {
  return ShillManagerClient::Get()->GetTestInterface()->GetInteractiveDelay();
}

// Extracts the hex SSID from shill |service_properties|.
std::string GetHexSSID(const base::Value::Dict& service_properties) {
  const std::string* hex_ssid =
      service_properties.FindString(shill::kWifiHexSsid);
  if (hex_ssid)
    return *hex_ssid;
  const std::string* ssid = service_properties.FindString(shill::kSSIDProperty);
  if (ssid) {
    return base::HexEncode(*ssid);
  }
  return std::string();
}

std::string GetSecurityClass(const base::Value::Dict& service_properties) {
  // Mimics shill's WiFiProvider::GetServiceParametersFromStorage with
  // WiFiService::ComputeSecurityClass.
  const std::string* security_class =
      service_properties.FindString(shill::kSecurityClassProperty);

  if (security_class)
    return *security_class;

  const std::string* security =
      service_properties.FindString(shill::kSecurityProperty);

  if (!security)
    return shill::kSecurityClassNone;

  static const std::array<std::string, 6> psk_securities = {
      shill::kSecurityWpa,  shill::kSecurityWpaWpa2,  shill::kSecurityWpaAll,
      shill::kSecurityWpa2, shill::kSecurityWpa2Wpa3, shill::kSecurityWpa3};
  if (base::Contains(psk_securities, *security))
    return shill::kSecurityClassPsk;

  static const std::array<std::string, 6> eap_securities = {
      shill::kSecurityWpaEnterprise,      shill::kSecurityWpaWpa2Enterprise,
      shill::kSecurityWpaAllEnterprise,   shill::kSecurityWpa2Enterprise,
      shill::kSecurityWpa2Wpa3Enterprise, shill::kSecurityWpa3Enterprise};
  if (base::Contains(eap_securities, *security))
    return shill::kSecurityClass8021x;

  // Neither PSK nor 8021x so it is either "wep" or "none" securities which
  // are the same as their SecurityClass names.
  return *security;
}

// Returns true if both |template_service_properties| and |service_properties|
// have the key |key| and both have the same value for it.
bool HaveSameValueForKey(const base::Value::Dict& template_service_properties,
                         const base::Value::Dict& service_properties,
                         std::string_view key) {
  const base::Value* template_service_value =
      template_service_properties.Find(key);
  const base::Value* service_value = service_properties.Find(key);
  return template_service_value && service_value &&
         *template_service_value == *service_value;
}

// Mimics shill's similar service matching logic. This is only invoked if
// |template_service_properties| and |service_properties| refer to a service of
// the same type.
bool IsSimilarService(const std::string& service_type,
                      const base::Value::Dict& template_service_properties,
                      const base::Value::Dict& service_properties) {
  if (service_type == shill::kTypeWifi) {
    // Mimics shill's WiFiProvider::FindSimilarService.
    return GetHexSSID(template_service_properties) ==
               GetHexSSID(service_properties) &&
           HaveSameValueForKey(template_service_properties, service_properties,
                               shill::kModeProperty) &&
           GetSecurityClass(template_service_properties) ==
               GetSecurityClass(service_properties);
  }

  // Assume that Ethernet / EthernetEAP services are always similar.
  if (service_type == shill::kTypeEthernet ||
      service_type == shill::kTypeEthernetEap) {
    // Mimics shill's EthernetProvider::FindSimilarService.
    return true;
  }

  return false;
}

// Properties that should be retained when a visible service is deleted from a
// profile, i.e. when all its configured properties are removed. This should
// contain properties which can be "observed", e.g. a SSID.
// For simplicity, these are not distinguished by service type.
constexpr const char* kIntrinsicServiceProperties[] = {
    shill::kTypeProperty,
    shill::kDeviceProperty,
    shill::kVisibleProperty,
    shill::kStateProperty,
    shill::kSSIDProperty,
    shill::kWifiHexSsid,
    shill::kSignalStrengthProperty,
    shill::kWifiFrequency,
    shill::kWifiFrequencyListProperty,
    shill::kWifiHexSsid,
    shill::kModeProperty,
    shill::kSecurityProperty,
    shill::kSecurityClassProperty,
    shill::kNetworkTechnologyProperty,
    shill::kNameProperty,
    shill::kProviderProperty};

}  // namespace

FakeShillServiceClient::FakeShillServiceClient() {
  SetDefaultFakeTrafficCounters();
}

FakeShillServiceClient::~FakeShillServiceClient() = default;

// ShillServiceClient overrides.

void FakeShillServiceClient::AddPropertyChangedObserver(
    const dbus::ObjectPath& service_path,
    ShillPropertyChangedObserver* observer) {
  GetObserverList(service_path).AddObserver(observer);
}

void FakeShillServiceClient::RemovePropertyChangedObserver(
    const dbus::ObjectPath& service_path,
    ShillPropertyChangedObserver* observer) {
  GetObserverList(service_path).RemoveObserver(observer);
}

void FakeShillServiceClient::GetProperties(
    const dbus::ObjectPath& service_path,
    chromeos::DBusMethodCallback<base::Value::Dict> callback) {
  std::optional<base::Value::Dict> result_properties;
  const base::Value::Dict* nested_dict =
      GetServiceProperties(service_path.value());
  if (nested_dict) {
    result_properties = nested_dict->Clone();
    // Remove credentials that Shill wouldn't send.
    result_properties->Remove(shill::kPassphraseProperty);
  } else {
    DCHECK(!require_service_to_get_properties_);

    // This may happen if we remove services from the list.
    VLOG(2) << "Properties not found for: " << service_path.value();
  }

  base::OnceClosure property_update =
      base::BindOnce(std::move(callback), std::move(result_properties));
  if (hold_back_service_property_updates_) {
    recorded_property_updates_.push_back(std::move(property_update));
  } else {
    base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
        FROM_HERE, std::move(property_update));
  }
}

void FakeShillServiceClient::SetProperty(const dbus::ObjectPath& service_path,
                                         const std::string& name,
                                         const base::Value& value,
                                         base::OnceClosure callback,
                                         ErrorCallback error_callback) {
  if (!SetServiceProperty(service_path.value(), name, value)) {
    LOG(ERROR) << "Service not found: " << service_path.value();
    std::move(error_callback).Run("Error.InvalidService", "Invalid Service");
    return;
  }
  base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
      FROM_HERE, std::move(callback));
}

void FakeShillServiceClient::SetProperties(const dbus::ObjectPath& service_path,
                                           const base::Value::Dict& properties,
                                           base::OnceClosure callback,
                                           ErrorCallback error_callback) {
  if (set_properties_error_name_.has_value()) {
    std::move(error_callback)
        .Run(set_properties_error_name_.value(),
             /*error_message=*/std::string());
    set_properties_error_name_ = std::nullopt;
    return;
  }
  for (auto iter : properties) {
    if (!SetServiceProperty(service_path.value(), iter.first, iter.second)) {
      LOG(ERROR) << "Service not found: " << service_path.value();
      std::move(error_callback).Run("Error.InvalidService", "Invalid Service");
      return;
    }
  }
  base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
      FROM_HERE, std::move(callback));
}

void FakeShillServiceClient::ClearProperty(const dbus::ObjectPath& service_path,
                                           const std::string& name,
                                           base::OnceClosure callback,
                                           ErrorCallback error_callback) {
  base::Value::Dict* dict = GetModifiableServiceProperties(
      service_path.value(), /*create_if_missing=*/false);
  if (!dict) {
    std::move(error_callback).Run("Error.InvalidService", "Invalid Service");
    return;
  }
  dict->Remove(name);
  // Note: Shill does not send notifications when properties are cleared.
  base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
      FROM_HERE, std::move(callback));
}

void FakeShillServiceClient::ClearProperties(
    const dbus::ObjectPath& service_path,
    const std::vector<std::string>& names,
    ListValueCallback callback,
    ErrorCallback error_callback) {
  base::Value::Dict* dict = GetModifiableServiceProperties(
      service_path.value(), /*create_if_missing=*/false);
  if (!dict) {
    std::move(error_callback).Run("Error.InvalidService", "Invalid Service");
    return;
  }

  base::Value::List result;
  for (const auto& name : names) {
    // Note: Shill does not send notifications when properties are cleared.
    result.Append(dict->Remove(name));
  }
  base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
      FROM_HERE, base::BindOnce(std::move(callback), std::move(result)));
}

void FakeShillServiceClient::Connect(const dbus::ObjectPath& service_path,
                                     base::OnceClosure callback,
                                     ErrorCallback error_callback) {
  VLOG(1) << "FakeShillServiceClient::Connect: " << service_path.value();
  base::Value::Dict* service_properties = GetModifiableServiceProperties(
      service_path.value(), /*create_if_missing=*/false);
  if (!service_properties) {
    LOG(ERROR) << "Service not found: " << service_path.value();
    std::move(error_callback).Run("Error.InvalidService", "Invalid Service");
    return;
  }

  if (connect_error_name_) {
    base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
        FROM_HERE,
        base::BindOnce(std::move(error_callback), *connect_error_name_,
                       /*error_message=*/std::string()));
    connect_error_name_ = std::nullopt;
    return;
  }

  // Set any other services of the same Type to 'offline' first, before setting
  // State to Association which will trigger sorting Manager.Services and
  // sending an update.
  SetOtherServicesOffline(service_path.value());

  // Clear Error.
  service_properties->Set(shill::kErrorProperty, "");

  // Set Associating.
  base::Value associating_value(shill::kStateAssociation);
  SetServiceProperty(service_path.value(), shill::kStateProperty,
                     associating_value);

  // Stay Associating until the state is changed again after a delay.
  base::SingleThreadTaskRunner::GetCurrentDefault()->PostDelayedTask(
      FROM_HERE,
      base::BindOnce(&FakeShillServiceClient::ContinueConnect,
                     weak_ptr_factory_.GetWeakPtr(), service_path.value()),
      GetInteractiveDelay());

  base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
      FROM_HERE, std::move(callback));
}

void FakeShillServiceClient::Disconnect(const dbus::ObjectPath& service_path,
                                        base::OnceClosure callback,
                                        ErrorCallback error_callback) {
  const base::Value::Dict* service = GetServiceProperties(service_path.value());
  if (!service) {
    std::move(error_callback).Run("Error.InvalidService", "Invalid Service");
    return;
  }
  // Set Idle after a delay
  base::SingleThreadTaskRunner::GetCurrentDefault()->PostDelayedTask(
      FROM_HERE,
      base::BindOnce(&FakeShillServiceClient::SetProperty,
                     weak_ptr_factory_.GetWeakPtr(), service_path,
                     shill::kStateProperty, base::Value(shill::kStateIdle),
                     std::move(callback), std::move(error_callback)),
      GetInteractiveDelay());
}

void FakeShillServiceClient::Remove(const dbus::ObjectPath& service_path,
                                    base::OnceClosure callback,
                                    ErrorCallback error_callback) {
  base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
      FROM_HERE, std::move(callback));
}

void FakeShillServiceClient::CompleteCellularActivation(
    const dbus::ObjectPath& service_path,
    base::OnceClosure callback,
    ErrorCallback error_callback) {
  base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
      FROM_HERE, std::move(callback));
}

void FakeShillServiceClient::GetLoadableProfileEntries(
    const dbus::ObjectPath& service_path,
    chromeos::DBusMethodCallback<base::Value::Dict> callback) {
  ShillProfileClient::TestInterface* profile_client =
      ShillProfileClient::Get()->GetTestInterface();
  std::vector<std::string> profiles;
  profile_client->GetProfilePathsContainingService(service_path.value(),
                                                   &profiles);

  DCHECK(profiles.size()) << "No profiles contain given service";
  // Provide a dictionary with {profile_path: service_path} entries for
  // profile_paths that contain the service.
  base::Value::Dict result_properties;
  for (const auto& profile : profiles) {
    result_properties.Set(profile, service_path.value());
  }

  base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
      FROM_HERE,
      base::BindOnce(std::move(callback), std::move(result_properties)));
}

void FakeShillServiceClient::GetWiFiPassphrase(
    const dbus::ObjectPath& service_path,
    StringCallback callback,
    ErrorCallback error_callback) {
  base::Value::Dict* service_properties =
      GetModifiableServiceProperties(service_path.value(), false);
  if (!service_properties) {
    LOG(ERROR) << "Service not found: " << service_path.value();
    std::move(error_callback).Run("Error.InvalidService", "Invalid Service");
    return;
  }

  const std::string* passphrase =
      service_properties->FindString(shill::kPassphraseProperty);
  std::move(callback).Run(passphrase ? *passphrase : std::string());
}

void FakeShillServiceClient::GetEapPassphrase(
    const dbus::ObjectPath& service_path,
    StringCallback callback,
    ErrorCallback error_callback) {
  base::Value::Dict* service_properties =
      GetModifiableServiceProperties(service_path.value(), false);
  if (!service_properties) {
    LOG(ERROR) << "Service not found: " << service_path.value();
    std::move(error_callback).Run("Error.InvalidService", "Invalid Service");
    return;
  }

  const std::string* passphrase =
      service_properties->FindString(shill::kEapPasswordProperty);
  std::move(callback).Run(passphrase ? *passphrase : std::string());
}

void FakeShillServiceClient::RequestPortalDetection(
    const dbus::ObjectPath& service_path,
    chromeos::VoidDBusMethodCallback callback) {
  if (request_portal_state_) {
    SetServiceProperty(service_path.value(), shill::kStateProperty,
                       base::Value(*request_portal_state_));
    request_portal_state_ = std::nullopt;
  }
  std::move(callback).Run(/*success=*/true);
}

void FakeShillServiceClient::RequestTrafficCounters(
    const dbus::ObjectPath& service_path,
    chromeos::DBusMethodCallback<base::Value> callback) {
  std::move(callback).Run(base::Value(fake_traffic_counters_.Clone()));
}

void FakeShillServiceClient::ResetTrafficCounters(
    const dbus::ObjectPath& service_path,
    base::OnceClosure callback,
    ErrorCallback error_callback) {
  fake_traffic_counters_.clear();
  base::Time reset_time =
      !time_getter_.is_null() ? time_getter_.Run() : base::Time::Now();
  SetServiceProperty(
      service_path.value(), shill::kTrafficCounterResetTimeProperty,
      base::Value(reset_time.ToDeltaSinceWindowsEpoch().InMillisecondsF()));
  base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
      FROM_HERE, std::move(callback));
}

ShillServiceClient::TestInterface* FakeShillServiceClient::GetTestInterface() {
  return this;
}

// ShillServiceClient::TestInterface overrides.
void FakeShillServiceClient::AddService(const std::string& service_path,
                                        const std::string& guid,
                                        const std::string& name,
                                        const std::string& type,
                                        const std::string& state,
                                        bool visible) {
  AddServiceWithIPConfig(service_path, guid, name, type, state,
                         std::string() /* ipconfig_path */, visible);
}

void FakeShillServiceClient::AddServiceWithIPConfig(
    const std::string& service_path,
    const std::string& guid,
    const std::string& name,
    const std::string& type,
    const std::string& state,
    const std::string& ipconfig_path,
    bool visible) {
  base::Value::Dict* properties =
      SetServiceProperties(service_path, guid, name, type, state, visible);

  if (!ipconfig_path.empty())
    properties->Set(shill::kIPConfigProperty, ipconfig_path);

  ShillManagerClient::Get()->GetTestInterface()->AddManagerService(service_path,
                                                                   true);
}

base::Value::Dict* FakeShillServiceClient::SetServiceProperties(
    const std::string& service_path,
    const std::string& guid,
    const std::string& name,
    const std::string& type,
    const std::string& state,
    bool visible) {
  base::Value::Dict* properties =
      GetModifiableServiceProperties(service_path, true);
  connect_behavior_.erase(service_path);

  // If |guid| is provided, set Service.GUID to that. Otherwise if a GUID is
  // stored in a profile entry, use that. Otherwise leave it blank. Shill does
  // not enforce a valid guid, we do that at the NetworkStateHandler layer.
  std::string guid_to_set = guid;
  if (guid_to_set.empty()) {
    std::string profile_path;
    std::optional<base::Value::Dict> profile_properties =
        ShillProfileClient::Get()->GetTestInterface()->GetService(
            service_path, &profile_path);
    if (profile_properties) {
      const std::string* profile_guid =
          profile_properties->FindString(shill::kGuidProperty);
      if (profile_guid) {
        guid_to_set = *profile_guid;
      }
    }
  }
  if (!guid_to_set.empty()) {
    properties->Set(shill::kGuidProperty, guid_to_set);
  }
  if (type == shill::kTypeWifi) {
    properties->Set(shill::kSSIDProperty, name);
    properties->Set(shill::kWifiHexSsid, base::HexEncode(name));
  }
  properties->Set(shill::kNameProperty, name);
  std::string device_path =
      ShillDeviceClient::Get()->GetTestInterface()->GetDevicePathForType(type);
  properties->Set(shill::kDeviceProperty, device_path);
  properties->Set(shill::kTypeProperty, type);
  properties->Set(shill::kStateProperty, state);
  properties->Set(shill::kVisibleProperty, visible);
  if (type == shill::kTypeWifi) {
    properties->Set(shill::kSecurityClassProperty, shill::kSecurityClassNone);
    properties->Set(shill::kModeProperty, shill::kModeManaged);
  }

  // Ethernet is always connectable.
  if (type == shill::kTypeEthernet)
    properties->Set(shill::kConnectableProperty, true);

  // Cellular is always metered.
  if (type == shill::kTypeCellular) {
    properties->Set(shill::kMeteredProperty, true);
    properties->Set(shill::kTrafficCounterResetTimeProperty, 0.0);
  }

  return properties;
}

void FakeShillServiceClient::RemoveService(const std::string& service_path) {
  stub_services_.Remove(service_path);
  connect_behavior_.erase(service_path);
  ShillManagerClient::Get()->GetTestInterface()->RemoveManagerService(
      service_path);
}

bool FakeShillServiceClient::SetServiceProperty(const std::string& service_path,
                                                const std::string& property,
                                                const base::Value& value) {
  base::Value::Dict* dict =
      GetModifiableServiceProperties(service_path, /*create_if_missing=*/false);
  if (!dict)
    return false;

  VLOG(1) << "Service.SetProperty: " << property << " = " << value
          << " For: " << service_path;

  std::string changed_property;
  base::CompareCase case_sensitive = base::CompareCase::SENSITIVE;
  if (base::StartsWith(property, "Provider.", case_sensitive) ||
      base::StartsWith(property, "OpenVPN.", case_sensitive) ||
      base::StartsWith(property, "L2TPIPsec.", case_sensitive)) {
    // These properties are only nested within the Provider dictionary if read
    // from Shill. Properties that start with "Provider" need to have that
    // stripped off, other properties are nested in the "Provider" dictionary
    // as-is.
    std::string key = property;
    if (base::StartsWith(property, "Provider.", case_sensitive))
      key = property.substr(strlen("Provider."));
    base::Value::Dict* provider = dict->EnsureDict(shill::kProviderProperty);
    provider->Set(key, value.Clone());
    changed_property = shill::kProviderProperty;
  } else {
    dict->Set(property, value.Clone());
    changed_property = property;
  }

  if (changed_property == shill::kPassphraseProperty ||
      changed_property == shill::kSecurityClassProperty) {
    const std::string* passphrase =
        dict->FindString(shill::kPassphraseProperty);
    const std::string* security =
        dict->FindString(shill::kSecurityClassProperty);
    // Make PSK networks connectable if 'Passphrase' is set.
    if (passphrase && !passphrase->empty()) {
      dict->Set(shill::kPassphraseRequiredProperty, false);
      if (security && *security == shill::kSecurityClassPsk) {
        dict->Set(shill::kConnectableProperty, true);
      }
    }
    // Make open networks always connectable.
    if (security && *security == shill::kSecurityClassNone) {
      dict->Set(shill::kConnectableProperty, true);
    }
  }

  // Add or update the profile entry.
  ShillProfileClient::TestInterface* profile_test =
      ShillProfileClient::Get()->GetTestInterface();
  if (property == shill::kProfileProperty) {
    const std::string* profile_path = value.GetIfString();
    if (profile_path) {
      if (!profile_path->empty())
        profile_test->AddService(*profile_path, service_path);
    } else {
      LOG(ERROR) << "Profile value is not a String!";
    }
  } else {
    const std::string* profile_path = dict->FindString(shill::kProfileProperty);
    if (profile_path && !profile_path->empty())
      profile_test->UpdateService(*profile_path, service_path);
  }

  if (property == shill::kCellularCustomApnListProperty) {
    const std::string* type = dict->FindString(shill::kTypeProperty);
    CHECK(type && *type == shill::kTypeCellular);
    bool deafult_apn_found = false;
    // Set connected APN to the latest custom default type APN to simulate the
    // behavior in Shill. When no default type custom APNs are found, the
    // default Modb APN should be used for connection.
    for (const auto& custom_apn : value.GetList()) {
      CHECK(custom_apn.is_dict());
      const std::string* apn_types =
          custom_apn.GetDict().FindString(shill::kApnTypesProperty);
      if (!apn_types || *apn_types == shill::kApnTypeIA) {
        continue;
      }
      dict->Set(shill::kCellularLastGoodApnProperty, custom_apn.Clone());
      deafult_apn_found = true;
      break;
    }
    if (!deafult_apn_found) {
      dict->Set(shill::kCellularLastGoodApnProperty,
                GetFakeDefaultModbApnDict());
    }
  }

  // Notify the Manager if the state changed (affects DefaultService).
  if (property == shill::kStateProperty) {
    ShillManagerClient::Get()->GetTestInterface()->ServiceStateChanged(
        service_path, value.is_string() ? value.GetString() : std::string());
  }

  // If the State or Visibility changes, the sort order of service lists may
  // change and the DefaultService property may change.
  if (property == shill::kStateProperty ||
      property == shill::kVisibleProperty) {
    base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
        FROM_HERE, base::BindOnce(&CallSortManagerServices));
  }

  // Notify Chrome of the property change.
  base::OnceClosure property_update =
      base::BindOnce(&FakeShillServiceClient::NotifyObserversPropertyChanged,
                     weak_ptr_factory_.GetWeakPtr(),
                     dbus::ObjectPath(service_path), changed_property);
  if (hold_back_service_property_updates_)
    recorded_property_updates_.push_back(std::move(property_update));
  else
    base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
        FROM_HERE, std::move(property_update));
  return true;
}

const base::Value::Dict* FakeShillServiceClient::GetServiceProperties(
    const std::string& service_path) const {
  return stub_services_.FindDict(service_path);
}

bool FakeShillServiceClient::ClearConfiguredServiceProperties(
    const std::string& service_path) {
  base::Value::Dict* service_dict = GetModifiableServiceProperties(
      service_path, false /* create_if_missing */);
  if (!service_dict)
    return false;

  const std::optional<bool> visible_property =
      service_dict->FindBool(shill::kVisibleProperty);
  const std::string* service_type =
      service_dict->FindString(shill::kTypeProperty);
  if (!visible_property || !visible_property.value() || !service_type ||
      (*service_type == shill::kTypeVPN) ||
      (*service_type == shill::kTypeCellular)) {
    stub_services_.Remove(service_path);
    RemoveService(service_path);
    return true;
  }

  base::Value::Dict properties_after_delete_entry;

  // Explicitly clear the profile property using SetServiceProperty so a
  // notification is sent about that.
  SetServiceProperty(service_path, shill::kProfileProperty,
                     base::Value(std::string()));
  properties_after_delete_entry.Set(shill::kProfileProperty, std::string());

  for (const std::string& property_to_retain : kIntrinsicServiceProperties) {
    const base::Value* value = service_dict->Find(property_to_retain);
    if (!value)
      continue;
    properties_after_delete_entry.Set(property_to_retain, value->Clone());
  }
  stub_services_.Set(service_path, std::move(properties_after_delete_entry));
  return true;
}

base::Value::Dict FakeShillServiceClient::GetFakeDefaultModbApnDict() {
  return base::Value::Dict()
      .Set(shill::kApnProperty, "default_apn")
      .Set(shill::kApnNameProperty, "default_apn")
      .Set(shill::kApnLocalizedNameProperty, "localized test apn")
      .Set(shill::kApnUsernameProperty, "user name")
      .Set(shill::kApnPasswordProperty, "password")
      .Set(shill::kApnAuthenticationProperty, "chap")
      .Set(shill::kApnTypesProperty, shill::kApnTypeDefault)
      .Set(shill::kApnSourceProperty, shill::kApnSourceMoDb);
}

std::string FakeShillServiceClient::FindServiceMatchingGUID(
    const std::string& guid) {
  for (const auto [service_path, service_properties] : stub_services_) {
    const std::string* service_guid =
        service_properties.GetDict().FindString(shill::kGuidProperty);
    if (service_guid && *service_guid == guid)
      return service_path;
  }

  return std::string();
}

std::string FakeShillServiceClient::FindServiceMatchingName(
    const std::string& name) {
  for (const auto [service_path, service_properties] : stub_services_) {
    const std::string* service_name =
        service_properties.GetDict().FindString(shill::kNameProperty);
    if (service_name && *service_name == name) {
      return service_path;
    }
  }

  return std::string();
}

std::string FakeShillServiceClient::FindSimilarService(
    const base::Value::Dict& template_service_properties) {
  const std::string* template_type =
      template_service_properties.FindString(shill::kTypeProperty);
  if (!template_type)
    return std::string();

  for (const auto service_pair : stub_services_) {
    const auto& service_path = service_pair.first;
    const base::Value::Dict& service_properties = service_pair.second.GetDict();

    const std::string* service_type =
        service_properties.FindString(shill::kTypeProperty);
    if (!service_type || *service_type != *template_type)
      continue;

    if (IsSimilarService(*service_type, template_service_properties,
                         service_properties)) {
      return service_path;
    }
  }

  return std::string();
}

void FakeShillServiceClient::ClearServices() {
  ShillManagerClient::Get()->GetTestInterface()->ClearManagerServices();
  stub_services_.clear();
  connect_behavior_.clear();
}

void FakeShillServiceClient::SetConnectBehavior(
    const std::string& service_path,
    const base::RepeatingClosure& behavior) {
  if (!behavior) {
    connect_behavior_.erase(service_path);
    return;
  }
  connect_behavior_[service_path] = behavior;
}

void FakeShillServiceClient::SetErrorForNextConnectionAttempt(
    const std::string& error_name) {
  connect_error_name_ = error_name;
}

void FakeShillServiceClient::SetErrorForNextSetPropertiesAttempt(
    const std::string& error_name) {
  set_properties_error_name_ = error_name;
}

void FakeShillServiceClient::SetRequestPortalState(const std::string& state) {
  request_portal_state_ = state;
}

void FakeShillServiceClient::SetHoldBackServicePropertyUpdates(bool hold_back) {
  hold_back_service_property_updates_ = hold_back;
  std::vector<base::OnceClosure> property_updates;
  recorded_property_updates_.swap(property_updates);

  if (hold_back_service_property_updates_)
    return;

  for (auto& property_update : property_updates)
    base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
        FROM_HERE, std::move(property_update));
}

void FakeShillServiceClient::SetRequireServiceToGetProperties(
    bool require_service_to_get_properties) {
  require_service_to_get_properties_ = require_service_to_get_properties;
}

void FakeShillServiceClient::NotifyObserversPropertyChanged(
    const dbus::ObjectPath& service_path,
    const std::string& property) {
  std::string path = service_path.value();
  const base::Value::Dict* dict = GetServiceProperties(path);
  if (!dict) {
    LOG(ERROR) << "Notify for unknown service: " << path;
    return;
  }
  const base::Value* value = dict->Find(property);
  if (!value) {
    LOG(ERROR) << "Notify for unknown property: " << path << " : " << property;
    return;
  }
  for (auto& observer : GetObserverList(service_path))
    observer.OnPropertyChanged(property, *value);
}

base::Value::Dict* FakeShillServiceClient::GetModifiableServiceProperties(
    const std::string& service_path,
    bool create_if_missing) {
  base::Value::Dict* properties = stub_services_.FindDict(service_path);
  if (!properties && create_if_missing) {
    properties = stub_services_.EnsureDict(service_path);
  }
  return properties;
}

FakeShillServiceClient::PropertyObserverList&
FakeShillServiceClient::GetObserverList(const dbus::ObjectPath& device_path) {
  auto iter = observer_list_.find(device_path);
  if (iter != observer_list_.end())
    return *(iter->second);
  PropertyObserverList* observer_list = new PropertyObserverList();
  observer_list_[device_path] = base::WrapUnique(observer_list);
  return *observer_list;
}

void FakeShillServiceClient::SetOtherServicesOffline(
    const std::string& service_path) {
  const base::Value::Dict* service_properties =
      GetServiceProperties(service_path);
  if (!service_properties) {
    LOG(ERROR) << "Missing service: " << service_path;
    return;
  }
  const std::string* service_type =
      service_properties->FindString(shill::kTypeProperty);
  if (!service_type)
    return;

  // Set all other services of the same type to offline (Idle).
  for (auto iter : stub_services_) {
    const std::string& path = iter.first;
    if (path == service_path)
      continue;
    base::Value& properties = iter.second;
    const std::string* type =
        properties.GetDict().FindString(shill::kTypeProperty);
    if (!type || *type != *service_type)
      continue;

    properties.GetDict().Set(shill::kStateProperty, shill::kStateIdle);
  }
}

void FakeShillServiceClient::SetCellularActivated(
    const dbus::ObjectPath& service_path,
    ErrorCallback error_callback) {
  SetProperty(service_path, shill::kActivationStateProperty,
              base::Value(shill::kActivationStateActivated), base::DoNothing(),
              std::move(error_callback));
  SetProperty(service_path, shill::kConnectableProperty, base::Value(true),
              base::DoNothing(), std::move(error_callback));
}

void FakeShillServiceClient::ContinueConnect(const std::string& service_path) {
  VLOG(1) << "FakeShillServiceClient::ContinueConnect: " << service_path;
  const base::Value::Dict* service_properties =
      GetServiceProperties(service_path);
  if (!service_properties) {
    LOG(ERROR) << "Service not found: " << service_path;
    return;
  }

  if (base::Contains(connect_behavior_, service_path)) {
    const base::RepeatingClosure& custom_connect_behavior =
        connect_behavior_[service_path];
    VLOG(1) << "Running custom connect behavior for " << service_path;
    custom_connect_behavior.Run();
    return;
  }

  // No custom connect behavior set, continue with the default connect behavior.
  const std::string* passphrase =
      service_properties->FindString(shill::kPassphraseProperty);
  if (passphrase && *passphrase == "failure") {
    // Simulate a password failure.
    SetServiceProperty(service_path, shill::kErrorProperty,
                       base::Value(shill::kErrorBadPassphrase));
    SetServiceProperty(service_path, shill::kStateProperty,
                       base::Value(shill::kStateFailure));
    base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
        FROM_HERE,
        base::BindOnce(
            base::IgnoreResult(&FakeShillServiceClient::SetServiceProperty),
            weak_ptr_factory_.GetWeakPtr(), service_path, shill::kErrorProperty,
            base::Value(shill::kErrorBadPassphrase)));
    return;
  }

  // Set other services of the same type that were previously not "idle" to
  // "idle".
  const std::string* service_type =
      service_properties->FindString(shill::kTypeProperty);
  for (const auto service_pair : stub_services_) {
    const auto& other_service_path = service_pair.first;
    if (other_service_path == service_path) {
      continue;
    }

    const base::Value::Dict& other_service_properties =
        service_pair.second.GetDict();
    const std::string* other_service_type =
        other_service_properties.FindString(shill::kTypeProperty);
    if (!service_type || !other_service_type ||
        *other_service_type != *service_type) {
      continue;
    }
    const std::string* other_service_state =
        other_service_properties.FindString(shill::kStateProperty);
    if (!other_service_state || *other_service_type == shill::kStateIdle) {
      continue;
    }
    SetServiceProperty(other_service_path, shill::kStateProperty,
                       base::Value(shill::kStateIdle));
  }

  // Set the connected service to "online".
  VLOG(1) << "Setting state to Online " << service_path;
  SetServiceProperty(service_path, shill::kStateProperty,
                     base::Value(shill::kStateOnline));
}

void FakeShillServiceClient::SetDefaultFakeTrafficCounters() {
  base::Value::List traffic_counters;

  base::Value::Dict chrome_dict;
  chrome_dict.Set("source", shill::kTrafficCounterSourceChrome);
  chrome_dict.Set("rx_bytes", 1000);
  chrome_dict.Set("tx_bytes", 2000.5);
  traffic_counters.Append(std::move(chrome_dict));

  base::Value::Dict user_dict;
  user_dict.Set("source", shill::kTrafficCounterSourceUser);
  user_dict.Set("rx_bytes", 45);
  user_dict.Set("tx_bytes", 55);
  traffic_counters.Append(std::move(user_dict));

  SetFakeTrafficCounters(std::move(traffic_counters));
}

void FakeShillServiceClient::SetFakeTrafficCounters(
    base::Value::List fake_traffic_counters) {
  fake_traffic_counters_ = std::move(fake_traffic_counters);
}

void FakeShillServiceClient::SetTimeGetterForTest(
    base::RepeatingCallback<base::Time()> time_getter) {
  time_getter_ = std::move(time_getter);
}

}  // namespace ash