about summary refs log tree commit diff
path: root/src/nm-dispatcher/tests
diff options
context:
space:
mode:
authorSebastien Bacher <seb128@ubuntu.com>2021-07-05 20:35:03 +0200
committerSebastien Bacher <seb128@ubuntu.com>2021-07-05 20:35:03 +0200
commit35779c6675728fa6f0fd0a21cefb904408509c23 (patch)
tree553e7239e0ba182b4f9b29b1e7a9d66a595d08bc /src/nm-dispatcher/tests
parentd92aa7f298fe84d4cf686c5ad64b73438e00d377 (diff)
New upstream version 1.32.2
Diffstat (limited to 'src/nm-dispatcher/tests')
-rw-r--r--src/nm-dispatcher/tests/dispatcher-connectivity-full17
-rw-r--r--src/nm-dispatcher/tests/dispatcher-connectivity-unknown16
-rw-r--r--src/nm-dispatcher/tests/dispatcher-down23
-rw-r--r--src/nm-dispatcher/tests/dispatcher-external40
-rw-r--r--src/nm-dispatcher/tests/dispatcher-up66
-rw-r--r--src/nm-dispatcher/tests/dispatcher-vpn-down65
-rw-r--r--src/nm-dispatcher/tests/dispatcher-vpn-up65
-rw-r--r--src/nm-dispatcher/tests/meson.build26
-rw-r--r--src/nm-dispatcher/tests/test-dispatcher-envp.c645
9 files changed, 963 insertions, 0 deletions
diff --git a/src/nm-dispatcher/tests/dispatcher-connectivity-full b/src/nm-dispatcher/tests/dispatcher-connectivity-full
new file mode 100644
index 00000000..937e79d7
--- /dev/null
+++ b/src/nm-dispatcher/tests/dispatcher-connectivity-full
@@ -0,0 +1,17 @@
+[main]
+action=connectivity-change
+uuid=3fd2a33a-d81b-423f-ae99-e6baba742311
+id=Random Connection
+connectivity-state=FULL
+
+[device]
+state=30
+ip-interface=wlan0
+type=2
+interface=wlan0
+path=/org/freedesktop/NetworkManager/Devices/0
+
+[env]
+PATH=
+NM_DISPATCHER_ACTION=connectivity-change
+CONNECTIVITY_STATE=FULL
diff --git a/src/nm-dispatcher/tests/dispatcher-connectivity-unknown b/src/nm-dispatcher/tests/dispatcher-connectivity-unknown
new file mode 100644
index 00000000..9aa48e4a
--- /dev/null
+++ b/src/nm-dispatcher/tests/dispatcher-connectivity-unknown
@@ -0,0 +1,16 @@
+[main]
+action=connectivity-change
+uuid=3fd2a33a-d81b-423f-ae99-e6baba742311
+id=Random Connection
+connectivity-state=UNKNOWN
+
+[device]
+state=30
+ip-interface=wlan0
+type=2
+interface=wlan0
+path=/org/freedesktop/NetworkManager/Devices/0
+
+[env]
+PATH=
+NM_DISPATCHER_ACTION=connectivity-change
diff --git a/src/nm-dispatcher/tests/dispatcher-down b/src/nm-dispatcher/tests/dispatcher-down
new file mode 100644
index 00000000..bd7460ae
--- /dev/null
+++ b/src/nm-dispatcher/tests/dispatcher-down
@@ -0,0 +1,23 @@
+[main]
+action=down
+expected-iface=wlan0
+uuid=3fd2a33a-d81b-423f-ae99-e6baba742311
+id=Random Connection
+
+[device]
+state=30
+ip-interface=wlan0
+type=2
+interface=wlan0
+path=/org/freedesktop/NetworkManager/Devices/0
+
+[env]
+PATH=
+NM_DISPATCHER_ACTION=down
+CONNECTION_UUID=3fd2a33a-d81b-423f-ae99-e6baba742311
+CONNECTION_DBUS_PATH=/org/freedesktop/NetworkManager/Connections/5
+CONNECTION_ID=Random Connection
+CONNECTION_FILENAME=/src/nm-dispatcher/tests/dispatcher-down
+DEVICE_IFACE=wlan0
+DEVICE_IP_IFACE=wlan0
+
diff --git a/src/nm-dispatcher/tests/dispatcher-external b/src/nm-dispatcher/tests/dispatcher-external
new file mode 100644
index 00000000..382ff706
--- /dev/null
+++ b/src/nm-dispatcher/tests/dispatcher-external
@@ -0,0 +1,40 @@
+[main]
+action=up
+expected-iface=virbr0
+uuid=92bbc2fb-7304-46be-8ebb-6093dbe19a6a
+id=virbr0
+external=1
+
+[device]
+state=100
+ip-interface=virbr0
+type=13
+interface=virbr0
+path=/org/freedesktop/NetworkManager/Devices/0
+
+[proxy]
+pac-url=http://networkmanager.com/proxy.pac
+pac-script="function FindProxyForURL (url, host) {}"
+
+[ip4]
+addresses=192.168.122.1/24 0.0.0.0
+domains=
+gateway=0.0.0.0
+
+[env]
+PATH=
+NM_DISPATCHER_ACTION=up
+CONNECTION_UUID=92bbc2fb-7304-46be-8ebb-6093dbe19a6a
+CONNECTION_DBUS_PATH=/org/freedesktop/NetworkManager/Connections/5
+CONNECTION_FILENAME=/src/nm-dispatcher/tests/dispatcher-external
+CONNECTION_ID=virbr0
+CONNECTION_EXTERNAL=1
+DEVICE_IFACE=virbr0
+DEVICE_IP_IFACE=virbr0
+PROXY_PAC_URL=http://networkmanager.com/proxy.pac
+PROXY_PAC_SCRIPT="function FindProxyForURL (url, host) {}"
+IP4_NUM_ADDRESSES=1
+IP4_ADDRESS_0=192.168.122.1/24 0.0.0.0
+IP4_GATEWAY=0.0.0.0
+IP4_NUM_ROUTES=0
+
diff --git a/src/nm-dispatcher/tests/dispatcher-up b/src/nm-dispatcher/tests/dispatcher-up
new file mode 100644
index 00000000..cd6c0fad
--- /dev/null
+++ b/src/nm-dispatcher/tests/dispatcher-up
@@ -0,0 +1,66 @@
+[main]
+action=up
+expected-iface=wlan0
+uuid=3fd2a33a-d81b-423f-ae99-e6baba742311
+id=Random Connection
+
+[device]
+state=100
+ip-interface=wlan0
+type=2
+interface=wlan0
+path=/org/freedesktop/NetworkManager/Devices/0
+
+[dhcp4]
+netbios_name_servers=0.0.0.0
+domain_name_servers=68.87.77.134 68.87.72.134 192.168.1.1
+dhcp_lease_time=86400
+network_number=192.168.1.0
+domain_name=hsd1.mn.comcast.net.
+ip_address=192.168.1.119
+dhcp_message_type=5
+dhcp_server_identifier=192.168.1.1
+routers=192.168.1.1
+broadcast_address=192.168.1.255
+subnet_mask=255.255.255.0
+expiry=1304300446
+
+[proxy]
+pac-url=http://networkmanager.com/proxy.pac
+pac-script="function FindProxyForURL (url, host) {}"
+
+[ip4]
+addresses=192.168.1.119/24 192.168.1.1
+nameservers=68.87.77.134 68.87.72.134 192.168.1.1
+domains=hsd1.mn.comcast.net.
+
+[env]
+PATH=
+NM_DISPATCHER_ACTION=up
+CONNECTION_UUID=3fd2a33a-d81b-423f-ae99-e6baba742311
+CONNECTION_DBUS_PATH=/org/freedesktop/NetworkManager/Connections/5
+CONNECTION_ID=Random Connection
+CONNECTION_FILENAME=/src/nm-dispatcher/tests/dispatcher-up
+DEVICE_IFACE=wlan0
+DEVICE_IP_IFACE=wlan0
+PROXY_PAC_URL=http://networkmanager.com/proxy.pac
+PROXY_PAC_SCRIPT="function FindProxyForURL (url, host) {}"
+IP4_ADDRESS_0=192.168.1.119/24 192.168.1.1
+IP4_NUM_ADDRESSES=1
+IP4_NAMESERVERS=68.87.77.134 68.87.72.134 192.168.1.1
+IP4_GATEWAY=192.168.1.1
+IP4_DOMAINS=hsd1.mn.comcast.net.
+IP4_NUM_ROUTES=0
+DHCP4_NETBIOS_NAME_SERVERS=0.0.0.0
+DHCP4_DOMAIN_NAME_SERVERS=68.87.77.134 68.87.72.134 192.168.1.1
+DHCP4_DHCP_LEASE_TIME=86400
+DHCP4_NETWORK_NUMBER=192.168.1.0
+DHCP4_DOMAIN_NAME=hsd1.mn.comcast.net.
+DHCP4_IP_ADDRESS=192.168.1.119
+DHCP4_DHCP_MESSAGE_TYPE=5
+DHCP4_DHCP_SERVER_IDENTIFIER=192.168.1.1
+DHCP4_ROUTERS=192.168.1.1
+DHCP4_BROADCAST_ADDRESS=192.168.1.255
+DHCP4_SUBNET_MASK=255.255.255.0
+DHCP4_EXPIRY=1304300446
+
diff --git a/src/nm-dispatcher/tests/dispatcher-vpn-down b/src/nm-dispatcher/tests/dispatcher-vpn-down
new file mode 100644
index 00000000..e99c6142
--- /dev/null
+++ b/src/nm-dispatcher/tests/dispatcher-vpn-down
@@ -0,0 +1,65 @@
+[main]
+action=vpn-down
+expected-iface=tun0
+uuid=355653c0-34d3-4777-ad25-f9a498b7ef8e
+id=Random Connection
+
+[device]
+state=100
+ip-interface=tun0
+type=2
+interface=wlan0
+path=/org/freedesktop/NetworkManager/Devices/0
+
+[dhcp4]
+netbios_name_servers=0.0.0.0
+domain_name_servers=68.87.77.134 68.87.72.134 192.168.1.1
+dhcp_lease_time=86400
+network_number=192.168.1.0
+domain_name=hsd1.mn.comcast.net.
+ip_address=192.168.1.119
+dhcp_message_type=5
+dhcp_server_identifier=192.168.1.1
+routers=192.168.1.1
+broadcast_address=192.168.1.255
+subnet_mask=255.255.255.0
+expiry=1304349405
+
+[proxy]
+pac-url=http://networkmanager.com/proxy.pac
+pac-script="function FindProxyForURL (url, host) {}"
+
+[ip4]
+addresses=192.168.1.119/24 192.168.1.1
+nameservers=68.87.77.134 68.87.72.134 192.168.1.1
+domains=hsd1.mn.comcast.net.
+
+[env]
+PATH=
+NM_DISPATCHER_ACTION=vpn-down
+CONNECTION_UUID=355653c0-34d3-4777-ad25-f9a498b7ef8e
+CONNECTION_DBUS_PATH=/org/freedesktop/NetworkManager/Connections/5
+CONNECTION_ID=Random Connection
+CONNECTION_FILENAME=/src/nm-dispatcher/tests/dispatcher-vpn-down
+DEVICE_IFACE=wlan0
+DEVICE_IP_IFACE=tun0
+PROXY_PAC_URL=http://networkmanager.com/proxy.pac
+PROXY_PAC_SCRIPT="function FindProxyForURL (url, host) {}"
+IP4_ADDRESS_0=192.168.1.119/24 192.168.1.1
+IP4_NUM_ADDRESSES=1
+IP4_NAMESERVERS=68.87.77.134 68.87.72.134 192.168.1.1
+IP4_GATEWAY=192.168.1.1
+IP4_DOMAINS=hsd1.mn.comcast.net.
+IP4_NUM_ROUTES=0
+DHCP4_NETBIOS_NAME_SERVERS=0.0.0.0
+DHCP4_DOMAIN_NAME_SERVERS=68.87.77.134 68.87.72.134 192.168.1.1
+DHCP4_DHCP_LEASE_TIME=86400
+DHCP4_NETWORK_NUMBER=192.168.1.0
+DHCP4_DOMAIN_NAME=hsd1.mn.comcast.net.
+DHCP4_IP_ADDRESS=192.168.1.119
+DHCP4_DHCP_MESSAGE_TYPE=5
+DHCP4_DHCP_SERVER_IDENTIFIER=192.168.1.1
+DHCP4_ROUTERS=192.168.1.1
+DHCP4_BROADCAST_ADDRESS=192.168.1.255
+DHCP4_SUBNET_MASK=255.255.255.0
+DHCP4_EXPIRY=1304349405
diff --git a/src/nm-dispatcher/tests/dispatcher-vpn-up b/src/nm-dispatcher/tests/dispatcher-vpn-up
new file mode 100644
index 00000000..a2414790
--- /dev/null
+++ b/src/nm-dispatcher/tests/dispatcher-vpn-up
@@ -0,0 +1,65 @@
+[main]
+action=vpn-up
+expected-iface=tun0
+uuid=355653c0-34d3-4777-ad25-f9a498b7ef8e
+id=Random Connection
+
+[device]
+state=100
+ip-interface=tun0
+type=2
+interface=wlan0
+path=/org/freedesktop/NetworkManager/Devices/0
+
+[dhcp4]
+netbios_name_servers=0.0.0.0
+domain_name_servers=68.87.77.134 68.87.72.134 192.168.1.1
+dhcp_lease_time=86400
+network_number=192.168.1.0
+domain_name=hsd1.mn.comcast.net.
+ip_address=192.168.1.119
+dhcp_message_type=5
+dhcp_server_identifier=192.168.1.1
+routers=192.168.1.1
+broadcast_address=192.168.1.255
+subnet_mask=255.255.255.0
+expiry=1304349405
+
+[proxy]
+pac-url=http://networkmanager.com/proxy.pac
+pac-script="function FindProxyForURL (url, host) {}"
+
+[ip4]
+addresses=192.168.1.119/24 192.168.1.1
+nameservers=68.87.77.134 68.87.72.134 192.168.1.1
+domains=hsd1.mn.comcast.net.
+
+[env]
+PATH=
+NM_DISPATCHER_ACTION=vpn-up
+CONNECTION_UUID=355653c0-34d3-4777-ad25-f9a498b7ef8e
+CONNECTION_DBUS_PATH=/org/freedesktop/NetworkManager/Connections/5
+CONNECTION_ID=Random Connection
+CONNECTION_FILENAME=/src/nm-dispatcher/tests/dispatcher-vpn-up
+DEVICE_IFACE=wlan0
+DEVICE_IP_IFACE=tun0
+PROXY_PAC_URL=http://networkmanager.com/proxy.pac
+PROXY_PAC_SCRIPT="function FindProxyForURL (url, host) {}"
+IP4_ADDRESS_0=192.168.1.119/24 192.168.1.1
+IP4_NUM_ADDRESSES=1
+IP4_NAMESERVERS=68.87.77.134 68.87.72.134 192.168.1.1
+IP4_GATEWAY=192.168.1.1
+IP4_DOMAINS=hsd1.mn.comcast.net.
+IP4_NUM_ROUTES=0
+DHCP4_NETBIOS_NAME_SERVERS=0.0.0.0
+DHCP4_DOMAIN_NAME_SERVERS=68.87.77.134 68.87.72.134 192.168.1.1
+DHCP4_DHCP_LEASE_TIME=86400
+DHCP4_NETWORK_NUMBER=192.168.1.0
+DHCP4_DOMAIN_NAME=hsd1.mn.comcast.net.
+DHCP4_IP_ADDRESS=192.168.1.119
+DHCP4_DHCP_MESSAGE_TYPE=5
+DHCP4_DHCP_SERVER_IDENTIFIER=192.168.1.1
+DHCP4_ROUTERS=192.168.1.1
+DHCP4_BROADCAST_ADDRESS=192.168.1.255
+DHCP4_SUBNET_MASK=255.255.255.0
+DHCP4_EXPIRY=1304349405
diff --git a/src/nm-dispatcher/tests/meson.build b/src/nm-dispatcher/tests/meson.build
new file mode 100644
index 00000000..ac96ef92
--- /dev/null
+++ b/src/nm-dispatcher/tests/meson.build
@@ -0,0 +1,26 @@
+# SPDX-License-Identifier: LGPL-2.1-or-later
+
+exe = executable(
+  'test-dispatcher-envp',
+  [
+    'test-dispatcher-envp.c',
+    dispatcher_nmdbus_dispatcher_sources,
+  ],
+  dependencies: [
+    libnm_dep,
+    glib_dep,
+  ],
+  c_args: introspection_extra_cflags,
+  link_with: [
+    libnm_dispatcher_core,
+    libnm_glib_aux,
+    libnm_std_aux,
+    libc_siphash,
+  ],
+)
+
+test(
+  'src/nm-dispatcher/tests/test-dispatcher-envp',
+  test_script,
+  args: test_args + [exe.full_path()],
+)
diff --git a/src/nm-dispatcher/tests/test-dispatcher-envp.c b/src/nm-dispatcher/tests/test-dispatcher-envp.c
new file mode 100644
index 00000000..297cfbbd
--- /dev/null
+++ b/src/nm-dispatcher/tests/test-dispatcher-envp.c
@@ -0,0 +1,645 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2011 Red Hat, Inc.
+ */
+
+#include "libnm-client-aux-extern/nm-default-client.h"
+
+#include <arpa/inet.h>
+#include <stdlib.h>
+
+#include "nm-dispatcher/nm-dispatcher-utils.h"
+#include "libnm-core-aux-extern/nm-dispatcher-api.h"
+
+#include "libnm-glib-aux/nm-test-utils.h"
+
+#include "nm-dispatcher/nmdbus-dispatcher.h"
+
+#define TEST_DIR NM_BUILD_SRCDIR "/src/nm-dispatcher/tests"
+
+/*****************************************************************************/
+
+static void
+_print_env(const char *const *denv, GHashTable *expected_env)
+{
+    const char *const *iter;
+    GHashTableIter     k;
+    const char *       key;
+
+    g_print("\n******* Generated environment:\n");
+    for (iter = denv; iter && *iter; iter++)
+        g_print("   %s\n", *iter);
+
+    g_print("\n******* Expected environment:\n");
+    g_hash_table_iter_init(&k, expected_env);
+    while (g_hash_table_iter_next(&k, (gpointer) &key, NULL))
+        g_print("   %s\n", key);
+}
+
+static gboolean
+parse_main(GKeyFile *  kf,
+           const char *filename,
+           GVariant ** out_con_dict,
+           GVariant ** out_con_props,
+           char **     out_expected_iface,
+           char **     out_action,
+           char **     out_connectivity_state,
+           char **     out_vpn_ip_iface,
+           GError **   error)
+{
+    nm_auto_clear_variant_builder GVariantBuilder props = {};
+    gs_free char *                                uuid  = NULL;
+    gs_free char *                                id    = NULL;
+    gs_unref_object NMConnection *connection            = NULL;
+    NMSettingConnection *         s_con;
+    const char *                  s;
+
+    *out_expected_iface = g_key_file_get_string(kf, "main", "expected-iface", NULL);
+
+    *out_connectivity_state = g_key_file_get_string(kf, "main", "connectivity-state", NULL);
+    *out_vpn_ip_iface       = g_key_file_get_string(kf, "main", "vpn-ip-iface", NULL);
+
+    *out_action = g_key_file_get_string(kf, "main", "action", error);
+    if (*out_action == NULL)
+        return FALSE;
+
+    uuid = g_key_file_get_string(kf, "main", "uuid", error);
+    if (uuid == NULL)
+        return FALSE;
+    id = g_key_file_get_string(kf, "main", "id", error);
+    if (id == NULL)
+        return FALSE;
+
+    connection = nm_simple_connection_new();
+    s_con      = (NMSettingConnection *) nm_setting_connection_new();
+    g_object_set(s_con, NM_SETTING_CONNECTION_UUID, uuid, NM_SETTING_CONNECTION_ID, id, NULL);
+    nm_connection_add_setting(connection, NM_SETTING(s_con));
+
+    *out_con_dict = nm_connection_to_dbus(connection, NM_CONNECTION_SERIALIZE_ALL);
+
+    g_variant_builder_init(&props, G_VARIANT_TYPE("a{sv}"));
+    g_variant_builder_add(
+        &props,
+        "{sv}",
+        NMD_CONNECTION_PROPS_PATH,
+        g_variant_new_object_path("/org/freedesktop/NetworkManager/Connections/5"));
+
+    /* Strip out the non-fixed portion of the filename */
+    s        = filename;
+    filename = NULL;
+    while ((s = strstr(s, "/src/nm-dispatcher"))) {
+        filename = s;
+        s += 1;
+    }
+    g_assert(filename);
+    g_assert(g_str_has_prefix(filename, "/src/nm-dispatcher"));
+    g_variant_builder_add(&props, "{sv}", "filename", g_variant_new_string(filename));
+
+    if (g_key_file_get_boolean(kf, "main", "external", NULL)) {
+        g_variant_builder_add(&props, "{sv}", "external", g_variant_new_boolean(TRUE));
+    }
+
+    *out_con_props = g_variant_builder_end(&props);
+
+    return TRUE;
+}
+
+static gboolean
+parse_device(GKeyFile *kf, GVariant **out_device_props, GError **error)
+{
+    nm_auto_clear_variant_builder GVariantBuilder props = {};
+    gs_free char *                                tmp   = NULL;
+    int                                           i;
+
+    g_variant_builder_init(&props, G_VARIANT_TYPE("a{sv}"));
+
+    i = g_key_file_get_integer(kf, "device", "state", error);
+    if (i == 0)
+        return FALSE;
+    g_variant_builder_add(&props, "{sv}", NMD_DEVICE_PROPS_STATE, g_variant_new_uint32(i));
+
+    i = g_key_file_get_integer(kf, "device", "type", error);
+    if (i == 0)
+        return FALSE;
+    g_variant_builder_add(&props, "{sv}", NMD_DEVICE_PROPS_TYPE, g_variant_new_uint32(i));
+
+    tmp = g_key_file_get_string(kf, "device", "interface", error);
+    if (tmp == NULL)
+        return FALSE;
+    g_variant_builder_add(&props, "{sv}", NMD_DEVICE_PROPS_INTERFACE, g_variant_new_string(tmp));
+
+    nm_clear_g_free(&tmp);
+    tmp = g_key_file_get_string(kf, "device", "ip-interface", error);
+    if (tmp == NULL)
+        return FALSE;
+    g_variant_builder_add(&props, "{sv}", NMD_DEVICE_PROPS_IP_INTERFACE, g_variant_new_string(tmp));
+
+    nm_clear_g_free(&tmp);
+    tmp = g_key_file_get_string(kf, "device", "path", error);
+    if (tmp == NULL)
+        return FALSE;
+    g_variant_builder_add(&props, "{sv}", NMD_DEVICE_PROPS_PATH, g_variant_new_object_path(tmp));
+
+    *out_device_props = g_variant_builder_end(&props);
+    return TRUE;
+}
+
+static gboolean
+add_uint_array(GKeyFile *       kf,
+               GVariantBuilder *props,
+               const char *     section,
+               const char *     key,
+               GError **        error)
+{
+    gs_free char *       tmp   = NULL;
+    gs_free const char **split = NULL;
+    gsize                i;
+
+    tmp = g_key_file_get_string(kf, section, key, NULL);
+    if (tmp == NULL)
+        return TRUE;
+
+    split = nm_utils_strsplit_set_with_empty(tmp, " ");
+    if (split) {
+        gs_unref_array GArray *items = NULL;
+
+        items = g_array_sized_new(FALSE, TRUE, sizeof(guint32), NM_PTRARRAY_LEN(split));
+        for (i = 0; split[i]; i++) {
+            const char *s;
+
+            s = split[i];
+            g_strstrip((char *) s);
+            if (s[0]) {
+                guint32 addr;
+
+                if (inet_pton(AF_INET, s, &addr) != 1)
+                    g_assert_not_reached();
+                g_array_append_val(items, addr);
+            }
+        }
+        g_variant_builder_add(props,
+                              "{sv}",
+                              key,
+                              g_variant_new_fixed_array(G_VARIANT_TYPE_UINT32,
+                                                        items->data,
+                                                        items->len,
+                                                        sizeof(guint32)));
+    }
+
+    return TRUE;
+}
+
+static gboolean
+parse_proxy(GKeyFile *kf, GVariant **out_props, const char *section, GError **error)
+{
+    nm_auto_clear_variant_builder GVariantBuilder props = {};
+    gs_free char *                                tmp   = NULL;
+
+    g_variant_builder_init(&props, G_VARIANT_TYPE("a{sv}"));
+
+    tmp = g_key_file_get_string(kf, section, "pac-url", error);
+    if (tmp == NULL)
+        return FALSE;
+    g_variant_builder_add(&props, "{sv}", "pac-url", g_variant_new_string(tmp));
+
+    nm_clear_g_free(&tmp);
+    tmp = g_key_file_get_string(kf, section, "pac-script", error);
+    if (tmp == NULL)
+        return FALSE;
+    g_variant_builder_add(&props, "{sv}", "pac-script", g_variant_new_string(tmp));
+
+    *out_props = g_variant_builder_end(&props);
+    return TRUE;
+}
+
+static gboolean
+parse_ip4(GKeyFile *kf, GVariant **out_props, const char *section, GError **error)
+{
+    nm_auto_clear_variant_builder GVariantBuilder props = {};
+    gs_free char *                                tmp   = NULL;
+    gs_free const char **                         split = NULL;
+    const char **                                 iter;
+
+    g_variant_builder_init(&props, G_VARIANT_TYPE("a{sv}"));
+
+    /* search domains */
+    /* Use char** for domains. (DBUS_TYPE_G_ARRAY_OF_STRING of NMIP4Config
+     * becomes G_TYPE_STRV when sending the value over D-Bus)
+     */
+    tmp = g_key_file_get_string(kf, section, "domains", error);
+    if (tmp == NULL)
+        return FALSE;
+    split = nm_utils_strsplit_set_with_empty(tmp, " ");
+    if (split) {
+        for (iter = split; *iter; iter++)
+            g_strstrip((char *) *iter);
+        g_variant_builder_add(&props, "{sv}", "domains", g_variant_new_strv((gpointer) split, -1));
+    }
+    nm_clear_g_free(&split);
+
+    if (!add_uint_array(kf, &props, "ip4", "nameservers", error))
+        return FALSE;
+
+    if (!add_uint_array(kf, &props, "ip4", "wins-servers", error))
+        return FALSE;
+
+    nm_clear_g_free(&tmp);
+    tmp = g_key_file_get_string(kf, section, "addresses", error);
+    if (tmp == NULL)
+        return FALSE;
+    split = nm_utils_strsplit_set_with_empty(tmp, ",");
+    if (split) {
+        gs_unref_ptrarray GPtrArray *addresses = NULL;
+        const char *                 gateway   = NULL;
+
+        addresses = g_ptr_array_new_with_free_func((GDestroyNotify) nm_ip_address_unref);
+        for (iter = split; *iter; iter++) {
+            const char * s = *iter;
+            NMIPAddress *addr;
+            const char * ip;
+            const char * prefix;
+
+            g_strstrip((char *) s);
+            if (s[0] == '\0')
+                continue;
+
+            ip = *iter;
+
+            prefix = strchr(ip, '/');
+            g_assert(prefix);
+            ((char *) (prefix++))[0] = '\0';
+
+            if (addresses->len == 0) {
+                gateway = strchr(prefix, ' ');
+                g_assert(gateway);
+                gateway++;
+            }
+
+            addr = nm_ip_address_new(AF_INET, ip, (guint) atoi(prefix), error);
+            if (!addr)
+                return FALSE;
+
+            g_ptr_array_add(addresses, addr);
+        }
+
+        g_variant_builder_add(&props,
+                              "{sv}",
+                              "addresses",
+                              nm_utils_ip4_addresses_to_variant(addresses, gateway));
+    }
+    nm_clear_g_free(&split);
+
+    nm_clear_g_free(&tmp);
+    tmp   = g_key_file_get_string(kf, section, "routes", NULL);
+    split = nm_utils_strsplit_set_with_empty(tmp, ",");
+    if (split) {
+        gs_unref_ptrarray GPtrArray *routes = NULL;
+
+        routes = g_ptr_array_new_with_free_func((GDestroyNotify) nm_ip_route_unref);
+        for (iter = split; *iter; iter++) {
+            const char *s = *iter;
+            NMIPRoute * route;
+            const char *dest;
+            const char *prefix;
+            const char *next_hop;
+            const char *metric;
+
+            g_strstrip((char *) s);
+            if (s[0] == '\0')
+                continue;
+
+            dest = s;
+
+            prefix = strchr(dest, '/');
+            g_assert(prefix);
+            ((char *) (prefix++))[0] = '\0';
+
+            next_hop = strchr(prefix, ' ');
+            g_assert(next_hop);
+            ((char *) (next_hop++))[0] = '\0';
+
+            metric = strchr(next_hop, ' ');
+            g_assert(metric);
+            ((char *) (metric++))[0] = '\0';
+
+            route = nm_ip_route_new(AF_INET,
+                                    dest,
+                                    _nm_utils_ascii_str_to_int64(prefix, 10, 0, 32, 255),
+                                    next_hop,
+                                    (guint) atoi(metric),
+                                    error);
+            if (!route)
+                return FALSE;
+            g_ptr_array_add(routes, route);
+        }
+
+        g_variant_builder_add(&props, "{sv}", "routes", nm_utils_ip4_routes_to_variant(routes));
+    }
+
+    *out_props = g_variant_builder_end(&props);
+    return TRUE;
+}
+
+static gboolean
+parse_dhcp(GKeyFile *kf, const char *group_name, GVariant **out_props, GError **error)
+{
+    nm_auto_clear_variant_builder GVariantBuilder props = {};
+    gs_strfreev char **                           keys  = NULL;
+    char **                                       iter;
+
+    keys = g_key_file_get_keys(kf, group_name, NULL, error);
+    if (!keys)
+        return FALSE;
+
+    g_variant_builder_init(&props, G_VARIANT_TYPE("a{sv}"));
+    for (iter = keys; iter && *iter; iter++) {
+        gs_free char *val = NULL;
+
+        val = g_key_file_get_string(kf, group_name, *iter, error);
+        if (!val)
+            return FALSE;
+        g_variant_builder_add(&props, "{sv}", *iter, g_variant_new_string(val));
+    }
+
+    *out_props = g_variant_builder_end(&props);
+    return TRUE;
+}
+
+static gboolean
+get_dispatcher_file(const char * file,
+                    GVariant **  out_con_dict,
+                    GVariant **  out_con_props,
+                    GVariant **  out_device_props,
+                    GVariant **  out_device_proxy_props,
+                    GVariant **  out_device_ip4_props,
+                    GVariant **  out_device_ip6_props,
+                    GVariant **  out_device_dhcp4_props,
+                    GVariant **  out_device_dhcp6_props,
+                    char **      out_connectivity_state,
+                    char **      out_vpn_ip_iface,
+                    GVariant **  out_vpn_proxy_props,
+                    GVariant **  out_vpn_ip4_props,
+                    GVariant **  out_vpn_ip6_props,
+                    char **      out_expected_iface,
+                    char **      out_action,
+                    GHashTable **out_env,
+                    GError **    error)
+{
+    nm_auto_unref_keyfile GKeyFile *kf   = NULL;
+    gs_strfreev char **             keys = NULL;
+    char **                         iter;
+
+    g_assert(!error || !*error);
+    g_assert(out_con_dict && !*out_con_dict);
+    g_assert(out_con_props && !*out_con_props);
+    g_assert(out_device_props && !*out_device_props);
+    g_assert(out_device_proxy_props && !*out_device_proxy_props);
+    g_assert(out_device_ip4_props && !*out_device_ip4_props);
+    g_assert(out_device_ip6_props && !*out_device_ip6_props);
+    g_assert(out_device_dhcp4_props && !*out_device_dhcp4_props);
+    g_assert(out_device_dhcp6_props && !*out_device_dhcp6_props);
+    g_assert(out_connectivity_state && !*out_connectivity_state);
+    g_assert(out_vpn_ip_iface && !*out_vpn_ip_iface);
+    g_assert(out_vpn_proxy_props && !*out_vpn_proxy_props);
+    g_assert(out_vpn_ip4_props && !*out_vpn_ip4_props);
+    g_assert(out_vpn_ip6_props && !*out_vpn_ip6_props);
+    g_assert(out_expected_iface && !*out_expected_iface);
+    g_assert(out_action && !*out_action);
+    g_assert(out_env && !*out_env);
+
+    kf = g_key_file_new();
+    if (!g_key_file_load_from_file(kf, file, G_KEY_FILE_NONE, error))
+        return FALSE;
+
+    if (!parse_main(kf,
+                    file,
+                    out_con_dict,
+                    out_con_props,
+                    out_expected_iface,
+                    out_action,
+                    out_connectivity_state,
+                    out_vpn_ip_iface,
+                    error))
+        return FALSE;
+
+    if (!parse_device(kf, out_device_props, error))
+        return FALSE;
+
+    if (g_key_file_has_group(kf, "proxy")) {
+        if (!parse_proxy(kf, out_device_proxy_props, "proxy", error))
+            return FALSE;
+    }
+
+    if (g_key_file_has_group(kf, "ip4")) {
+        if (!parse_ip4(kf, out_device_ip4_props, "ip4", error))
+            return FALSE;
+    }
+
+    if (g_key_file_has_group(kf, "dhcp4")) {
+        if (!parse_dhcp(kf, "dhcp4", out_device_dhcp4_props, error))
+            return FALSE;
+    }
+
+    if (g_key_file_has_group(kf, "dhcp6")) {
+        if (!parse_dhcp(kf, "dhcp6", out_device_dhcp6_props, error))
+            return FALSE;
+    }
+
+    g_assert(g_key_file_has_group(kf, "env"));
+    keys     = g_key_file_get_keys(kf, "env", NULL, error);
+    *out_env = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL);
+    for (iter = keys; iter && *iter; iter++) {
+        gs_free char *val = NULL;
+
+        val = g_key_file_get_string(kf, "env", *iter, error);
+        if (!val)
+            return FALSE;
+        g_hash_table_insert(*out_env, g_strdup_printf("%s=%s", *iter, val), GUINT_TO_POINTER(1));
+    }
+
+    return TRUE;
+}
+
+/*****************************************************************************/
+
+static void
+test_generic(const char *file, const char *override_vpn_ip_iface)
+{
+    gs_unref_variant GVariant *con_dict            = NULL;
+    gs_unref_variant GVariant *con_props           = NULL;
+    gs_unref_variant GVariant *device_props        = NULL;
+    gs_unref_variant GVariant *device_proxy_props  = NULL;
+    gs_unref_variant GVariant *device_ip4_props    = NULL;
+    gs_unref_variant GVariant *device_ip6_props    = NULL;
+    gs_unref_variant GVariant *device_dhcp4_props  = NULL;
+    gs_unref_variant GVariant *device_dhcp6_props  = NULL;
+    gs_free char *             connectivity_change = NULL;
+    gs_free char *             vpn_ip_iface        = NULL;
+    gs_unref_variant GVariant *vpn_proxy_props     = NULL;
+    gs_unref_variant GVariant *vpn_ip4_props       = NULL;
+    gs_unref_variant GVariant *vpn_ip6_props       = NULL;
+    gs_free char *             expected_iface      = NULL;
+    gs_free char *             action              = NULL;
+    gs_free char *             out_iface           = NULL;
+    const char *               error_message       = NULL;
+    gs_unref_hashtable GHashTable *expected_env    = NULL;
+    GError *                       error           = NULL;
+    gboolean                       success;
+    gs_free char *                 filename = NULL;
+    gs_strfreev char **            denv     = NULL;
+    char **                        iter;
+
+    filename = g_build_filename(TEST_DIR, file, NULL);
+    success  = get_dispatcher_file(filename,
+                                  &con_dict,
+                                  &con_props,
+                                  &device_props,
+                                  &device_proxy_props,
+                                  &device_ip4_props,
+                                  &device_ip6_props,
+                                  &device_dhcp4_props,
+                                  &device_dhcp6_props,
+                                  &connectivity_change,
+                                  &vpn_ip_iface,
+                                  &vpn_proxy_props,
+                                  &vpn_ip4_props,
+                                  &vpn_ip6_props,
+                                  &expected_iface,
+                                  &action,
+                                  &expected_env,
+                                  &error);
+    nmtst_assert_success(success, error);
+
+    /* Get the environment from the dispatcher code */
+    denv = nm_dispatcher_utils_construct_envp(action,
+                                              con_dict,
+                                              con_props,
+                                              device_props,
+                                              device_proxy_props,
+                                              device_ip4_props,
+                                              device_ip6_props,
+                                              device_dhcp4_props,
+                                              device_dhcp6_props,
+                                              connectivity_change,
+                                              override_vpn_ip_iface ?: vpn_ip_iface,
+                                              vpn_proxy_props,
+                                              vpn_ip4_props,
+                                              vpn_ip6_props,
+                                              &out_iface,
+                                              &error_message);
+
+    g_assert((!denv && error_message) || (denv && !error_message));
+
+    if (error_message)
+        g_error("FAILED: %s", error_message);
+
+    if (g_strv_length(denv) != g_hash_table_size(expected_env)) {
+        _print_env(NM_CAST_STRV_CC(denv), expected_env);
+        g_assert_cmpint(g_strv_length(denv), ==, g_hash_table_size(expected_env));
+    }
+
+    /* Compare dispatcher generated env and expected env */
+    for (iter = denv; iter && *iter; iter++) {
+        gpointer    foo;
+        const char *i_value = *iter;
+
+        if (strstr(i_value, "PATH=") == i_value) {
+            g_assert_cmpstr(&i_value[strlen("PATH=")], ==, g_getenv("PATH"));
+
+            /* The path is constructed dynamically. Ignore the actual value. */
+            i_value = "PATH=";
+        }
+
+        foo = g_hash_table_lookup(expected_env, i_value);
+        if (!foo) {
+            _print_env(NM_CAST_STRV_CC(denv), expected_env);
+            g_error("Failed to find %s in environment", i_value);
+        }
+    }
+
+    g_assert_cmpstr(expected_iface, ==, out_iface);
+}
+
+/*****************************************************************************/
+
+static void
+test_up(void)
+{
+    test_generic("dispatcher-up", NULL);
+}
+
+static void
+test_down(void)
+{
+    test_generic("dispatcher-down", NULL);
+}
+
+static void
+test_vpn_up(void)
+{
+    test_generic("dispatcher-vpn-up", NULL);
+}
+
+static void
+test_vpn_down(void)
+{
+    test_generic("dispatcher-vpn-down", NULL);
+}
+
+static void
+test_external(void)
+{
+    test_generic("dispatcher-external", NULL);
+}
+
+static void
+test_connectivity_changed(void)
+{
+    /* These tests will check that the CONNECTIVITY_STATE environment
+     * variable is only defined for known states, such as 'full'. */
+    test_generic("dispatcher-connectivity-unknown", NULL);
+    test_generic("dispatcher-connectivity-full", NULL);
+}
+
+static void
+test_up_empty_vpn_iface(void)
+{
+    /* Test that an empty VPN iface variable, like is passed through D-Bus
+     * from NM, is ignored by the dispatcher environment construction code.
+     */
+    test_generic("dispatcher-up", "");
+}
+
+/*****************************************************************************/
+
+static void
+test_gdbus_codegen(void)
+{
+    gs_unref_object NMDBusDispatcher *dbus_dispatcher = NULL;
+
+    dbus_dispatcher = nmdbus_dispatcher_skeleton_new();
+    g_assert(NMDBUS_IS_DISPATCHER_SKELETON(dbus_dispatcher));
+}
+
+/*****************************************************************************/
+
+NMTST_DEFINE();
+
+int
+main(int argc, char **argv)
+{
+    nmtst_init(&argc, &argv, TRUE);
+
+    g_test_add_func("/dispatcher/up", test_up);
+    g_test_add_func("/dispatcher/down", test_down);
+    g_test_add_func("/dispatcher/vpn_up", test_vpn_up);
+    g_test_add_func("/dispatcher/vpn_down", test_vpn_down);
+    g_test_add_func("/dispatcher/external", test_external);
+    g_test_add_func("/dispatcher/connectivity_changed", test_connectivity_changed);
+
+    g_test_add_func("/dispatcher/up_empty_vpn_iface", test_up_empty_vpn_iface);
+
+    g_test_add_func("/dispatcher/gdbus-codegen", test_gdbus_codegen);
+
+    return g_test_run();
+}