You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1041 lines
46 KiB
C++

// SPDX-FileCopyrightText: 2026 Reion Wong <reionwong@gmail.com>
// SPDX-License-Identifier: GPL-3.0-or-later
#include <gtest/gtest.h>
#include <chrono>
#include <cstdlib>
#include <endian.h>
#include <filesystem>
#include <fstream>
#include <functional>
#include <memory>
#include <optional>
#include <time.h>
#include <unistd.h>
#include "base/event_loop.h"
#include "base/timer.h"
#include "fakenetworkmanager.h"
#include "networkmanager/client.h"
#include "networkmanager/connections.h"
#include "networkmanager/handler.h"
#include "networkmanager/hotspot.h"
#include "networkmanager/networkmodel.h"
#include "networkmanager/vpnutils.h"
using namespace std::chrono_literals;
using namespace cutefish::networkmanager;
using namespace cutefish::networkmanager::variants;
using cutefish::base::EventLoop;
using cutefish::base::Timer;
using fakenm::FakeNetworkManager;
static EventLoop* loop = nullptr;
// Runs the loop until the condition holds, or a few seconds have passed.
static bool waitFor(const std::function<bool()>& condition, std::chrono::milliseconds timeout = 3000ms)
{
if (condition())
return true;
bool met = false;
Timer poll;
poll.onTimeout = [&] {
if (condition()) {
met = true;
loop->quit();
}
};
poll.start(2ms);
Timer limit;
limit.setSingleShot(true);
limit.onTimeout = [&] { loop->quit(); };
limit.start(timeout);
loop->run();
return met || condition();
}
// Lets what is under way settle.
static void wait(std::chrono::milliseconds time)
{
waitFor([] { return false; }, time);
}
static const std::string wiredDevice = fakenm::managerPath + "/Devices/1";
static const std::string wifiDevice = fakenm::managerPath + "/Devices/2";
static const std::string wiredProfile = fakenm::managerPath + "/Settings/1";
static const std::string homeProfile = fakenm::managerPath + "/Settings/2";
static const std::string wiredActive = fakenm::managerPath + "/ActiveConnection/1";
static const std::string ip4Config = fakenm::managerPath + "/IP4Config/1";
static const std::string homeStrong = fakenm::managerPath + "/AccessPoint/1";
static const std::string homeWeak = fakenm::managerPath + "/AccessPoint/2";
static const std::string cafe = fakenm::managerPath + "/AccessPoint/3";
static const std::string hidden = fakenm::managerPath + "/AccessPoint/4";
static const std::string library = fakenm::managerPath + "/AccessPoint/5";
static const std::string office = fakenm::managerPath + "/AccessPoint/6";
static const std::string homeActive = fakenm::managerPath + "/ActiveConnection/2";
static const std::string officeVpn = fakenm::managerPath + "/Settings/30";
static const std::string branchVpn = fakenm::managerPath + "/Settings/31";
static const std::string homeVpn = fakenm::managerPath + "/Settings/32";
static const std::string vpnInterface = "org.freedesktop.NetworkManager.VPN.Connection";
// NM_802_11_AP_SEC_*: CCMP pairwise and group ciphers with PSK or 802.1X key management.
static constexpr uint32_t rsnPsk = 0x8 | 0x80 | 0x100;
static constexpr uint32_t rsnEap = 0x8 | 0x80 | 0x200;
// WEP40, WEP104, TKIP, CCMP, WPA, RSN and AP.
static constexpr uint32_t wifiCapabilities = 0x1 | 0x2 | 0x4 | 0x8 | 0x10 | 0x20 | 0x40;
using Variant = cutefish::base::dbus::Variant;
using VariantMap = cutefish::base::dbus::VariantMap;
static Variant addressData(const std::string& address)
{
return Variant(std::vector<VariantMap>{{{"address", Variant(address)}, {"prefix", Variant(uint32_t(24))}}});
}
static fakenm::Interfaces accessPoint(const std::string& ssid, uint32_t flags, uint32_t rsn, uint8_t strength)
{
static int address = 0;
char hw[32];
snprintf(hw, sizeof(hw), "10:00:00:00:00:%02d", ++address);
return {{fakenm::accessPoint,
{{"Ssid", fakenm::bytes(ssid)},
{"Flags", Variant(flags)},
{"WpaFlags", Variant(uint32_t(0))},
{"RsnFlags", Variant(rsn)},
{"Strength", Variant(strength)},
{"Mode", Variant(uint32_t(2))},
{"HwAddress", Variant(std::string(hw))},
{"Frequency", Variant(uint32_t(2412))}}}};
}
// One Ethernet device with its profile up, and one idle Wi-Fi device in range
// of a saved network on two access points, an open one, an enterprise one, a
// WPA2 one and a hidden one.
static void populate(FakeNetworkManager& nm)
{
nm.settings[wiredProfile] = {
{"connection",
{{"id", Variant(std::string("Wired connection 1"))},
{"uuid", Variant(std::string("u-wired"))},
{"type", Variant(std::string("802-3-ethernet"))},
{"timestamp", Variant(uint64_t(1000))}}},
{"ipv4", {{"method", Variant(std::string("auto"))}}},
};
nm.add(wiredProfile, {{fakenm::connection, {{"Flags", Variant(uint32_t(0))}}}}, false);
nm.settings[homeProfile] = {
{"connection",
{{"id", Variant(std::string("Home"))},
{"uuid", Variant(std::string("u-home"))},
{"type", Variant(std::string("802-11-wireless"))},
{"permissions", Variant(std::vector<std::string>{"user:alice:"})},
{"timestamp", Variant(uint64_t(2000))}}},
{"802-11-wireless", {{"ssid", fakenm::bytes("Home")}, {"mode", Variant(std::string("infrastructure"))}}},
{"802-11-wireless-security", {{"key-mgmt", Variant(std::string("wpa-psk"))}, {"psk-flags", Variant(uint32_t(1))}}},
{"ipv4", {{"method", Variant(std::string("manual"))}, {"address-data", addressData("10.0.0.2")}}},
};
nm.add(homeProfile, {{fakenm::connection, {{"Flags", Variant(uint32_t(0))}}}}, false);
nm.add(wiredActive,
{{fakenm::active,
{{"Connection", fakenm::path(wiredProfile)},
{"Devices", fakenm::paths({wiredDevice})},
{"State", Variant(uint32_t(2))},
{"Type", Variant(std::string("802-3-ethernet"))},
{"Vpn", Variant(false)},
{"Id", Variant(std::string("Wired connection 1"))}}}},
false);
nm.add(ip4Config,
{{"org.freedesktop.NetworkManager.IP4Config",
{{"AddressData", addressData("192.168.1.5")},
{"Gateway", Variant(std::string("192.168.1.1"))},
{"NameserverData", addressData("192.168.1.1")}}}},
false);
nm.add(wiredDevice,
{{fakenm::device,
{{"DeviceType", Variant(uint32_t(1))},
{"Interface", Variant(std::string("eth0"))},
{"State", Variant(uint32_t(100))},
{"Managed", Variant(true)},
{"Autoconnect", Variant(true)},
{"HwAddress", Variant(std::string("02:00:00:00:00:01"))},
{"ActiveConnection", fakenm::path(wiredActive)},
{"AvailableConnections", fakenm::paths({wiredProfile})},
{"Ip4Config", fakenm::path(ip4Config)},
{"Ip6Config", fakenm::path({})}}}},
false);
nm.add(homeStrong, accessPoint("Home", 0x1, rsnPsk, 70), false);
nm.add(homeWeak, accessPoint("Home", 0x1, rsnPsk, 40), false);
nm.add(cafe, accessPoint("Cafe", 0x0, 0, 50), false);
nm.add(hidden, accessPoint({}, 0x1, rsnPsk, 60), false);
nm.add(library, accessPoint("Library", 0x1, rsnEap, 30), false);
nm.add(office, accessPoint("Office", 0x1, rsnPsk, 20), false);
nm.add(wifiDevice,
{{fakenm::device,
{{"DeviceType", Variant(uint32_t(2))},
{"Interface", Variant(std::string("wlan0"))},
{"State", Variant(uint32_t(30))},
{"Managed", Variant(true)},
{"Autoconnect", Variant(true)},
{"HwAddress", Variant(std::string("02:00:00:00:00:02"))},
{"ActiveConnection", fakenm::path({})},
{"AvailableConnections", fakenm::paths({homeProfile})},
{"Ip4Config", fakenm::path({})},
{"Ip6Config", fakenm::path({})}}},
{fakenm::wireless,
{{"WirelessCapabilities", Variant(wifiCapabilities)},
{"Mode", Variant(uint32_t(2))},
{"ActiveAccessPoint", fakenm::path({})},
{"AccessPoints", fakenm::paths({homeStrong, homeWeak, cafe, hidden, library, office})}}}},
false);
}
static int64_t bootTimeMs()
{
timespec now;
clock_gettime(CLOCK_BOOTTIME, &now);
return int64_t(now.tv_sec) * 1000 + now.tv_nsec / 1000000;
}
static std::optional<NetworkItem> row(const NetworkModel& model, const std::string& name)
{
for (const NetworkItem& item : model.items()) {
if (item.name == name)
return item;
}
return std::nullopt;
}
namespace {
struct ClientTest : testing::Test
{
void SetUp() override
{
server = cutefish::base::dbus::Connection::open(cutefish::base::dbus::Connection::Bus::Session);
clientBus = cutefish::base::dbus::Connection::open(cutefish::base::dbus::Connection::Bus::Session);
ASSERT_TRUE(server && clientBus);
nm = std::make_unique<FakeNetworkManager>(server.get());
populate(*nm);
ASSERT_TRUE(nm->serve());
Client::setInstanceBus(clientBus.get());
ASSERT_TRUE(waitFor([] { return Client::instance()->ready(); }));
EXPECT_EQ(Client::instance()->owner(), server->uniqueName());
}
void TearDown() override
{
nm->stop();
waitFor([] { return !Client::instance()->ready(); });
Client::setInstanceBus(nullptr);
nm.reset();
server.reset();
clientBus.reset();
}
// The device's last scan ended `ago` ms back and saw every access point it lists.
void scannedAgo(int64_t ago, int64_t offset = 0)
{
const int64_t lastScan = bootTimeMs() - ago + offset;
for (const std::string& ap : Client::instance()->accessPoints(wifiDevice))
nm->set(ap, fakenm::accessPoint, "LastSeen", Variant(int32_t(lastScan / 1000)));
nm->set(wifiDevice, fakenm::wireless, "LastScan", Variant(int64_t(lastScan)));
ASSERT_TRUE(waitFor([&] {
return toInt64(Client::instance()->property(wifiDevice, fakenm::wireless, "LastScan")) == lastScan;
}));
}
void dropAccessPoint(const std::string& ap)
{
std::vector<std::string> listed = Client::instance()->accessPoints(wifiDevice);
std::erase(listed, ap);
nm->set(wifiDevice, fakenm::wireless, "AccessPoints", fakenm::paths(listed));
nm->remove(ap);
ASSERT_TRUE(waitFor([&] { return !Client::instance()->has(ap, fakenm::accessPoint); }));
}
static std::string otherActive()
{
std::string found;
for (const std::string& path : Client::instance()->activeConnections()) {
if (path != wiredActive)
found = path;
}
return found;
}
void addVpn(const std::string& path, const std::string& name, const std::string& service, uint64_t timestamp)
{
std::string uuid = name;
for (char& c : uuid)
c = char(std::tolower(static_cast<unsigned char>(c)));
nm->settings[path] = {
{"connection",
{{"id", Variant(name)},
{"uuid", Variant(uuid)},
{"type", Variant(std::string("vpn"))},
{"timestamp", Variant(timestamp)}}},
{"vpn",
{{"service-type", Variant(service)},
{"data", Variant(std::map<std::string, std::string>{{"connection-type", "password"}})}}},
};
nm->add(path, {{fakenm::connection, {{"Flags", Variant(uint32_t(0))}}}});
}
void addWireGuard(const std::string& path, const std::string& name, uint64_t timestamp)
{
nm->settings[path] = {
{"connection",
{{"id", Variant(name)},
{"uuid", Variant(std::string("home wg"))},
{"type", Variant(std::string("wireguard"))},
{"timestamp", Variant(timestamp)}}},
{"wireguard", {{"listen-port", Variant(uint32_t(0))}}},
};
nm->add(path, {{fakenm::connection, {{"Flags", Variant(uint32_t(0))}}}});
}
std::unique_ptr<cutefish::base::dbus::Connection> server;
std::unique_ptr<cutefish::base::dbus::Connection> clientBus;
std::unique_ptr<FakeNetworkManager> nm;
};
} // namespace
static Settings settingsOf(const fakenm::Call& call)
{
return call.arguments.empty() ? Settings() : call.arguments[0].get<Settings>().value_or(Settings());
}
static std::string pathArgument(const fakenm::Call& call, size_t index)
{
return index < call.arguments.size() ? toString(call.arguments[index]) : std::string();
}
TEST_F(ClientTest, ListsNetworksByName)
{
NetworkModel model;
ASSERT_TRUE(waitFor([&] { return model.items().size() == 5; }));
// Both of its access points make one network, shown by the stronger one.
const auto home = row(model, "Home");
ASSERT_TRUE(home);
EXPECT_EQ(home->kind, NetworkItem::Kind::AvailableConnection);
EXPECT_EQ(home->connectionPath, homeProfile);
EXPECT_EQ(home->specificPath, homeStrong);
EXPECT_EQ(home->signal, 70);
EXPECT_EQ(home->securityType, SecurityType::Wpa2Psk);
const auto cafeRow = row(model, "Cafe");
ASSERT_TRUE(cafeRow);
EXPECT_EQ(cafeRow->kind, NetworkItem::Kind::AvailableAccessPoint);
EXPECT_EQ(cafeRow->securityType, SecurityType::None);
EXPECT_EQ(row(model, "Library")->securityType, SecurityType::Wpa2Eap);
const auto wired = row(model, "Wired connection 1");
ASSERT_TRUE(wired);
EXPECT_EQ(wired->type, ConnectionType::Wired);
EXPECT_EQ(wired->connectionState, ConnectionStatus::Activated);
EXPECT_EQ(wired->ipV4Address, "192.168.1.5");
EXPECT_EQ(wired->gateway, "192.168.1.1");
EXPECT_EQ(wired->nameServer, "192.168.1.1");
EXPECT_EQ(wired->macAddress, "02:00:00:00:00:01");
}
TEST_F(ClientTest, HiddenAccessPointJoinsOnceNamed)
{
NetworkModel model;
ASSERT_TRUE(waitFor([&] { return model.items().size() == 5; }));
nm->set(hidden, fakenm::accessPoint, "Ssid", fakenm::bytes("Garden"));
ASSERT_TRUE(waitFor([&] { return row(model, "Garden").has_value(); }));
EXPECT_EQ(model.items().size(), 6u);
}
TEST_F(ClientTest, RowStaysWhileItsAccessPointsChange)
{
NetworkModel model;
ASSERT_TRUE(waitFor([&] { return model.items().size() == 5; }));
const std::string key = row(model, "Home")->key;
nm->set(wifiDevice, fakenm::wireless, "AccessPoints", fakenm::paths({homeWeak, cafe, hidden, library, office}));
ASSERT_TRUE(waitFor([&] { return row(model, "Home")->specificPath == homeWeak; }));
EXPECT_EQ(row(model, "Home")->key, key);
EXPECT_EQ(row(model, "Home")->signal, 40);
}
TEST_F(ClientTest, AgedOutNetworkStaysUntilScansMissIt)
{
NetworkModel model;
ASSERT_TRUE(waitFor([&] { return model.items().size() == 5; }));
scannedAgo(300 * 1000);
dropAccessPoint(cafe);
ASSERT_TRUE(waitFor([] { return Client::instance()->remembers(cafe); }));
wait(100ms);
ASSERT_TRUE(row(model, "Cafe"));
EXPECT_EQ(row(model, "Cafe")->specificPath, cafe);
EXPECT_EQ(row(model, "Cafe")->signal, 50);
// NetworkManager drops an access point that two scans missed; so does the list.
scannedAgo(0);
wait(100ms);
EXPECT_TRUE(row(model, "Cafe"));
scannedAgo(0, 1);
EXPECT_TRUE(waitFor([&] { return !row(model, "Cafe"); }));
EXPECT_FALSE(Client::instance()->remembers(cafe));
}
TEST_F(ClientTest, RememberedNetworkIsJoinedOnceScanned)
{
Handler handler;
scannedAgo(300 * 1000);
const fakenm::Interfaces cafeAgain = {{fakenm::accessPoint, nm->objects[cafe][fakenm::accessPoint]}};
dropAccessPoint(cafe);
ASSERT_TRUE(waitFor([] { return Client::instance()->remembers(cafe); }));
handler.addAndActivateConnection(wifiDevice, cafe);
ASSERT_TRUE(waitFor([&] { return nm->count("RequestScan") == 1; }));
wait(100ms);
EXPECT_EQ(nm->count("AddAndActivateConnection"), 0);
const std::string found = fakenm::managerPath + "/AccessPoint/7";
nm->add(found, cafeAgain);
nm->set(wifiDevice, fakenm::wireless, "AccessPoints", fakenm::paths({homeStrong, homeWeak, hidden, library, office, found}));
scannedAgo(0);
ASSERT_TRUE(waitFor([&] { return nm->count("AddAndActivateConnection") == 1; }));
EXPECT_EQ(pathArgument(nm->last("AddAndActivateConnection"), 2), found);
EXPECT_FALSE(Client::instance()->remembers(cafe));
}
TEST_F(ClientTest, UnreadableProfileStillShowsItsNetworkInUse)
{
NetworkModel model;
ASSERT_TRUE(waitFor([&] { return model.items().size() == 5; }));
const std::string theirs = fakenm::managerPath + "/Settings/50";
const std::string activePath = fakenm::managerPath + "/ActiveConnection/50";
nm->privateConnections.insert(theirs);
nm->add(theirs, {{fakenm::connection, {{"Flags", Variant(uint32_t(0))}}}});
nm->add(activePath, {{fakenm::active,
{{"Connection", fakenm::path(theirs)},
{"SpecificObject", fakenm::path(cafe)},
{"Devices", fakenm::paths({wifiDevice})},
{"State", Variant(uint32_t(2))},
{"Type", Variant(std::string("802-11-wireless"))},
{"Vpn", Variant(false)}}}});
nm->set(wifiDevice, fakenm::wireless, "ActiveAccessPoint", fakenm::path(cafe));
nm->set(wifiDevice, fakenm::device, "ActiveConnection", fakenm::path(activePath));
ASSERT_TRUE(waitFor([&] { return row(model, "Cafe")->connectionState == ConnectionStatus::Activated; }));
EXPECT_TRUE(row(model, "Cafe")->connectionPath.empty());
EXPECT_FALSE(Client::instance()->readable(theirs));
}
TEST_F(ClientTest, JoiningSendsATypedPartialProfile)
{
Handler handler;
handler.addAndActivateConnection(wifiDevice, office, "secret pass");
ASSERT_TRUE(waitFor([&] { return nm->count("AddAndActivateConnection") == 1; }));
const fakenm::Call& call = nm->last("AddAndActivateConnection");
EXPECT_EQ(call.signature, "a{sa{sv}}oo");
EXPECT_EQ(pathArgument(call, 2), office);
const Settings settings = settingsOf(call);
// NetworkManager fills in the SSID's bytes and the key management from the access point.
EXPECT_FALSE(settings.count("802-11-wireless"));
const VariantMap& security = settings.at("802-11-wireless-security");
EXPECT_FALSE(contains(security, "key-mgmt"));
EXPECT_EQ(toString(value(security, "psk")), "secret pass");
EXPECT_EQ(value(security, "psk-flags").signature(), "u");
EXPECT_EQ(toUInt(value(security, "psk-flags")), 1u);
EXPECT_EQ(toString(value(settings.at("connection"), "id")), "Office");
EXPECT_EQ(toStringList(value(settings.at("connection"), "permissions")),
std::vector<std::string>{Client::userPermission()});
}
TEST_F(ClientTest, FailedJoinIsForgotten)
{
Handler handler;
handler.addAndActivateConnection(wifiDevice, office, "wrong pass");
ASSERT_TRUE(waitFor([&] { return nm->count("AddAndActivateConnection") == 1; }));
ASSERT_TRUE(waitFor([] { return Client::instance()->activeConnections().size() == 2; }));
const std::string activePath = otherActive();
ASSERT_FALSE(activePath.empty());
// NM_ACTIVE_CONNECTION_STATE_DEACTIVATED for NO_SECRETS.
nm->emitSignal(activePath, fakenm::active, "StateChanged", {Variant(uint32_t(4)), Variant(uint32_t(9))});
ASSERT_TRUE(waitFor([&] { return nm->count("Delete") == 1; }));
EXPECT_TRUE(nm->last("Delete").path.starts_with(fakenm::managerPath + "/Settings/"));
}
TEST_F(ClientTest, EnterpriseJoinSendsOnlyTheSignIn)
{
Handler handler;
const auto network = handler.wirelessNetwork(library);
ASSERT_TRUE(network);
EXPECT_EQ(network->device, wifiDevice);
EXPECT_EQ(network->name, "Library");
EXPECT_EQ(network->security, SecurityType::Wpa2Eap);
EXPECT_FALSE(handler.wirelessNetwork(fakenm::managerPath + "/AccessPoint/99"));
handler.addAndActivateEnterprise(wifiDevice, library, "alice", "secret");
ASSERT_TRUE(waitFor([&] { return nm->count("AddAndActivateConnection") == 1; }));
const fakenm::Call& call = nm->last("AddAndActivateConnection");
EXPECT_EQ(pathArgument(call, 2), library);
const Settings settings = settingsOf(call);
// Not a hidden network: NetworkManager fills in the SSID and key management.
EXPECT_FALSE(settings.count("802-11-wireless"));
EXPECT_FALSE(settings.count("802-11-wireless-security"));
const VariantMap& eap = settings.at("802-1x");
EXPECT_EQ(toStringList(value(eap, "eap")), (std::vector<std::string>{"peap", "ttls"}));
EXPECT_EQ(toString(value(eap, "phase2-auth")), "mschapv2");
EXPECT_EQ(toString(value(eap, "identity")), "alice");
EXPECT_EQ(toString(value(eap, "password")), "secret");
EXPECT_EQ(value(eap, "password-flags").signature(), "u");
EXPECT_EQ(toUInt(value(eap, "password-flags")), 1u);
// NetworkManager's completion took it, and made it a WPA-EAP profile for that network.
ASSERT_TRUE(waitFor([] { return Client::instance()->activeConnections().size() == 2; }));
const std::string profile = objectPath(nm->objects[otherActive()][fakenm::active]["Connection"]);
EXPECT_EQ(toBytes(value(nm->settings[profile]["802-11-wireless"], "ssid")), "Library");
EXPECT_EQ(toString(value(nm->settings[profile]["802-11-wireless-security"], "key-mgmt")), "wpa-eap");
// A personal network is not joined this way, nor an enterprise one without a user name.
handler.addAndActivateEnterprise(wifiDevice, office, "alice", "secret");
handler.addAndActivateConnection(wifiDevice, library);
wait(100ms);
EXPECT_EQ(nm->count("AddAndActivateConnection"), 1);
}
TEST_F(ClientTest, HiddenNetworkIsSpeltOut)
{
Handler handler;
EXPECT_TRUE(handler.addOtherConnection("Garden", {}, "garden pass", "wpa2-wpa3"));
ASSERT_TRUE(waitFor([&] { return nm->count("AddAndActivateConnection") == 1; }));
// A hidden network answers only probes that name it.
ASSERT_EQ(nm->count("RequestScan"), 1);
const fakenm::Call& scan = nm->last("RequestScan");
EXPECT_EQ(scan.signature, "a{sv}");
const VariantMap options = scan.arguments[0].get<VariantMap>().value_or(VariantMap());
EXPECT_EQ(toBytesList(value(options, "ssids")), std::vector<std::string>{"Garden"});
const Settings settings = settingsOf(nm->last("AddAndActivateConnection"));
EXPECT_EQ(toBytes(value(settings.at("802-11-wireless"), "ssid")), "Garden");
EXPECT_TRUE(toBool(value(settings.at("802-11-wireless"), "hidden")));
EXPECT_EQ(toString(value(settings.at("802-11-wireless-security"), "key-mgmt")), "wpa-psk");
EXPECT_EQ(toStringList(value(settings.at("802-11-wireless-security"), "proto")), std::vector<std::string>{"rsn"});
EXPECT_FALSE(handler.addOtherConnection(std::string(33, 'x'), {}, {}, "none"));
}
TEST_F(ClientTest, UpdateKeepsWhatItDoesNotChange)
{
ConnectionSettings item(homeProfile);
EXPECT_TRUE(item.autoConnect());
EXPECT_FALSE(item.availableToOtherUsers());
item.setAutoConnect(false);
// An unrelated change arriving before the profile is re-read must not put
// the switch back to what the stale cache says.
Client::instance()->changed.emit();
EXPECT_FALSE(item.autoConnect());
ASSERT_TRUE(waitFor([&] { return nm->count("Update") == 1; }));
const Settings settings = settingsOf(nm->last("Update"));
EXPECT_FALSE(toBool(value(settings.at("connection"), "autoconnect"), true));
EXPECT_EQ(toBytes(value(settings.at("802-11-wireless"), "ssid")), "Home");
EXPECT_EQ(value(settings.at("802-11-wireless-security"), "psk-flags").signature(), "u");
// A value NetworkManager sent in a container type goes back in that type.
EXPECT_EQ(value(settings.at("ipv4"), "address-data").signature(), "aa{sv}");
// The profile NetworkManager reports after the update is what the switch shows.
wait(100ms);
EXPECT_FALSE(item.autoConnect());
}
TEST_F(ClientTest, IpSettingsAreSavedAndReapplied)
{
Handler handler;
std::vector<std::string> saved;
auto subscription = handler.ipSaved.connect([&](const std::string& connection) { saved.push_back(connection); });
const ip::Form home = handler.ipForm(homeProfile);
EXPECT_EQ(home.ipv4Method, "manual");
EXPECT_EQ(home.ipv4Address, "10.0.0.2");
EXPECT_EQ(home.ipv4Netmask, "255.255.255.0");
EXPECT_TRUE(handler.currentAddresses(homeProfile).ipv4Address.empty());
// The wired profile is up; its form offers the addresses it has now.
ip::Form form = handler.ipForm(wiredProfile);
const ip::Form current = handler.currentAddresses(wiredProfile);
EXPECT_EQ(current.ipv4Address, "192.168.1.5");
EXPECT_EQ(current.ipv4Netmask, "255.255.255.0");
EXPECT_EQ(current.ipv4Gateway, "192.168.1.1");
form.ipv4Method = "manual";
form.ipv4Address = current.ipv4Address;
form.ipv4Netmask = current.ipv4Netmask;
form.ipv4Gateway = current.ipv4Gateway;
form.automaticDns = false;
form.dns = "1.1.1.1";
handler.saveIp(wiredProfile, form);
ASSERT_TRUE(waitFor([&] { return saved.size() == 1; }));
EXPECT_EQ(saved[0], wiredProfile);
EXPECT_EQ(nm->last("Update").path, wiredProfile);
const VariantMap ipv4 = settingsOf(nm->last("Update")).at("ipv4");
EXPECT_EQ(toString(value(ipv4, "method")), "manual");
EXPECT_EQ(toString(value(ipv4, "gateway")), "192.168.1.1");
EXPECT_EQ(value(ipv4, "address-data").signature(), "aa{sv}");
EXPECT_EQ(value(ipv4, "dns").signature(), "au");
EXPECT_TRUE(toBool(value(ipv4, "ignore-auto-dns")));
// The device it is up on takes the change without disconnecting.
ASSERT_TRUE(waitFor([&] { return nm->count("Reapply") == 1; }));
EXPECT_EQ(nm->last("Reapply").path, wiredDevice);
EXPECT_EQ(nm->last("Reapply").signature, "a{sa{sv}}tu");
EXPECT_EQ(nm->count("ActivateConnection"), 0);
// A profile that isn't up is only saved.
form = handler.ipForm(homeProfile);
form.ipv4Address = "10.0.0.3";
handler.saveIp(homeProfile, form);
ASSERT_TRUE(waitFor([&] { return saved.size() == 2; }));
EXPECT_EQ(nm->count("Update"), 2);
wait(50ms);
EXPECT_EQ(nm->count("Reapply"), 1);
// Saving what is there changes nothing.
ASSERT_TRUE(waitFor([&] { return handler.ipForm(homeProfile).ipv4Address == "10.0.0.3"; }));
handler.saveIp(homeProfile, handler.ipForm(homeProfile));
EXPECT_EQ(saved.size(), 3u);
EXPECT_EQ(nm->count("Update"), 2);
}
TEST_F(ClientTest, ScansOnlyDevicesThatCan)
{
Handler handler;
handler.requestScan();
ASSERT_TRUE(waitFor([&] { return nm->count("RequestScan") == 1; }));
EXPECT_EQ(nm->last("RequestScan").path, wifiDevice);
nm->set(wifiDevice, fakenm::device, "State", Variant(uint32_t(20)));
ASSERT_TRUE(waitFor([] { return Client::instance()->deviceState(wifiDevice) == 20u; }));
handler.requestScan();
wait(200ms);
EXPECT_EQ(nm->count("RequestScan"), 1);
}
TEST_F(ClientTest, SwitchesGoToNetworkManager)
{
EnabledConnections enabled;
EXPECT_TRUE(enabled.isReady());
EXPECT_TRUE(enabled.isWirelessEnabled());
EXPECT_TRUE(enabled.isWiredAvailable());
EXPECT_TRUE(enabled.isWiredEnabled());
Handler handler;
handler.enableWireless(false);
ASSERT_TRUE(waitFor([&] { return !enabled.isWirelessEnabled(); }));
// The wired device is up: turning Ethernet off disconnects it.
handler.enableWired(false);
ASSERT_TRUE(waitFor([&] { return nm->count("Disconnect") == 1; }));
EXPECT_EQ(nm->last("Disconnect").path, wiredDevice);
}
TEST_F(ClientTest, HotspotKeepsOneProfile)
{
Handler handler;
ASSERT_TRUE(waitFor([&] { return handler.hotspotCapable(); }));
EXPECT_TRUE(handler.hotspotSupported());
EXPECT_TRUE(handler.hotspotReplaces().empty());
EXPECT_FALSE(handler.hotspotActive());
HotspotConfiguration* configuration = HotspotConfiguration::instance();
configuration->setName("Desk");
configuration->setPassword("good password");
handler.createHotspot();
ASSERT_TRUE(waitFor([&] { return nm->count("AddAndActivateConnection") == 1; }));
const fakenm::Call& call = nm->last("AddAndActivateConnection");
EXPECT_EQ(pathArgument(call, 1), wifiDevice);
const Settings settings = settingsOf(call);
EXPECT_EQ(toString(value(settings.at("802-11-wireless"), "mode")), "ap");
EXPECT_EQ(toBytes(value(settings.at("802-11-wireless"), "ssid")), "Desk");
EXPECT_EQ(toString(value(settings.at("802-11-wireless-security"), "psk")), "good password");
EXPECT_EQ(toString(value(settings.at("ipv4"), "method")), "shared");
ASSERT_TRUE(waitFor([&] { return handler.hotspotActive(); }));
// The next start updates that profile rather than adding another.
nm->remove(otherActive());
ASSERT_TRUE(waitFor([&] { return !handler.hotspotActive(); }));
configuration->setName("Desk 2");
handler.createHotspot();
ASSERT_TRUE(waitFor([&] { return nm->count("ActivateConnection") == 1; }));
EXPECT_EQ(nm->count("AddAndActivateConnection"), 1);
EXPECT_EQ(toBytes(value(settingsOf(nm->last("Update")).at("802-11-wireless"), "ssid")), "Desk 2");
EXPECT_EQ(pathArgument(nm->last("ActivateConnection"), 0), nm->last("Update").path);
EXPECT_TRUE(waitFor([&] { return handler.hotspotActive(); }));
}
TEST_F(ClientTest, HotspotThatNeverComesUpIsReported)
{
Handler handler;
int failures = 0;
auto subscription = handler.hotspotFailed.connect([&](const std::string&) { ++failures; });
handler.createHotspot();
ASSERT_TRUE(waitFor([] { return Client::instance()->activeConnections().size() == 2; }));
const std::string activePath = otherActive();
// NM_ACTIVE_CONNECTION_STATE_DEACTIVATED for DEVICE_DISCONNECTED.
wait(100ms);
nm->emitSignal(activePath, fakenm::active, "StateChanged", {Variant(uint32_t(4)), Variant(uint32_t(3))});
EXPECT_TRUE(waitFor([&] { return failures == 1; }));
}
TEST_F(ClientTest, HotspotGoneBeforeReplyIsReported)
{
Handler handler;
ASSERT_TRUE(waitFor([&] { return handler.hotspotCapable(); }));
int failures = 0;
auto subscription = handler.hotspotFailed.connect([&](const std::string&) { ++failures; });
nm->dropActivations = true;
handler.createHotspot();
EXPECT_TRUE(waitFor([&] { return failures == 1; }));
EXPECT_FALSE(handler.hotspotActive());
}
TEST_F(ClientTest, HotspotNamesTheNetworkItWouldEnd)
{
Handler handler;
ASSERT_TRUE(waitFor([&] { return handler.hotspotCapable(); }));
nm->add(homeActive, {{fakenm::active,
{{"Connection", fakenm::path(homeProfile)},
{"Id", Variant(std::string("Home"))},
{"Devices", fakenm::paths({wifiDevice})},
{"State", Variant(uint32_t(2))},
{"Type", Variant(std::string("802-11-wireless"))},
{"Vpn", Variant(false)}}}});
nm->set(wifiDevice, fakenm::device, "ActiveConnection", fakenm::path(homeActive));
ASSERT_TRUE(waitFor([&] { return handler.hotspotReplaces() == "Home"; }));
EXPECT_TRUE(handler.hotspotSupported());
nm->permissions["org.freedesktop.NetworkManager.wifi.share.protected"] = "no";
nm->emitSignal(fakenm::managerPath, fakenm::manager, "CheckPermissions", {});
ASSERT_TRUE(waitFor([&] { return !handler.hotspotAllowed(); }));
EXPECT_FALSE(handler.hotspotSupported());
EXPECT_TRUE(handler.hotspotCapable());
}
TEST_F(ClientTest, VpnRowsFollowTheirState)
{
NetworkModel model;
ASSERT_TRUE(waitFor([&] { return model.items().size() == 5; }));
addVpn(officeVpn, "Office VPN", "org.freedesktop.NetworkManager.openvpn", 3000);
addVpn(branchVpn, "Branch", "org.freedesktop.NetworkManager.l2tp", 2000);
addWireGuard(homeVpn, "Home WG", 1000);
ASSERT_TRUE(waitFor([&] { return model.items().size() == 8; }));
const auto officeRow = row(model, "Office VPN");
EXPECT_EQ(officeRow->type, ConnectionType::Vpn);
EXPECT_EQ(officeRow->vpnType, "OpenVPN");
EXPECT_TRUE(officeRow->canActivate);
EXPECT_EQ(officeRow->connectionState, ConnectionStatus::Deactivated);
// No l2tp plugin is installed in this test.
const auto branchRow = row(model, "Branch");
EXPECT_FALSE(branchRow->canActivate);
EXPECT_NE(branchRow->statusMessage.find("network-manager-l2tp"), std::string::npos);
EXPECT_EQ(row(model, "Home WG")->vpnType, "WireGuard");
// The tunnel is up when the plugin says so, not when the active connection exists.
const std::string activePath = fakenm::managerPath + "/ActiveConnection/60";
nm->add(activePath, {{fakenm::active,
{{"Connection", fakenm::path(officeVpn)},
{"Devices", fakenm::paths({})},
{"State", Variant(uint32_t(2))},
{"Type", Variant(std::string("vpn"))},
{"Vpn", Variant(true)}}},
{vpnInterface, {{"VpnState", Variant(uint32_t(3))}}}});
ASSERT_TRUE(waitFor([&] { return row(model, "Office VPN")->connectionState == ConnectionStatus::Activating; }));
nm->set(activePath, vpnInterface, "VpnState", Variant(uint32_t(5)));
ASSERT_TRUE(waitFor([&] { return row(model, "Office VPN")->connectionState == ConnectionStatus::Activated; }));
// NM_ACTIVE_CONNECTION_STATE_REASON_LOGIN_FAILED stays on the row until the next try.
nm->emitSignal(activePath, fakenm::active, "StateChanged", {Variant(uint32_t(4)), Variant(uint32_t(10))});
nm->remove(activePath);
ASSERT_TRUE(waitFor([&] { return !row(model, "Office VPN")->statusMessage.empty(); }));
EXPECT_EQ(row(model, "Office VPN")->connectionState, ConnectionStatus::Deactivated);
}
TEST_F(ClientTest, VpnFailureIsReported)
{
Handler handler;
std::vector<std::pair<std::string, std::string>> failures;
auto subscription = handler.vpnFailed.connect(
[&](const std::string& name, const std::string& message) { failures.emplace_back(name, message); });
addVpn(officeVpn, "Office VPN", "org.freedesktop.NetworkManager.openvpn", 1000);
const std::string activePath = fakenm::managerPath + "/ActiveConnection/61";
nm->add(activePath, {{fakenm::active,
{{"Connection", fakenm::path(officeVpn)},
{"Devices", fakenm::paths({})},
{"Id", Variant(std::string("Office VPN"))},
{"State", Variant(uint32_t(1))},
{"Type", Variant(std::string("vpn"))},
{"Vpn", Variant(true)}}},
{vpnInterface, {{"VpnState", Variant(uint32_t(3))}}}});
ASSERT_TRUE(waitFor([&] {
const auto list = Client::instance()->activeConnections();
return std::find(list.begin(), list.end(), activePath) != list.end();
}));
// NO_SECRETS (the prompt was cancelled) and USER_DISCONNECTED are not failures.
for (const uint32_t reason : {9u, 2u, 10u})
nm->emitSignal(activePath, fakenm::active, "StateChanged", {Variant(uint32_t(4)), Variant(reason)});
ASSERT_TRUE(waitFor([&] { return failures.size() == 1; }));
EXPECT_EQ(failures[0].first, "Office VPN");
EXPECT_EQ(failures[0].second, vpn::failureMessage(10));
}
TEST_F(ClientTest, VpnFormIsSavedInTheProfile)
{
NetworkModel model;
addVpn(officeVpn, "Office VPN", "org.freedesktop.NetworkManager.openvpn", 1000);
ASSERT_TRUE(waitFor([&] { return row(model, "Office VPN").has_value(); }));
EXPECT_TRUE(row(model, "Office VPN")->vpnUsesUserName);
EXPECT_TRUE(row(model, "Office VPN")->vpnUserName.empty());
Handler handler;
const VpnForm form = handler.vpnForm({}, officeVpn);
EXPECT_EQ(form.kind, "openvpn");
EXPECT_EQ(form.name, "Office VPN");
EXPECT_TRUE(form.shared);
EXPECT_FALSE(form.ownNetworkOnly);
EXPECT_TRUE(form.exportable);
vpn::schema::Values values;
for (const vpn::schema::FormField& field : form.fields)
values[field.key] = field.type == vpn::schema::Type::Bool ? (field.checked ? "true" : "false") : field.value;
EXPECT_EQ(values["connection-type"], "password");
std::vector<std::string> saved;
auto subscription = handler.vpnSaved.connect([&](const std::string& connection) { saved.push_back(connection); });
values["remote"] = "vpn.example.com";
values["username"] = "alice";
VpnValues edit;
edit.name = "Office HQ";
edit.shared = false;
edit.ownNetworkOnly = true;
edit.values = values;
edit.storage = {{"password", SecretSystemOwned}};
edit.secrets = {{"password", "s3cret"}};
handler.saveVpn(officeVpn, edit);
ASSERT_TRUE(waitFor([&] { return saved.size() == 1; }));
EXPECT_EQ(nm->count("Update"), 1);
const Settings settings = settingsOf(nm->last("Update"));
const VariantMap& connection = settings.at("connection");
EXPECT_EQ(toString(value(connection, "id")), "Office HQ");
EXPECT_EQ(toString(value(connection, "uuid")), "office vpn");
EXPECT_EQ(toStringList(value(connection, "permissions")), std::vector<std::string>{Client::userPermission()});
EXPECT_TRUE(toBool(value(settings.at("ipv4"), "never-default")));
const Variant& data = value(settings.at("vpn"), "data");
EXPECT_EQ(data.signature(), "a{ss}");
const auto dataMap = toStringMap(data);
EXPECT_EQ(dataMap.at("username"), "alice");
// Private now, so the password is the agent's rather than the system's.
EXPECT_EQ(dataMap.at("password-flags"), "1");
EXPECT_EQ(toStringMap(value(settings.at("vpn"), "secrets")).at("password"), "s3cret");
EXPECT_TRUE(waitFor([&] { return row(model, "Office HQ") && row(model, "Office HQ")->vpnUserName == "alice"; }));
std::vector<std::string> failed;
auto failure = handler.vpnSaveFailed.connect([&](const std::string& message) { failed.push_back(message); });
values["remote"] = "";
VpnValues broken;
broken.values = values;
handler.saveVpn(officeVpn, broken);
EXPECT_EQ(failed.size(), 1u);
EXPECT_EQ(nm->count("Update"), 1);
}
TEST_F(ClientTest, WireGuardReadsBackFromDBus)
{
const std::string path = fakenm::managerPath + "/Settings/33";
nm->settings[path] = {
{"connection",
{{"id", Variant(std::string("Work WG"))},
{"uuid", Variant(std::string("work wg"))},
{"type", Variant(std::string("wireguard"))},
{"interface-name", Variant(std::string("wg0"))}}},
{"wireguard",
{{"private-key-flags", Variant(uint32_t(SecretSystemOwned))},
{"mtu", Variant(uint32_t(1280))},
{"peers", Variant(std::vector<VariantMap>{{
{"public-key", Variant(std::string("xTIBA5rboUvnH4htodjb6e697QjLERt1NAB4mZqp8Dg="))},
{"preshared-key-flags", Variant(uint32_t(SecretSystemOwned))},
{"endpoint", Variant(std::string("vpn.example:51820"))},
{"allowed-ips", Variant(std::vector<std::string>{"0.0.0.0/0"})},
{"persistent-keepalive", Variant(uint32_t(25))},
}})}}},
{"ipv4",
{{"method", Variant(std::string("manual"))},
{"address-data", addressData("10.0.0.2")},
{"dns", Variant(std::vector<uint32_t>{htobe32(0x0a000001u)})},
{"dns-search", Variant(std::vector<std::string>{"corp.example"})}}},
{"ipv6",
{{"method", Variant(std::string("manual"))},
{"address-data",
Variant(std::vector<VariantMap>{{{"address", Variant(std::string("fd00::2"))}, {"prefix", Variant(uint32_t(64))}}})},
{"dns", Variant(std::vector<std::vector<uint8_t>>{{0xfd, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}})}}},
};
nm->add(path, {{fakenm::connection, {{"Flags", Variant(uint32_t(0))}}}});
ASSERT_TRUE(waitFor([&] { return !Client::instance()->settings(path).empty(); }));
const Settings settings = Client::instance()->settings(path);
const vpn::WireGuardForm form = vpn::wireGuardForm(settings);
EXPECT_EQ(form.addresses, "10.0.0.2/24, fd00::2/64");
EXPECT_EQ(form.dns, "10.0.0.1, corp.example, fd00::1");
EXPECT_EQ(form.mtu, "1280");
ASSERT_EQ(form.peers.size(), 1u);
EXPECT_EQ(form.peers[0].publicKey, "xTIBA5rboUvnH4htodjb6e697QjLERt1NAB4mZqp8Dg=");
EXPECT_EQ(form.peers[0].endpoint, "vpn.example:51820");
EXPECT_EQ(form.peers[0].allowedIps, "0.0.0.0/0");
EXPECT_EQ(form.peers[0].keepalive, "25");
EXPECT_TRUE(form.peers[0].hasPresharedKey);
// Saved again with the keys left empty, the retained preshared key stays.
std::string error;
const auto edited = vpn::wireGuardSettings(form, settings, &error);
ASSERT_TRUE(edited) << error;
const auto peers = toMapList(value(edited->at("wireguard"), "peers"));
ASSERT_EQ(peers.size(), 1u);
EXPECT_EQ(toUInt(value(peers[0], "preshared-key-flags"), 99), uint32_t(SecretSystemOwned));
const std::string config = vpn::wireGuardConfig(settings);
EXPECT_NE(config.find("Address = 10.0.0.2/24, fd00::2/64"), std::string::npos);
EXPECT_NE(config.find("[Peer]\nPublicKey = xTIBA5rboUvnH4htodjb6e697QjLERt1NAB4mZqp8Dg="), std::string::npos);
EXPECT_NE(config.find("AllowedIPs = 0.0.0.0/0"), std::string::npos);
}
TEST_F(ClientTest, VpnAddedByHandIsPrivate)
{
Handler handler;
std::vector<std::string> added;
auto subscription = handler.vpnAdded.connect([&](const std::string& uuid) { added.push_back(uuid); });
VpnValues branch;
branch.kind = "l2tp";
branch.name = "Branch";
branch.values = {{"gateway", "vpn.example.com"}, {"user", "alice"}, {"ipsec-enabled", "true"}};
branch.secrets = {{"password", "s3cret"}, {"ipsec-psk", "shared"}};
handler.saveVpn({}, branch);
ASSERT_TRUE(waitFor([&] { return added.size() == 1; }));
const Settings settings = settingsOf(nm->last("AddConnection"));
const VariantMap& connection = settings.at("connection");
EXPECT_EQ(toString(value(connection, "id")), "Branch");
EXPECT_EQ(toString(value(connection, "type")), "vpn");
EXPECT_FALSE(toBool(value(connection, "autoconnect"), true));
EXPECT_EQ(toStringList(value(connection, "permissions")), std::vector<std::string>{Client::userPermission()});
const VariantMap& vpnSetting = settings.at("vpn");
EXPECT_EQ(toString(value(vpnSetting, "service-type")), "org.freedesktop.NetworkManager.l2tp");
EXPECT_EQ(value(vpnSetting, "data").signature(), "a{ss}");
EXPECT_EQ(value(vpnSetting, "secrets").signature(), "a{ss}");
const auto data = toStringMap(value(vpnSetting, "data"));
const auto secrets = toStringMap(value(vpnSetting, "secrets"));
EXPECT_EQ(data.at("user"), "alice");
EXPECT_EQ(data.at("password-flags"), "1");
EXPECT_EQ(data.at("ipsec-enabled"), "yes");
EXPECT_EQ(secrets.at("ipsec-psk"), "shared");
EXPECT_EQ(toString(value(settings.at("ipv4"), "method")), "auto");
// Unnamed, it goes by its server; shared, it belongs to everyone.
VpnValues pptp;
pptp.kind = "pptp";
pptp.shared = true;
pptp.values = {{"gateway", "pptp.example.com"}};
handler.saveVpn({}, pptp);
ASSERT_TRUE(waitFor([&] { return added.size() == 2; }));
const Settings second = settingsOf(nm->last("AddConnection"));
EXPECT_EQ(toString(value(second.at("connection"), "id")), "pptp.example.com");
EXPECT_TRUE(toStringList(value(second.at("connection"), "permissions")).empty());
EXPECT_EQ(toStringMap(value(second.at("vpn"), "data")).at("password-flags"), "0");
std::vector<std::string> failed;
auto failure = handler.vpnAddFailed.connect([&](const std::string& message) { failed.push_back(message); });
VpnValues openvpn;
openvpn.kind = "openvpn";
handler.saveVpn({}, openvpn);
EXPECT_EQ(failed.size(), 1u);
}
TEST_F(ClientTest, WireGuardImportsAsPrivateAndManual)
{
char pattern[] = "/tmp/wireguard-import-XXXXXX";
const std::string directory = mkdtemp(pattern);
const std::string file = directory + "/Home Office.conf";
{
std::ofstream out(file);
out << "[Interface]\nPrivateKey = yAnz5TF+lXXJte14tji3zlMNq+hd2rYUIgJBgB3fBmk=\n"
"Address = 10.99.0.2/24\nDNS = 10.99.0.1\n\n"
"[Peer]\nPublicKey = xTIBA5rboUvnH4htodjb6e697QjLERt1NAB4mZqp8Dg=\n"
"AllowedIPs = 10.99.0.0/24\nEndpoint = 192.0.2.1:51820\n";
}
Handler handler;
std::vector<std::string> imported;
auto subscription = handler.vpnAdded.connect([&](const std::string& uuid) { imported.push_back(uuid); });
handler.importVpn(file);
ASSERT_TRUE(waitFor([&] { return imported.size() == 1; }));
EXPECT_EQ(nm->count("AddConnection"), 1);
const fakenm::Call& call = nm->last("AddConnection");
EXPECT_EQ(call.signature, "a{sa{sv}}");
const Settings settings = settingsOf(call);
const VariantMap& connection = settings.at("connection");
EXPECT_EQ(toString(value(connection, "id")), "Home Office");
EXPECT_FALSE(toBool(value(connection, "autoconnect"), true));
EXPECT_EQ(toStringList(value(connection, "permissions")), std::vector<std::string>{Client::userPermission()});
EXPECT_EQ(toString(value(connection, "uuid")), imported[0]);
EXPECT_EQ(value(settings.at("wireguard"), "peers").signature(), "aa{sv}");
EXPECT_EQ(value(settings.at("ipv4"), "address-data").signature(), "aa{sv}");
EXPECT_EQ(value(settings.at("ipv4"), "dns").signature(), "au");
std::filesystem::remove_all(directory);
}
TEST_F(ClientTest, ReloadsAfterRestart)
{
NetworkModel model;
ASSERT_TRUE(waitFor([&] { return model.items().size() == 5; }));
nm->stop();
ASSERT_TRUE(waitFor([] { return !Client::instance()->ready(); }));
ASSERT_TRUE(waitFor([&] { return model.items().empty(); }));
ASSERT_TRUE(nm->serve());
ASSERT_TRUE(waitFor([] { return Client::instance()->ready(); }));
ASSERT_TRUE(waitFor([&] { return model.items().size() == 5; }));
EXPECT_EQ(nm->count("GetManagedObjects"), 2);
}
int main(int argc, char** argv)
{
testing::InitGoogleTest(&argc, argv);
EventLoop eventLoop;
loop = &eventLoop;
// Only OpenVPN's plugin is installed.
char pattern[] = "/tmp/vpn-plugins-XXXXXX";
const std::string plugins = mkdtemp(pattern);
{
std::ofstream name(plugins + "/nm-openvpn-service.name");
name << "[VPN Connection]\nname=openvpn\nservice=org.freedesktop.NetworkManager.openvpn\n";
}
vpn::setPluginDirectories({plugins});
const int result = RUN_ALL_TESTS();
std::filesystem::remove_all(plugins);
return result;
}