From bf03f1a74f975afe3a68fab062576c97604eecf0 Mon Sep 17 00:00:00 2001 From: Isaac Yang <47034756+seankingyang@users.noreply.github.com> Date: Fri, 13 Sep 2024 19:19:15 +0800 Subject: [PATCH] Fix wifi nmcli backup/restore /delete on 24.04 Desktop (BugFix) (#1324) * Correct the backup/restore from/to the correct path * Fix the delete nmcli will make the all connection will be disappeared * minor changes * remove the shell=True in subprocess * Fix some minor bug * Fix typo, some missings, and Stanley's advices * Update providers/base/tests/test_wifi_nmcli_test.py Simplify the unit test Co-authored-by: Fernando Bravo <39527354+fernando79513@users.noreply.github.com> * Update providers/base/bin/wifi_nmcli_test.py Fix typo Co-authored-by: Fernando Bravo <39527354+fernando79513@users.noreply.github.com> * Update providers/base/tests/test_wifi_nmcli_test.py Simplify the unit test Co-authored-by: Fernando Bravo <39527354+fernando79513@users.noreply.github.com> * Update providers/base/tests/test_wifi_nmcli_test.py Simplify the unit test Co-authored-by: Fernando Bravo <39527354+fernando79513@users.noreply.github.com> * Update providers/base/bin/wifi_nmcli_test.py Only catch the sp.CalledProcessError Co-authored-by: Fernando Bravo <39527354+fernando79513@users.noreply.github.com> * fix backup script not use the os.path join but Path * Fix test_wifi_nmcli_test.py * Tiny fix... * Fix the unittest parameter order --------- Co-authored-by: Fernando Bravo <39527354+fernando79513@users.noreply.github.com> --- providers/base/bin/wifi_nmcli_backup.py | 36 +- providers/base/bin/wifi_nmcli_test.py | 218 ++++--- .../base/tests/test_wifi_nmcli_backup.py | 125 +++- providers/base/tests/test_wifi_nmcli_test.py | 540 +++++++++++++++++- 4 files changed, 828 insertions(+), 91 deletions(-) diff --git a/providers/base/bin/wifi_nmcli_backup.py b/providers/base/bin/wifi_nmcli_backup.py index e2c2dcf02b..22d8e64427 100755 --- a/providers/base/bin/wifi_nmcli_backup.py +++ b/providers/base/bin/wifi_nmcli_backup.py @@ -11,6 +11,8 @@ import shutil import subprocess as sp import sys +import glob +from pathlib import Path from packaging import version as version_parser @@ -62,37 +64,41 @@ def reload_nm_connections(): def save_connections(keyfile_list): - if not os.path.exists(SAVE_DIR): - os.makedirs(SAVE_DIR) - if len(keyfile_list) == 0: + os.makedirs(SAVE_DIR, exist_ok=True) + + if not keyfile_list: print("No stored 802.11 connections to save") return + for f in keyfile_list: print("Save connection {}".format(f)) + if not os.path.exists(f): - print(" No stored connection fount at {}".format(f)) + print(" No stored connection found at {}".format(f)) continue + print(" Found file {}".format(f)) - save_f = shutil.copy(f, SAVE_DIR) + basedir = Path(f).parent + backup_loc = SAVE_DIR / basedir + + os.makedirs(backup_loc, exist_ok=True) + save_f = shutil.copy(f, backup_loc) print(" Saved copy at {}".format(save_f)) def restore_connections(): - saved_list = [ - f - for f in os.listdir(SAVE_DIR) - if os.path.isfile(os.path.join(SAVE_DIR, f)) - ] + saved_list = glob.glob( + "{}/**/*.nmconnection".format(SAVE_DIR), recursive=True + ) if len(saved_list) == 0: print("No stored 802.11 connections found") return for f in saved_list: - save_f = os.path.join(SAVE_DIR, f) + f_path = Path(f) + save_f = f_path.relative_to(SAVE_DIR) + dest_path = Path("/") / save_f print("Restore connection {}".format(save_f)) - restore_f = shutil.copy(save_f, NM_CON_DIR) - print(" Restored file at {}".format(restore_f)) - os.remove(save_f) - print(" Removed copy from {}".format(save_f)) + shutil.move(f, dest_path) if __name__ == "__main__": diff --git a/providers/base/bin/wifi_nmcli_test.py b/providers/base/bin/wifi_nmcli_test.py index 0f735c5a97..d4b2981fc9 100755 --- a/providers/base/bin/wifi_nmcli_test.py +++ b/providers/base/bin/wifi_nmcli_test.py @@ -16,6 +16,7 @@ import subprocess as sp import sys import time +import shlex from packaging import version as version_parser @@ -27,7 +28,7 @@ def legacy_nmcli(): cmd = "nmcli -v" - output = sp.check_output(cmd, shell=True) + output = sp.check_output(shlex.split(cmd)) version = version_parser.parse(output.strip().split()[-1].decode()) # check if using the 16.04 nmcli because of this bug # https://bugs.launchpad.net/plano/+bug/1896806 @@ -42,26 +43,82 @@ def print_cmd(cmd): print("+", cmd) -def cleanup_nm_connections(): - print_head("Cleaning up NM connections") - cmd = "nmcli -t -f TYPE,UUID,NAME c" +def _get_nm_wireless_connections(): + cmd = "nmcli -t -f TYPE,UUID,NAME,STATE connection" print_cmd(cmd) - output = sp.check_output(cmd, shell=True) + output = sp.check_output(shlex.split(cmd)) + connections = {} for line in output.decode(sys.stdout.encoding).splitlines(): - type, uuid, name = line.strip().split(":", 2) + type, uuid, name, state = line.strip().split(":", 3) if type == "802-11-wireless": - print("Deleting connection", name) - cmd = "nmcli c delete {}".format(uuid) + connections[name] = {"uuid": uuid, "state": state} + return connections + + +def get_nm_activate_connection(): + print_head("Get NM activate connection name") + connections = _get_nm_wireless_connections() + for name, value in connections.items(): + state = value["state"] + uuid = value["uuid"] + if state == "activated": + print("Activated Connection: {} {}".format(name, uuid)) + return uuid + return "" + + +def turn_up_connection(uuid): + # uuid can also be connection name + print_head("Turn up NM connection") + cmd = "nmcli c up {}".format(uuid) + print("Turn up {}".format(uuid)) + activate_uuid = get_nm_activate_connection() + if uuid == activate_uuid: + print("{} state is already activated".format(uuid)) + return None + try: + print_cmd(cmd) + sp.call(shlex.split(cmd)) + except Exception as e: + print("Can't turn on {}: {}".format(uuid, str(e))) + + +def turn_down_nm_connections(): + print_head("Turn off NM all connections") + connections = _get_nm_wireless_connections() + for name, value in connections.items(): + uuid = value["uuid"] + print("Turn down connection", name) + try: + cmd = "nmcli c down {}".format(uuid) print_cmd(cmd) - sp.call(cmd, shell=True) + sp.call(shlex.split(cmd)) + print("{} {} is down now".format(name, uuid)) + except sp.CalledProcessError as e: + print("Can't down {}: {}".format(uuid, str(e))) print() +def delete_test_ap_ssid_connection(): + print_head("Cleaning up TEST_CON connection") + connections = _get_nm_wireless_connections() + if "TEST_CON" not in connections: + print("No TEST_CON connection found, nothing to delete") + return + try: + cmd = "nmcli c delete TEST_CON" + print_cmd(cmd) + sp.call(shlex.split(cmd)) + print("TEST_CON is deleted") + except Exception as e: + print("Can't delete TEST_CON : {}".format(str(e))) + + def device_rescan(): print_head("Calling a rescan") cmd = "nmcli d wifi rescan" print_cmd(cmd) - retcode = sp.call(cmd, shell=True) + retcode = sp.call(shlex.split(cmd)) if retcode != 0: # Most often the rescan request fails because NM has itself started # a scan in recent past, we should let these operations complete before @@ -71,43 +128,47 @@ def device_rescan(): print() -def list_aps(args): - print_head("List APs") - count = 0 +def list_aps(ifname, essid=None): + if essid: + print_head("List APs with ESSID: {}".format(essid)) + else: + print("List all APs") + aps_dict = {} fields = "SSID,CHAN,FREQ,SIGNAL" - cmd = "nmcli -t -f {} d wifi list ifname {}".format(fields, args.device) - print_cmd(cmd) - output = sp.check_output(cmd, shell=True) + cmd = "nmcli -t -f {} d wifi list ifname {}".format(fields, ifname) + output = sp.check_output(shlex.split(cmd)) for line in output.decode(sys.stdout.encoding).splitlines(): # lp bug #1723372 - extra line in output on zesty - if line.strip() == args.device: + if line.strip() == ifname: # Skip device name line continue ssid, channel, frequency, signal = line.strip().rsplit(":", 3) + if essid and ssid != essid: + continue + aps_dict[ssid] = {"Chan": channel, "Freq": frequency, "Signal": signal} + return aps_dict + + +def show_aps(aps_dict): + for ssid, values in aps_dict.items(): print( "SSID: {} Chan: {} Freq: {} Signal: {}".format( - ssid, channel, frequency, signal + ssid, values["Chan"], values["Freq"], values["Signal"] ) ) - if hasattr(args, "essid"): - if ssid == args.essid: - count += 1 - else: - count += 1 print() - return count def print_address_info(interface): cmd = "ip address show dev {}".format(interface) print_cmd(cmd) - sp.call(cmd, shell=True) + sp.call(shlex.split(cmd)) print() def print_route_info(): cmd = "ip route" print_cmd(cmd) - sp.call(cmd, shell=True) + sp.call(shlex.split(cmd)) print() @@ -115,7 +176,7 @@ def perform_ping_test(interface): target = None cmd = "nmcli --mode tabular --terse --fields IP4.GATEWAY c show TEST_CON" print_cmd(cmd) - output = sp.check_output(cmd, shell=True) + output = sp.check_output(shlex.split(cmd)) target = output.decode(sys.stdout.encoding).strip() print("Got gateway address: {}".format(target)) @@ -128,27 +189,40 @@ def perform_ping_test(interface): return False -def wait_for_connected(interface, max_wait=5): +def wait_for_connected(interface, essid, max_wait=5): connected = False attempts = 0 while not connected and attempts < max_wait: - cmd = "nmcli -m tabular -t -f GENERAL.STATE d show {}".format( - args.device + cmd = ( + "nmcli -m tabular -t -f GENERAL.STATE,GENERAL.CONNECTION " + "d show {}".format(interface) ) print_cmd(cmd) - output = sp.check_output(cmd, shell=True) - state = output.decode(sys.stdout.encoding).strip() - print(state) + output = sp.check_output(shlex.split(cmd)) + state, ssid = output.decode(sys.stdout.encoding).strip().splitlines() - if state.startswith("100"): + if state.startswith("100") and ssid == essid: connected = True break + time.sleep(1) attempts += 1 + if connected: - print("Reached connected state") + print("Reached connected state with ESSID: {}".format(essid)) else: - print("ERROR: did not reach connected state") + print( + "ERROR: did not reach connected state with ESSID: {}".format(essid) + ) + if ssid != essid: + print( + "ESSID mismatch:\n Excepted:{}\n Actually:{}".format( + ssid, essid + ) + ) + if not state.startswith("100"): + print("State is not connected: {}".format(state)) + print() return connected @@ -171,19 +245,13 @@ def open_connection(args): "ipv6.method ignore".format(args.device, args.essid) ) print_cmd(cmd) - sp.call(cmd, shell=True) + sp.call(shlex.split(cmd)) # Make sure the connection is brought up - cmd = "nmcli c up TEST_CON" - print_cmd(cmd) - try: - sp.call(cmd, shell=True, timeout=200 if legacy_nmcli() else None) - except sp.TimeoutExpired: - print("Connection activation failed\n") - print() + turn_up_connection("TEST_CON") print_head("Ensure interface is connected") - reached_connected = wait_for_connected(args.device) + reached_connected = wait_for_connected(args.device, "TEST_CON") rc = 1 if reached_connected: @@ -225,19 +293,13 @@ def secured_connection(args): ) ) print_cmd(cmd) - sp.call(cmd, shell=True) + sp.call(shlex.split(cmd)) # Make sure the connection is brought up - cmd = "nmcli c up TEST_CON" - print_cmd(cmd) - try: - sp.call(cmd, shell=True, timeout=200 if legacy_nmcli() else None) - except sp.TimeoutExpired: - print("Connection activation failed\n") - print() + turn_up_connection("TEST_CON") print_head("Ensure interface is connected") - reached_connected = wait_for_connected(args.device) + reached_connected = wait_for_connected(args.device, "TEST_CON") rc = 1 if reached_connected: @@ -264,7 +326,7 @@ def hotspot(args): " ssid CHECKBOX_AP".format(args.device) ) print_cmd(cmd) - retcode = sp.call(cmd, shell=True) + retcode = sp.call(shlex.split(cmd)) if retcode != 0: print("Connection creation failed\n") return retcode @@ -273,7 +335,7 @@ def hotspot(args): " 802-11-wireless.band {}".format(args.band) ) print_cmd(cmd) - retcode = sp.call(cmd, shell=True) + retcode = sp.call(shlex.split(cmd)) if retcode != 0: print("Set band failed\n") return retcode @@ -282,13 +344,11 @@ def hotspot(args): 'wifi-sec.psk "ubuntu1234"' ) print_cmd(cmd) - retcode = sp.call(cmd, shell=True) + retcode = sp.call(shlex.split(cmd)) if retcode != 0: print("Setting up wifi security failed\n") return retcode - cmd = "nmcli connection up TEST_CON" - print_cmd(cmd) - retcode = sp.call(cmd, shell=True) + turn_up_connection("TEST_CON") if retcode != 0: print("Failed to bring up connection\n") print() @@ -305,10 +365,10 @@ def print_journal_entries(start): '--since "{}" '.format(start.strftime("%Y-%m-%d %H:%M:%S")) ) print_cmd(cmd) - sp.call(cmd, shell=True) + sp.call(shlex.split(cmd)) -if __name__ == "__main__": +def parser_args(): parser = argparse.ArgumentParser( description="WiFi connection test using mmcli" ) @@ -351,29 +411,45 @@ def print_journal_entries(start): args = parser.parse_args() - start_time = datetime.datetime.now() + return args + - cleanup_nm_connections() +def main(): + args = parser_args() + start_time = datetime.datetime.now() device_rescan() - count = list_aps(args) + essid = getattr(args, "essid", None) + aps_dict = list_aps(args.device, essid) + show_aps(aps_dict) if args.test_type == "scan": - if count == 0: + if not aps_dict: print("Failed to find any APs") - sys.exit(1) + return 1 else: - print("Found {} access points".format(count)) - sys.exit(0) + print("Found {} access points".format(len(aps_dict))) + return 0 + + if not aps_dict: + print("Targed access points: {} not found".format(args.essid)) + return 1 if args.func: + delete_test_ap_ssid_connection() + activated_uuid = get_nm_activate_connection() + turn_down_nm_connections() try: result = args.func(args) finally: - cleanup_nm_connections() + turn_up_connection(activated_uuid) + delete_test_ap_ssid_connection() # The test is not required to run as root, but root access is required for # journal access so only attempt to print when e.g. running under Remote if result != 0 and os.geteuid() == 0: print_journal_entries(start_time) + return result - sys.exit(result) + +if __name__ == "__main__": + sys.exit(main()) diff --git a/providers/base/tests/test_wifi_nmcli_backup.py b/providers/base/tests/test_wifi_nmcli_backup.py index 9cb65cfd9a..6c7f26cbd0 100644 --- a/providers/base/tests/test_wifi_nmcli_backup.py +++ b/providers/base/tests/test_wifi_nmcli_backup.py @@ -17,9 +17,14 @@ import unittest -from unittest.mock import patch +from pathlib import Path +from unittest.mock import patch, call, ANY -from wifi_nmcli_backup import legacy_nmcli +from wifi_nmcli_backup import ( + legacy_nmcli, + save_connections, + restore_connections, +) class WifiNmcliBackupTests(unittest.TestCase): @@ -36,3 +41,119 @@ def test_legacy_nmcli_false(self, subprocess_mock): b"nmcli tool, version 1.46.0-2" ) self.assertFalse(legacy_nmcli()) + + @patch("wifi_nmcli_backup.os.makedirs") + @patch("wifi_nmcli_backup.print") + def test_save_connections_empty_list(self, mock_print, mock_makedirs): + save_connections([]) + mock_print.assert_called_once_with( + "No stored 802.11 connections to save" + ) + self.assertEqual(mock_makedirs.call_count, 1) + + @patch("wifi_nmcli_backup.os.makedirs") + @patch("wifi_nmcli_backup.os.path.exists", return_value=True) + def test_save_connections_savedir_exists(self, mock_makedirs, mock_exists): + mock_makedirs.assert_not_called() + + @patch("wifi_nmcli_backup.os.path.exists", return_value=False) + @patch("wifi_nmcli_backup.print") + @patch("wifi_nmcli_backup.os.makedirs") + def test_save_connections_non_existing_files( + self, mock_makedirs, mock_print, mock_exists + ): + keyfile_list = [ + "/fake/path/to/connection1", + "/fake/path/to/connection2", + ] + + save_connections(keyfile_list) + expected_calls = [ + call("Save connection {}".format(f)) for f in keyfile_list + ] + expected_calls += [ + call(" No stored connection found at {}".format(f)) + for f in keyfile_list + ] + mock_print.assert_has_calls(expected_calls, any_order=True) + self.assertEqual(mock_makedirs.call_count, 1) + + @patch( + "wifi_nmcli_backup.shutil.copy", + return_value="/fake/backup/location/connection1", + ) + @patch( + "wifi_nmcli_backup.os.path.exists", + side_effect=lambda path: True if "connection" in path else False, + ) + @patch("wifi_nmcli_backup.print") + @patch("wifi_nmcli_backup.os.makedirs") + def test_save_connections_existing_files( + self, mock_makedirs, mock_print, mock_exists, mock_copy + ): + keyfile_list = ["/etc/NetworkManager/system-connections/connection1"] + save_connections(keyfile_list) + self.assertEqual(mock_makedirs.call_count, 2) + mock_copy.assert_called_once_with( + "/etc/NetworkManager/system-connections/connection1", ANY + ) + + expected_print_calls = [ + call( + "Save connection " + "/etc/NetworkManager/system-connections/connection1" + ), + call( + " Found file " + "/etc/NetworkManager/system-connections/connection1" + ), + call(" Saved copy at /fake/backup/location/connection1"), + ] + mock_print.assert_has_calls(expected_print_calls, any_order=True) + + @patch("wifi_nmcli_backup.print") + @patch("wifi_nmcli_backup.glob.glob", return_value=[]) + def test_restore_connections_no_stored_connections( + self, mock_glob, mock_print + ): + restore_connections() + mock_print.assert_called_once_with( + "No stored 802.11 connections found" + ) + + @patch("wifi_nmcli_backup.SAVE_DIR", "/stored-system-connections") + @patch("wifi_nmcli_backup.shutil.move") + @patch("wifi_nmcli_backup.glob.glob") + @patch("wifi_nmcli_backup.print") + def test_restore_connections_existing_files( + self, mock_print, mock_glob, mock_move + ): + mock_glob.return_value = [ + "/stored-system-connections/etc/NetworkManager/system-connections/" + "connection1.nmconnection", + "/stored-system-connections/run/NetworkManager/system-connections/" + "connection2.nmconnection", + ] + + restore_connections() + + expected_calls = [ + call( + "/stored-system-connections/etc/NetworkManager/" + "system-connections/connection1.nmconnection", + Path( + "/etc/NetworkManager/system-connections/" + "connection1.nmconnection" + ), + ), + call( + "/stored-system-connections/run/NetworkManager/" + "system-connections/connection2.nmconnection", + Path( + "/run/NetworkManager/system-connections/" + "connection2.nmconnection" + ), + ), + ] + + mock_move.assert_has_calls(expected_calls, any_order=True) diff --git a/providers/base/tests/test_wifi_nmcli_test.py b/providers/base/tests/test_wifi_nmcli_test.py index e0b52a3925..6d7d9e3dba 100644 --- a/providers/base/tests/test_wifi_nmcli_test.py +++ b/providers/base/tests/test_wifi_nmcli_test.py @@ -17,9 +17,24 @@ import unittest -from unittest.mock import patch - -from wifi_nmcli_test import legacy_nmcli +from unittest.mock import patch, call, MagicMock +from wifi_nmcli_test import ( + legacy_nmcli, + _get_nm_wireless_connections, + get_nm_activate_connection, + turn_up_connection, + turn_down_nm_connections, + delete_test_ap_ssid_connection, + device_rescan, + list_aps, + show_aps, + wait_for_connected, + open_connection, + secured_connection, + hotspot, + parser_args, + main, +) class WifiNmcliBackupTests(unittest.TestCase): @@ -36,3 +51,522 @@ def test_legacy_nmcli_false(self, subprocess_mock): b"nmcli tool, version 1.46.0-2" ) self.assertFalse(legacy_nmcli()) + + +class TestGetNmWirelessConnections(unittest.TestCase): + @patch("wifi_nmcli_test.sp.check_output", return_value=b"") + def test_no_wireless_connections(self, check_output_mock): + expected = {} + self.assertEqual(_get_nm_wireless_connections(), expected) + + @patch( + "wifi_nmcli_test.sp.check_output", + return_value=( + b"802-11-wireless:uuid1:Wireless1:activated\n" + b"802-3-ethernet:uuid2:Ethernet1:activated\n" + b"802-11-wireless:uuid3:Wireless2:deactivated\n" + ), + ) + def test_multiple_wireless_connections(self, check_output_mock): + expected = { + "Wireless1": {"uuid": "uuid1", "state": "activated"}, + "Wireless2": {"uuid": "uuid3", "state": "deactivated"}, + } + self.assertEqual(_get_nm_wireless_connections(), expected) + + +class TestGetNmActivateConnection(unittest.TestCase): + @patch("wifi_nmcli_test._get_nm_wireless_connections", return_value={}) + def test_no_active_connections(self, _): + self.assertEqual(get_nm_activate_connection(), "") + + @patch( + "wifi_nmcli_test._get_nm_wireless_connections", + return_value={"Wireless1": {"uuid": "uuid1", "state": "activated"}}, + ) + def test_single_active_connection(self, _): + self.assertEqual(get_nm_activate_connection(), "uuid1") + + @patch( + "wifi_nmcli_test._get_nm_wireless_connections", + return_value={ + "Wireless1": {"uuid": "uuid1", "state": "deactivated"}, + "Wireless2": {"uuid": "uuid2", "state": "activated"}, + "Wireless3": {"uuid": "uuid3", "state": "deactivated"}, + }, + ) + def test_multiple_connections_one_active(self, _): + self.assertEqual(get_nm_activate_connection(), "uuid2") + + +class TestTurnUpConnection(unittest.TestCase): + @patch("wifi_nmcli_test.sp.call") + @patch("wifi_nmcli_test.get_nm_activate_connection", return_value="uuid1") + def test_connection_already_activated( + self, get_nm_activate_connection_mock, sp_call_mock + ): + turn_up_connection("uuid1") + sp_call_mock.assert_not_called() + + @patch("wifi_nmcli_test.sp.call", return_value=0) + @patch("wifi_nmcli_test.get_nm_activate_connection", return_value="") + def test_connection_activation_succeeds( + self, get_nm_activate_connection_mock, sp_call_mock + ): + turn_up_connection("uuid2") + sp_call_mock.assert_called_with("nmcli c up uuid2".split()) + + @patch("wifi_nmcli_test.sp.call", side_effect=Exception("Command failed")) + @patch("wifi_nmcli_test.get_nm_activate_connection", return_value="") + def test_connection_activation_fails_due_to_exception( + self, + get_nm_activate_connection_mock, + sp_call_mock, + ): + turn_up_connection("uuid3") + + +class TestTurnDownNmConnections(unittest.TestCase): + @patch("wifi_nmcli_test.sp.call") + @patch("wifi_nmcli_test._get_nm_wireless_connections", return_value={}) + def test_no_connections_to_turn_down( + self, get_connections_mock, sp_call_mock + ): + turn_down_nm_connections() + self.assertEqual(get_connections_mock.call_count, 1) + sp_call_mock.assert_not_called() + + @patch("wifi_nmcli_test.sp.call") + @patch( + "wifi_nmcli_test._get_nm_wireless_connections", + return_value={"Wireless1": {"uuid": "uuid1", "state": "activated"}}, + ) + def test_turn_down_single_connection( + self, get_connections_mock, sp_call_mock + ): + turn_down_nm_connections() + self.assertEqual(get_connections_mock.call_count, 1) + sp_call_mock.assert_called_once_with("nmcli c down uuid1".split()) + + @patch( + "wifi_nmcli_test.sp.call", side_effect=Exception("Error turning down") + ) + @patch( + "wifi_nmcli_test._get_nm_wireless_connections", + return_value={"Wireless1": {"uuid": "uuid1", "state": "activated"}}, + ) + def test_turn_down_single_connection_with_exception( + self, get_connections_mock, sp_call_mock + ): + with self.assertRaises(Exception): + turn_down_nm_connections() + self.assertEqual(get_connections_mock.call_count, 1) + sp_call_mock.assert_called_once_with("nmcli c down uuid1".split()) + + @patch("wifi_nmcli_test.sp.call") + @patch( + "wifi_nmcli_test._get_nm_wireless_connections", + return_value={ + "Wireless1": {"uuid": "uuid1", "state": "activated"}, + "Wireless2": {"uuid": "uuid2", "state": "activated"}, + }, + ) + def test_turn_down_multiple_connections( + self, get_connections_mock, sp_call_mock + ): + turn_down_nm_connections() + self.assertEqual(get_connections_mock.call_count, 1) + calls = [ + call("nmcli c down uuid1".split()), + call("nmcli c down uuid2".split()), + ] + sp_call_mock.assert_has_calls(calls, any_order=True) + + +class TestDeleteTestApSsidConnection(unittest.TestCase): + @patch("wifi_nmcli_test.sp.call", return_value=0) + @patch( + "wifi_nmcli_test._get_nm_wireless_connections", + return_value={ + "TEST_CON": {"uuid": "uuid-test", "state": "deactivated"} + }, + ) + @patch("wifi_nmcli_test.print") + def test_delete_existing_test_con( + self, print_mock, get_nm_wireless_connections_mock, sp_call_mock + ): + delete_test_ap_ssid_connection() + print_mock.assert_called_with("TEST_CON is deleted") + + @patch("wifi_nmcli_test.sp.call", side_effect=Exception("Deletion failed")) + @patch( + "wifi_nmcli_test._get_nm_wireless_connections", + return_value={ + "TEST_CON": {"uuid": "uuid-test", "state": "deactivated"} + }, + ) + @patch("wifi_nmcli_test.print") + def test_delete_test_con_exception( + self, print_mock, get_nm_wireless_connections_mock, sp_call_mock + ): + delete_test_ap_ssid_connection() + print_mock.assert_called_with( + "Can't delete TEST_CON : Deletion failed" + ) + + @patch("wifi_nmcli_test._get_nm_wireless_connections", return_value={}) + @patch("wifi_nmcli_test.print") + def test_no_test_con_to_delete( + self, print_mock, get_nm_wireless_connections_mock + ): + delete_test_ap_ssid_connection() + print_mock.assert_called_with( + "No TEST_CON connection found, nothing to delete" + ) + + +class TestListAps(unittest.TestCase): + @patch("wifi_nmcli_test.sp.check_output") + def test_list_aps_no_essid(self, check_output_mock): + check_output_mock.return_value = ( + b"wlan0 \nSSID1:1:2412:60\nSSID2:6:2437:70\nSSID3:11:2462:80" + ) + expected = { + "SSID1": {"Chan": "1", "Freq": "2412", "Signal": "60"}, + "SSID2": {"Chan": "6", "Freq": "2437", "Signal": "70"}, + "SSID3": {"Chan": "11", "Freq": "2462", "Signal": "80"}, + } + self.assertEqual(list_aps("wlan0"), expected) + + @patch("wifi_nmcli_test.sp.check_output") + def test_list_aps_with_essid(self, check_output_mock): + check_output_mock.return_value = ( + b"SSID1:1:2412:60\nSSID2:6:2437:70\nSSID3:11:2462:80" + ) + expected = { + "SSID2": {"Chan": "6", "Freq": "2437", "Signal": "70"}, + } + self.assertEqual(list_aps("wlan0", "SSID2"), expected) + + @patch("wifi_nmcli_test.sp.check_output") + def test_list_aps_empty_output(self, check_output_mock): + check_output_mock.return_value = b"" + expected = {} + self.assertEqual(list_aps("wlan0"), expected) + + +class TestShowAps(unittest.TestCase): + @patch("wifi_nmcli_test.print") + def test_show_aps_empty(self, mock_print): + aps_dict = {} + show_aps(aps_dict) + mock_print.assert_called_with() + + @patch("wifi_nmcli_test.print") + def test_show_aps_multiple_aps(self, mock_print): + aps_dict = { + "AP1": {"Chan": "1", "Freq": "2412", "Signal": "-40"}, + "AP2": {"Chan": "6", "Freq": "2437", "Signal": "-50"}, + } + show_aps(aps_dict) + expected_calls = [ + call("SSID: AP1 Chan: 1 Freq: 2412 Signal: -40"), + call("SSID: AP2 Chan: 6 Freq: 2437 Signal: -50"), + ] + mock_print.assert_has_calls(expected_calls, any_order=True) + + +class TestWaitForConnected(unittest.TestCase): + @patch("wifi_nmcli_test.print_cmd", new=MagicMock()) + @patch("wifi_nmcli_test.time.sleep", MagicMock(return_value=None)) + @patch( + "wifi_nmcli_test.sp.check_output", + MagicMock( + side_effect=[ + b"30:disconnected\nTestESSID", + b"100:connected\nTestESSID", + ] + ), + ) + def test_wait_for_connected_success(self): + interface = "wlan0" + essid = "TestESSID" + self.assertTrue(wait_for_connected(interface, essid)) + + @patch( + "wifi_nmcli_test.sp.check_output", + MagicMock(return_value=b"30:disconnected\nTestESSID"), + ) + @patch("wifi_nmcli_test.print_cmd", new=MagicMock()) + @patch("wifi_nmcli_test.time.sleep", MagicMock(return_value=None)) + def test_wait_for_connected_failure_due_to_timeout(self): + interface = "wlan0" + essid = "TestESSID" + self.assertFalse(wait_for_connected(interface, essid, max_wait=3)) + + @patch( + "wifi_nmcli_test.sp.check_output", + MagicMock(return_value=b"100:connected\nWrongESSID"), + ) + @patch("wifi_nmcli_test.print_cmd", new=MagicMock()) + @patch("wifi_nmcli_test.time.sleep", MagicMock(return_value=None)) + def test_wait_for_connected_failure_due_to_essid_mismatch(self): + interface = "wlan0" + essid = "TestESSID" + self.assertFalse(wait_for_connected(interface, essid)) + + +class TestOpenConnection(unittest.TestCase): + @patch("wifi_nmcli_test.sp.call", new=MagicMock()) + @patch("wifi_nmcli_test.print_address_info", new=MagicMock()) + @patch("wifi_nmcli_test.print_route_info", new=MagicMock()) + @patch("wifi_nmcli_test.turn_up_connection", new=MagicMock()) + @patch("wifi_nmcli_test.print_head", new=MagicMock()) + @patch("wifi_nmcli_test.print_cmd", new=MagicMock()) + @patch("wifi_nmcli_test.perform_ping_test", return_value=True) + @patch("wifi_nmcli_test.wait_for_connected", return_value=True) + def test_open_connection_success( + self, perform_ping_test_mock, wait_for_connected_mock + ): + args = type("", (), {})() + args.device = "wlan0" + args.essid = "TestESSID" + rc = open_connection(args) + self.assertEqual(rc, 0) + + @patch("wifi_nmcli_test.sp.call", new=MagicMock()) + @patch("wifi_nmcli_test.print_address_info", new=MagicMock()) + @patch("wifi_nmcli_test.print_route_info", new=MagicMock()) + @patch("wifi_nmcli_test.turn_up_connection", new=MagicMock()) + @patch("wifi_nmcli_test.print_head", new=MagicMock()) + @patch("wifi_nmcli_test.print_cmd", new=MagicMock()) + @patch("wifi_nmcli_test.perform_ping_test", MagicMock(return_value=False)) + @patch("wifi_nmcli_test.wait_for_connected", MagicMock(return_value=True)) + def test_open_connection_failed_ping(self): + args = type("", (), {})() + args.device = "wlan0" + args.essid = "TestESSID" + rc = open_connection(args) + self.assertEqual(rc, 1) + + @patch("wifi_nmcli_test.sp.call", new=MagicMock()) + @patch("wifi_nmcli_test.print_head", new=MagicMock()) + @patch("wifi_nmcli_test.print_cmd", new=MagicMock()) + @patch("wifi_nmcli_test.turn_up_connection", new=MagicMock()) + @patch("wifi_nmcli_test.wait_for_connected", MagicMock(return_value=False)) + def test_open_connection_failed_to_connect(self): + args = type("", (), {})() + args.device = "wlan0" + args.essid = "TestESSID" + rc = open_connection(args) + self.assertEqual(rc, 1) + + +class TestSecuredConnection(unittest.TestCase): + @patch("wifi_nmcli_test.sp.call", new=MagicMock()) + @patch("wifi_nmcli_test.print_route_info", new=MagicMock()) + @patch("wifi_nmcli_test.print_address_info", new=MagicMock()) + @patch("wifi_nmcli_test.turn_up_connection", new=MagicMock()) + @patch("wifi_nmcli_test.sp.check_output", new=MagicMock()) + @patch("wifi_nmcli_test.wait_for_connected", return_value=True) + @patch("wifi_nmcli_test.perform_ping_test", return_value=True) + def test_secured_connection_success( + self, + perform_ping_test_mock, + wait_for_connected_mock, + ): + args = type("", (), {})() + args.device = "wlan0" + args.essid = "TestSSID" + args.exchange = "wpa-psk" + args.psk = "password123" + rc = secured_connection(args) + self.assertEqual(rc, 0) + wait_for_connected_mock.assert_called_with("wlan0", "TEST_CON") + perform_ping_test_mock.assert_called_with("wlan0") + + @patch("wifi_nmcli_test.sp.call", new=MagicMock()) + @patch("wifi_nmcli_test.print_route_info", new=MagicMock()) + @patch("wifi_nmcli_test.print_address_info", new=MagicMock()) + @patch("wifi_nmcli_test.turn_up_connection", new=MagicMock()) + @patch("wifi_nmcli_test.sp.check_output", new=MagicMock()) + @patch("wifi_nmcli_test.wait_for_connected", return_value=False) + @patch("wifi_nmcli_test.perform_ping_test", return_value=False) + def test_secured_connection_fail_to_connect( + self, + perform_ping_test_mock, + wait_for_connected_mock, + ): + args = type("", (), {})() + args.device = "wlan0" + args.essid = "TestSSID" + args.exchange = "wpa-psk" + args.psk = "password123" + rc = secured_connection(args) + self.assertEqual(rc, 1) + wait_for_connected_mock.assert_called_with("wlan0", "TEST_CON") + perform_ping_test_mock.assert_not_called() + + @patch("wifi_nmcli_test.sp.call", new=MagicMock()) + @patch("wifi_nmcli_test.print_route_info", new=MagicMock()) + @patch("wifi_nmcli_test.print_address_info", new=MagicMock()) + @patch("wifi_nmcli_test.turn_up_connection", new=MagicMock()) + @patch("wifi_nmcli_test.sp.check_output", new=MagicMock()) + @patch("wifi_nmcli_test.wait_for_connected", return_value=False) + @patch("wifi_nmcli_test.perform_ping_test", return_value=True) + def test_secured_connection_command_failure( + self, + perform_ping_test_mock, + wait_for_connected_mock, + ): + args = type("", (), {})() + args.device = "wlan0" + args.essid = "TestSSID" + args.exchange = "wpa-psk" + args.psk = "password123" + rc = secured_connection(args) + self.assertEqual(rc, 1) + wait_for_connected_mock.assert_called_with("wlan0", "TEST_CON") + perform_ping_test_mock.assert_not_called() + + +class TestParserArgs(unittest.TestCase): + @patch("sys.argv", ["wifi_nmcli_test.py", "scan", "wlan0"]) + def test_parser_args_scan(self): + args = parser_args() + self.assertEqual(args.test_type, "scan") + self.assertEqual(args.device, "wlan0") + + @patch("sys.argv", ["wifi_nmcli_test.py", "open", "wlan0", "TestSSID"]) + def test_parser_args_open(self): + args = parser_args() + self.assertEqual(args.test_type, "open") + self.assertEqual(args.device, "wlan0") + self.assertEqual(args.essid, "TestSSID") + + @patch( + "sys.argv", + ["wifi_nmcli_test.py", "secured", "wlan0", "TestSSID", "TestPSK"], + ) + def test_parser_args_secured(self): + args = parser_args() + self.assertEqual(args.test_type, "secured") + self.assertEqual(args.device, "wlan0") + self.assertEqual(args.essid, "TestSSID") + self.assertEqual(args.psk, "TestPSK") + self.assertEqual(args.exchange, "wpa-psk") + + @patch( + "sys.argv", + [ + "wifi_nmcli_test.py", + "secured", + "wlan0", + "TestSSID", + "TestPSK", + "--exchange", + "wpa2-psk", + ], + ) + def test_parser_args_secured_with_exchange(self): + args = parser_args() + self.assertEqual(args.exchange, "wpa2-psk") + + @patch("sys.argv", ["wifi_nmcli_test.py", "ap", "wlan0", "5GHz"]) + def test_parser_args_ap(self): + args = parser_args() + self.assertEqual(args.test_type, "ap") + self.assertEqual(args.device, "wlan0") + self.assertEqual(args.band, "5GHz") + + +class TestMainFunction(unittest.TestCase): + + @patch("wifi_nmcli_test.delete_test_ap_ssid_connection", new=MagicMock()) + @patch("wifi_nmcli_test.turn_down_nm_connections", new=MagicMock()) + @patch("wifi_nmcli_test.turn_up_connection", new=MagicMock()) + @patch("wifi_nmcli_test.device_rescan", new=MagicMock()) + @patch( + "wifi_nmcli_test.get_nm_activate_connection", return_value="uuid123" + ) + @patch("wifi_nmcli_test.list_aps", return_value={}) + @patch("wifi_nmcli_test.sys.argv", ["wifi_nmcli_test.py", "scan", "wlan0"]) + def test_main_scan_no_aps_found( + self, + list_aps_mock, + get_nm_activate_connection_mock, + ): + main() + + @patch("wifi_nmcli_test.delete_test_ap_ssid_connection", new=MagicMock()) + @patch("wifi_nmcli_test.turn_down_nm_connections", new=MagicMock()) + @patch("wifi_nmcli_test.turn_up_connection", new=MagicMock()) + @patch("wifi_nmcli_test.device_rescan", new=MagicMock()) + @patch( + "wifi_nmcli_test.get_nm_activate_connection", + return_value="uuid123", + ) + @patch( + "wifi_nmcli_test.list_aps", + return_value={ + "SSID1": {"Chan": "1", "Freq": "2412", "Signal": "60"}, + "SSID2": {"Chan": "6", "Freq": "2437", "Signal": "70"}, + "SSID3": {"Chan": "11", "Freq": "2462", "Signal": "80"}, + }, + ) + @patch("wifi_nmcli_test.sys.argv", ["wifi_nmcli_test.py", "scan", "wlan0"]) + def test_main_scan_aps_found( + self, + list_aps_mock, + get_nm_activate_connection_mock, + ): + main() + + @patch("wifi_nmcli_test.delete_test_ap_ssid_connection", new=MagicMock()) + @patch("wifi_nmcli_test.turn_down_nm_connections", new=MagicMock()) + @patch("wifi_nmcli_test.turn_up_connection", new=MagicMock()) + @patch("wifi_nmcli_test.device_rescan", new=MagicMock()) + @patch( + "wifi_nmcli_test.get_nm_activate_connection", + return_value="uuid123", + ) + @patch("wifi_nmcli_test.list_aps", return_value={}) + @patch( + "wifi_nmcli_test.sys.argv", + ["wifi_nmcli_test.py", "open", "wlan0", "TestSSID"], + ) + def test_main_open_no_aps_found( + self, + list_aps_mock, + get_nm_activate_connection_mock, + ): + main() + + @patch("wifi_nmcli_test.delete_test_ap_ssid_connection", new=MagicMock()) + @patch( + "wifi_nmcli_test.get_nm_activate_connection", + return_value="uuid123", + ) + @patch("wifi_nmcli_test.turn_down_nm_connections", new=MagicMock()) + @patch("wifi_nmcli_test.turn_up_connection", new=MagicMock()) + @patch("wifi_nmcli_test.device_rescan", new=MagicMock()) + @patch( + "wifi_nmcli_test.list_aps", + return_value={ + "SSID1": {"Chan": "1", "Freq": "2412", "Signal": "60"}, + "SSID2": {"Chan": "6", "Freq": "2437", "Signal": "70"}, + "TestSSID": {"Chan": "11", "Freq": "2462", "Signal": "80"}, + }, + ) + @patch("wifi_nmcli_test.open_connection", return_value=0) + @patch( + "wifi_nmcli_test.sys.argv", + ["wifi_nmcli_test.py", "open", "wlan0", "TestSSID"], + ) + def test_main_open_aps_found( + self, + list_aps_mock, + get_nm_activate_connection_mock, + mock_open_connection, + ): + main()