"""Tests for interface detection with retry fallback chain.""" import time from pathlib import Path from unittest.mock import patch, MagicMock from tempfile import TemporaryDirectory import pytest from utils.networking import get_primary_interface, detect_interface_with_retry class TestInterfaceDetection: """Test robust interface detection with retry fallback.""" def test_get_primary_interface_from_route_table(self): """get_primary_interface reads default route from /proc/net/route.""" route_content = """Iface Destination Gateway Flags RefCnt Use Metric Mask MTU Window IRTT eth0 00000000 0101010A 0003 0 0 100 00000000 0 0 0 eth0 0101010A 00000000 0001 0 0 100 FFFFFFFF 0 0 0 """ with patch("builtins.open", create=True) as mock_open: mock_open.return_value.__enter__.return_value.readlines.return_value = route_content.split("\n") result = get_primary_interface() assert result == "eth0" def test_get_primary_interface_fallback_eth(self): """get_primary_interface falls back to eth* interfaces if /proc/net/route is empty.""" with patch("builtins.open", side_effect=IOError): with patch("utils.networking.get_all_interfaces", return_value=["eth0", "eth1"]): result = get_primary_interface() assert result == "eth0" def test_get_primary_interface_fallback_wlan(self): """get_primary_interface falls back to first interface if no eth/en.""" with patch("builtins.open", side_effect=IOError): with patch("utils.networking.get_all_interfaces", return_value=["wlan0"]): result = get_primary_interface() assert result == "wlan0" def test_get_primary_interface_no_interfaces(self): """get_primary_interface returns None if no interfaces available.""" with patch("builtins.open", side_effect=IOError): with patch("utils.networking.get_all_interfaces", return_value=[]): result = get_primary_interface() assert result is None def test_detect_interface_with_retry_immediate_success(self): """detect_interface_with_retry returns interface immediately if found.""" with patch("utils.networking.get_primary_interface", return_value="eth0"): result = detect_interface_with_retry(max_retries=3, retry_delay=1) assert result == "eth0" def test_detect_interface_with_retry_eventual_success(self): """detect_interface_with_retry retries and succeeds after N attempts.""" call_count = [0] def get_iface_side_effect(): call_count[0] += 1 if call_count[0] <= 2: return None # Fail first 2 times return "wlan0" # Succeed on 3rd call with patch("utils.networking.get_primary_interface", side_effect=get_iface_side_effect): with patch("utils.networking._get_up_interface", return_value=None): result = detect_interface_with_retry(max_retries=3, retry_delay=0.01) assert result == "wlan0" assert call_count[0] == 3 def test_detect_interface_with_retry_exhausted(self): """detect_interface_with_retry returns None if all retries exhausted.""" with patch("utils.networking.get_primary_interface", return_value=None): with patch("utils.networking._get_up_interface", return_value=None): result = detect_interface_with_retry(max_retries=2, retry_delay=0.01) assert result is None def test_detect_interface_with_retry_uses_config_fallback(self): """detect_interface_with_retry accepts config-set interface as ultimate fallback.""" with patch("utils.networking.get_primary_interface", return_value=None): with patch("utils.networking._get_up_interface", return_value=None): result = detect_interface_with_retry( max_retries=1, retry_delay=0.01, config_interface="configured_iface" ) # Should use config fallback when detection fails assert result == "configured_iface" def test_detect_interface_exponential_backoff(self): """detect_interface_with_retry uses exponential backoff (5s, 10s, 20s).""" call_times = [] def get_iface_with_timing(): call_times.append(time.time()) return None with patch("utils.networking.get_primary_interface", side_effect=get_iface_with_timing): with patch("utils.networking._get_up_interface", return_value=None): with patch("time.sleep") as mock_sleep: result = detect_interface_with_retry( max_retries=3, retry_delay=5, # Base delay exponential=True ) # Should have called sleep with 5, 10, 20 assert mock_sleep.call_count == 3 # Check exponential backoff values (5 * 2^0, 5 * 2^1, 5 * 2^2) sleep_calls = [call[0][0] for call in mock_sleep.call_args_list] assert sleep_calls == [5, 10, 20] def test_detect_interface_filters_excluded(self): """detect_interface_with_retry excludes lo, tailscale*, wg*, tun*, docker*, veth*, br-*.""" excluded_ifaces = ["lo", "tailscale0", "wg0", "tun0", "docker0", "veth123", "br-abc"] with patch("utils.networking.get_primary_interface", return_value=None): # Mock get_all_interfaces to return only excluded ones with patch("utils.networking.get_all_interfaces", return_value=excluded_ifaces): result = detect_interface_with_retry(max_retries=1, retry_delay=0.01) assert result is None