Merge "Remove use of paramiko in Fuchsia SSH module"
diff --git a/acts_tests/acts_contrib/test_utils/net/net_test_utils.py b/acts_tests/acts_contrib/test_utils/net/net_test_utils.py
index c6086af..c789686 100644
--- a/acts_tests/acts_contrib/test_utils/net/net_test_utils.py
+++ b/acts_tests/acts_contrib/test_utils/net/net_test_utils.py
@@ -34,8 +34,6 @@
 from acts_contrib.test_utils.tel.tel_test_utils import verify_http_connection
 
 from acts_contrib.test_utils.wifi import wifi_test_utils as wutils
-from scapy.config import conf
-from scapy.compat import plain_str
 
 VPN_CONST = cconst.VpnProfile
 VPN_TYPE = cconst.VpnProfileType
@@ -572,6 +570,8 @@
         A list of the latest network interfaces. For example:
         ['cvd-ebr', ..., 'eno1', 'enx4afa19a8dde1', 'lo', 'wlxd03745d68d88']
     """
+    from scapy.config import conf
+    from scapy.compat import plain_str
 
     # Get ifconfig output
     result = job.run([conf.prog.ifconfig])
@@ -619,6 +619,8 @@
     Args:
         iface: network interface that need to enable
     """
+    from scapy.compat import plain_str
+
     result = job.run("sudo ifconfig %s up" % (iface), ignore_status=True)
     if result.exit_status:
         raise asserts.fail(
diff --git a/acts_tests/tests/google/fuchsia/wlan/functional/WlanWirelessNetworkManagementTest.py b/acts_tests/tests/google/fuchsia/wlan/functional/WlanWirelessNetworkManagementTest.py
index d9e3ac9..0c8336e 100644
--- a/acts_tests/tests/google/fuchsia/wlan/functional/WlanWirelessNetworkManagementTest.py
+++ b/acts_tests/tests/google/fuchsia/wlan/functional/WlanWirelessNetworkManagementTest.py
@@ -14,14 +14,27 @@
 #   See the License for the specific language governing permissions and
 #   limitations under the License.
 
+import time
+
+from datetime import datetime, timedelta, timezone
+from typing import FrozenSet
+
+from acts_contrib.test_utils.wifi.WifiBaseTest import WifiBaseTest
+from acts_contrib.test_utils.abstract_devices.wlan_device import create_wlan_device
 from acts import asserts
+from acts import signals
 from acts import utils
 from acts.controllers.access_point import setup_ap
 from acts.controllers.ap_lib import hostapd_constants
-from acts_contrib.test_utils.wifi.WifiBaseTest import WifiBaseTest
-from acts_contrib.test_utils.abstract_devices.wlan_device import create_wlan_device
+from acts.controllers.ap_lib.radio_measurement import BssidInformation, BssidInformationCapabilities, NeighborReportElement, PhyType
+from acts.controllers.ap_lib.wireless_network_management import BssTransitionManagementRequest
 
 
+# TODO(fxbug.dev/103440) WNM support should be visible/controllable in ACTS.
+# When ACTS can see WNM features that are enabled (through ACTS config) or
+# ACTS can enable WNM features (through new APIs), additional tests should be
+# added to this suite that check that features function properly when the DUT is
+# configured to support those features.
 class WlanWirelessNetworkManagementTest(WifiBaseTest):
     """Tests Fuchsia's Wireless Network Management (AKA 802.11v) support.
 
@@ -29,9 +42,8 @@
     * One Fuchsia device
     * One Whirlwind access point
 
-    Existing Fuchsia drivers do not yet support WNM features, so this suite verifies that
-    WNM features are not advertised or used by the Fuchsia DUT. When WNM features are
-    supported, tests will be added that confirm the proper functioning of those features.
+    Existing Fuchsia drivers do not yet support WNM features out-of-the-box, so these
+    tests check that WNM features are not enabled.
     """
 
     def setup_class(self):
@@ -65,21 +77,25 @@
         self.access_point.stop_all_aps()
 
     def setup_ap(self,
-                 wnm_features: frozenset[
+                 ssid: str,
+                 channel: int = hostapd_constants.AP_DEFAULT_CHANNEL_2G,
+                 wnm_features: FrozenSet[
                      hostapd_constants.WnmFeature] = frozenset()):
         """Sets up an AP using the provided parameters.
 
         Args:
-            wnm_features: Wireless Network Management features to enable.
+            ssid: SSID for the AP.
+            channel: which channel number to set the AP to (default is
+                AP_DEFAULT_CHANNEL_2G).
+            wnm_features: Wireless Network Management features to enable
+                (default is no WNM features).
         """
-        ssid = utils.rand_ascii_str(hostapd_constants.AP_SSID_LENGTH_2G)
         setup_ap(access_point=self.access_point,
                  profile_name='whirlwind',
-                 channel=hostapd_constants.AP_DEFAULT_CHANNEL_2G,
+                 channel=channel,
                  ssid=ssid,
                  security=None,
                  wnm_features=wnm_features)
-        self.ssid = ssid
 
     def _get_client_mac(self) -> str:
         """Get the MAC address of the DUT client interface.
@@ -110,11 +126,11 @@
         )
 
     def test_bss_transition_ap_supported_dut_unsupported(self):
+        ssid = utils.rand_ascii_str(hostapd_constants.AP_SSID_LENGTH_2G)
         wnm_features = frozenset(
             [hostapd_constants.WnmFeature.BSS_TRANSITION_MANAGEMENT])
-        self.setup_ap(wnm_features)
-        asserts.assert_true(self.dut.associate(self.ssid),
-                            'Failed to associate.')
+        self.setup_ap(ssid, wnm_features=wnm_features)
+        asserts.assert_true(self.dut.associate(ssid), 'Failed to associate.')
         asserts.assert_true(self.dut.is_connected(), 'Failed to connect.')
         client_mac = self._get_client_mac()
 
@@ -125,10 +141,10 @@
             'DUT is incorrectly advertising BSS Transition Management support')
 
     def test_wnm_sleep_mode_ap_supported_dut_unsupported(self):
+        ssid = utils.rand_ascii_str(hostapd_constants.AP_SSID_LENGTH_2G)
         wnm_features = frozenset([hostapd_constants.WnmFeature.WNM_SLEEP_MODE])
-        self.setup_ap(wnm_features)
-        asserts.assert_true(self.dut.associate(self.ssid),
-                            'Failed to associate.')
+        self.setup_ap(ssid, wnm_features=wnm_features)
+        asserts.assert_true(self.dut.associate(ssid), 'Failed to associate.')
         asserts.assert_true(self.dut.is_connected(), 'Failed to connect.')
         client_mac = self._get_client_mac()
 
@@ -137,3 +153,61 @@
         asserts.assert_false(
             ext_capabilities.wnm_sleep_mode,
             'DUT is incorrectly advertising WNM Sleep Mode support')
+
+    def test_btm_req_ignored_dut_unsupported(self):
+        ssid = utils.rand_ascii_str(hostapd_constants.AP_SSID_LENGTH_2G)
+        wnm_features = frozenset(
+            [hostapd_constants.WnmFeature.BSS_TRANSITION_MANAGEMENT])
+        # Setup 2.4 GHz AP.
+        self.setup_ap(ssid,
+                      channel=hostapd_constants.AP_DEFAULT_CHANNEL_2G,
+                      wnm_features=wnm_features)
+
+        asserts.assert_true(self.dut.associate(ssid), 'Failed to associate.')
+        # Verify that DUT is actually associated (as seen from AP).
+        client_mac = self._get_client_mac()
+        asserts.assert_true(
+            client_mac in self.access_point.get_stas(
+                self.access_point.wlan_2g),
+            'Client MAC not included in list of associated STAs on the 2.4GHz band'
+        )
+
+        # Setup 5 GHz AP with same SSID.
+        self.setup_ap(ssid,
+                      channel=hostapd_constants.AP_DEFAULT_CHANNEL_5G,
+                      wnm_features=wnm_features)
+
+        # Construct a BTM request.
+        dest_bssid = self.access_point.get_bssid_from_ssid(
+            ssid, self.access_point.wlan_5g)
+        dest_bssid_info = BssidInformation(
+            security=True, capabilities=BssidInformationCapabilities())
+        neighbor_5g_ap = NeighborReportElement(
+            dest_bssid,
+            dest_bssid_info,
+            operating_class=126,
+            channel_number=hostapd_constants.AP_DEFAULT_CHANNEL_5G,
+            phy_type=PhyType.VHT)
+        btm_req = BssTransitionManagementRequest(
+            disassociation_imminent=True, candidate_list=[neighbor_5g_ap])
+
+        # Send BTM request from 2.4 GHz AP to DUT
+        self.access_point.send_bss_transition_management_req(
+            self.access_point.wlan_2g, client_mac, btm_req)
+
+        # Check that DUT has not reassociated.
+        REASSOC_DEADLINE = datetime.now(timezone.utc) + timedelta(seconds=2)
+        while datetime.now(timezone.utc) < REASSOC_DEADLINE:
+            # Fail if DUT has reassociated to 5 GHz AP (as seen from AP).
+            if client_mac in self.access_point.get_stas(
+                    self.access_point.wlan_5g):
+                raise signals.TestFailure(
+                    'DUT unexpectedly roamed to target BSS after BTM request')
+            else:
+                time.sleep(0.25)
+
+        # DUT should have stayed associated to original AP.
+        asserts.assert_true(
+            client_mac in self.access_point.get_stas(
+                self.access_point.wlan_2g),
+            'DUT lost association on the 2.4GHz band after BTM request')