mirror of
https://github.com/home-assistant/supervisor.git
synced 2025-11-06 17:40:16 +00:00
Compare commits
3 Commits
2025.11.0
...
avoid-addi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
683588c1ab | ||
|
|
b4f2a5dca6 | ||
|
|
2c3e06231a |
4
.github/workflows/ci.yaml
vendored
4
.github/workflows/ci.yaml
vendored
@@ -386,7 +386,7 @@ jobs:
|
||||
-o console_output_style=count \
|
||||
tests
|
||||
- name: Upload coverage artifact
|
||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: coverage
|
||||
path: .coverage
|
||||
@@ -417,7 +417,7 @@ jobs:
|
||||
echo "Failed to restore Python virtual environment from cache"
|
||||
exit 1
|
||||
- name: Download all coverage artifacts
|
||||
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
|
||||
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
|
||||
with:
|
||||
name: coverage
|
||||
path: coverage/
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
repos:
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.14.3
|
||||
rev: v0.11.10
|
||||
hooks:
|
||||
- id: ruff
|
||||
args:
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
aiodns==3.5.0
|
||||
aiohttp==3.13.2
|
||||
aiohttp==3.13.1
|
||||
atomicwrites-homeassistant==1.4.1
|
||||
attrs==25.4.0
|
||||
awesomeversion==25.8.0
|
||||
@@ -17,13 +17,13 @@ faust-cchardet==2.1.19
|
||||
gitpython==3.1.45
|
||||
jinja2==3.1.6
|
||||
log-rate-limit==1.4.2
|
||||
orjson==3.11.4
|
||||
orjson==3.11.3
|
||||
pulsectl==24.12.0
|
||||
pyudev==0.24.4
|
||||
PyYAML==6.0.3
|
||||
requests==2.32.5
|
||||
securetar==2025.2.1
|
||||
sentry-sdk==2.43.0
|
||||
sentry-sdk==2.42.1
|
||||
setuptools==80.9.0
|
||||
voluptuous==0.15.2
|
||||
dbus-fast==2.44.5
|
||||
|
||||
@@ -8,7 +8,7 @@ pytest-asyncio==0.25.2
|
||||
pytest-cov==7.0.0
|
||||
pytest-timeout==2.4.0
|
||||
pytest==8.4.2
|
||||
ruff==0.14.3
|
||||
ruff==0.14.1
|
||||
time-machine==2.19.0
|
||||
types-docker==7.1.0.20251009
|
||||
types-pyyaml==6.0.12.20250915
|
||||
|
||||
@@ -1562,15 +1562,7 @@ class Addon(AddonModel):
|
||||
)
|
||||
break
|
||||
|
||||
# Exponential backoff to spread retries over the throttle window
|
||||
delay = WATCHDOG_RETRY_SECONDS * (1 << max(attempts - 1, 0))
|
||||
_LOGGER.debug(
|
||||
"Watchdog will retry addon %s in %s seconds (attempt %s)",
|
||||
self.name,
|
||||
delay,
|
||||
attempts + 1,
|
||||
)
|
||||
await asyncio.sleep(delay)
|
||||
await asyncio.sleep(WATCHDOG_RETRY_SECONDS)
|
||||
|
||||
async def container_state_changed(self, event: DockerContainerStateEvent) -> None:
|
||||
"""Set addon state from container state."""
|
||||
|
||||
@@ -253,18 +253,28 @@ class APIIngress(CoreSysAttributes):
|
||||
skip_auto_headers={hdrs.CONTENT_TYPE},
|
||||
) as result:
|
||||
headers = _response_header(result)
|
||||
|
||||
# Avoid parsing content_type in simple cases for better performance
|
||||
if maybe_content_type := result.headers.get(hdrs.CONTENT_TYPE):
|
||||
content_type = (maybe_content_type.partition(";"))[0].strip()
|
||||
else:
|
||||
content_type = result.content_type
|
||||
|
||||
# Empty body responses (304, 204, HEAD, etc.) should not be streamed,
|
||||
# otherwise aiohttp < 3.9.0 may generate an invalid "0\r\n\r\n" chunk
|
||||
# This also avoids setting content_type for empty responses.
|
||||
if must_be_empty_body(request.method, result.status):
|
||||
# If upstream contains content-type, preserve it (e.g. for HEAD requests)
|
||||
if maybe_content_type:
|
||||
headers[hdrs.CONTENT_TYPE] = content_type
|
||||
return web.Response(
|
||||
headers=headers,
|
||||
status=result.status,
|
||||
)
|
||||
|
||||
# Simple request
|
||||
if (
|
||||
# empty body responses should not be streamed,
|
||||
# otherwise aiohttp < 3.9.0 may generate
|
||||
# an invalid "0\r\n\r\n" chunk instead of an empty response.
|
||||
must_be_empty_body(request.method, result.status)
|
||||
or hdrs.CONTENT_LENGTH in result.headers
|
||||
hdrs.CONTENT_LENGTH in result.headers
|
||||
and int(result.headers.get(hdrs.CONTENT_LENGTH, 0)) < 4_194_000
|
||||
):
|
||||
# Return Response
|
||||
|
||||
@@ -306,8 +306,6 @@ class DockerInterface(JobGroup, ABC):
|
||||
# Our filters have all passed. Time to update the job
|
||||
# Only downloading and extracting have progress details. Use that to set extra
|
||||
# We'll leave it around on later stages as the total bytes may be useful after that stage
|
||||
# Enforce range to prevent float drift error
|
||||
progress = max(0, min(progress, 100))
|
||||
if (
|
||||
stage in {PullImageLayerStage.DOWNLOADING, PullImageLayerStage.EXTRACTING}
|
||||
and reference.progress_detail
|
||||
@@ -373,7 +371,7 @@ class DockerInterface(JobGroup, ABC):
|
||||
|
||||
# To reduce noise, limit updates to when result has changed by an entire percent or when stage changed
|
||||
if stage != install_job.stage or progress >= install_job.progress + 1:
|
||||
install_job.update(stage=stage.status, progress=max(0, min(progress, 100)))
|
||||
install_job.update(stage=stage.status, progress=progress)
|
||||
|
||||
@Job(
|
||||
name="docker_interface_install",
|
||||
|
||||
@@ -9,12 +9,7 @@ from typing import Any
|
||||
from supervisor.resolution.const import UnhealthyReason
|
||||
|
||||
from ..coresys import CoreSys, CoreSysAttributes
|
||||
from ..exceptions import (
|
||||
DBusError,
|
||||
DBusNotConnectedError,
|
||||
DBusObjectError,
|
||||
HardwareNotFound,
|
||||
)
|
||||
from ..exceptions import DBusError, DBusObjectError, HardwareNotFound
|
||||
from .const import UdevSubsystem
|
||||
from .data import Device
|
||||
|
||||
@@ -212,8 +207,6 @@ class HwDisk(CoreSysAttributes):
|
||||
try:
|
||||
block_device = self.sys_dbus.udisks2.get_block_device_by_path(device_path)
|
||||
drive = self.sys_dbus.udisks2.get_drive(block_device.drive)
|
||||
except DBusNotConnectedError:
|
||||
return None
|
||||
except DBusObjectError:
|
||||
_LOGGER.warning(
|
||||
"Unable to find UDisks2 drive for device at %s", device_path.as_posix()
|
||||
|
||||
@@ -327,17 +327,6 @@ class JobManager(FileConfiguration, CoreSysAttributes):
|
||||
if not curr_parent.child_job_syncs:
|
||||
continue
|
||||
|
||||
# HACK: If parent trigger the same child job, we just skip this second
|
||||
# sync. Maybe it would be better to have this reflected in the job stage
|
||||
# and reset progress to 0 instead? There is no support for such stage
|
||||
# information on Core update entities today though.
|
||||
if curr_parent.done is True or curr_parent.progress >= 100:
|
||||
_LOGGER.debug(
|
||||
"Skipping parent job sync for done parent job %s",
|
||||
curr_parent.name,
|
||||
)
|
||||
continue
|
||||
|
||||
# Break after first match at each parent as it doesn't make sense
|
||||
# to match twice. But it could match multiple parents
|
||||
for sync in curr_parent.child_job_syncs:
|
||||
|
||||
@@ -1,12 +1,28 @@
|
||||
"""Test ingress API."""
|
||||
|
||||
from unittest.mock import AsyncMock, patch
|
||||
from collections.abc import AsyncGenerator
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
from aiohttp.test_utils import TestClient
|
||||
import aiohttp
|
||||
from aiohttp import hdrs, web
|
||||
from aiohttp.test_utils import TestClient, TestServer
|
||||
import pytest
|
||||
|
||||
from supervisor.addons.addon import Addon
|
||||
from supervisor.coresys import CoreSys
|
||||
|
||||
|
||||
@pytest.fixture(name="real_websession")
|
||||
async def fixture_real_websession(
|
||||
coresys: CoreSys,
|
||||
) -> AsyncGenerator[aiohttp.ClientSession]:
|
||||
"""Fixture for real aiohttp ClientSession for ingress proxy tests."""
|
||||
session = aiohttp.ClientSession()
|
||||
coresys._websession = session # pylint: disable=W0212
|
||||
yield session
|
||||
await session.close()
|
||||
|
||||
|
||||
async def test_validate_session(api_client: TestClient, coresys: CoreSys):
|
||||
"""Test validating ingress session."""
|
||||
with patch("aiohttp.web_request.BaseRequest.__getitem__", return_value=None):
|
||||
@@ -86,3 +102,126 @@ async def test_validate_session_with_user_id(
|
||||
assert (
|
||||
coresys.ingress.get_session_data(session).user.display_name == "Some Name"
|
||||
)
|
||||
|
||||
|
||||
async def test_ingress_proxy_no_content_type_for_empty_body_responses(
|
||||
api_client: TestClient, coresys: CoreSys, real_websession: aiohttp.ClientSession
|
||||
):
|
||||
"""Test that empty body responses don't get Content-Type header."""
|
||||
|
||||
# Create a mock add-on backend server that returns various status codes
|
||||
async def mock_addon_handler(request: web.Request) -> web.Response:
|
||||
"""Mock add-on handler that returns different status codes based on path."""
|
||||
path = request.path
|
||||
|
||||
if path == "/204":
|
||||
# 204 No Content - should not have Content-Type
|
||||
return web.Response(status=204)
|
||||
elif path == "/304":
|
||||
# 304 Not Modified - should not have Content-Type
|
||||
return web.Response(status=304)
|
||||
elif path == "/100":
|
||||
# 100 Continue - should not have Content-Type
|
||||
return web.Response(status=100)
|
||||
elif path == "/head":
|
||||
# HEAD request - should have Content-Type (same as GET would)
|
||||
return web.Response(body=b"test", content_type="text/html")
|
||||
elif path == "/200":
|
||||
# 200 OK with body - should have Content-Type
|
||||
return web.Response(body=b"test content", content_type="text/plain")
|
||||
elif path == "/200-no-content-type":
|
||||
# 200 OK without explicit Content-Type - should get default
|
||||
return web.Response(body=b"test content")
|
||||
elif path == "/200-json":
|
||||
# 200 OK with JSON - should preserve Content-Type
|
||||
return web.Response(
|
||||
body=b'{"key": "value"}', content_type="application/json"
|
||||
)
|
||||
else:
|
||||
return web.Response(body=b"default", content_type="text/html")
|
||||
|
||||
# Create test server for mock add-on
|
||||
app = web.Application()
|
||||
app.router.add_route("*", "/{tail:.*}", mock_addon_handler)
|
||||
addon_server = TestServer(app)
|
||||
await addon_server.start_server()
|
||||
|
||||
try:
|
||||
# Create ingress session
|
||||
resp = await api_client.post("/ingress/session")
|
||||
result = await resp.json()
|
||||
session = result["data"]["session"]
|
||||
|
||||
# Create a mock add-on
|
||||
mock_addon = MagicMock(spec=Addon)
|
||||
mock_addon.slug = "test_addon"
|
||||
mock_addon.ip_address = addon_server.host
|
||||
mock_addon.ingress_port = addon_server.port
|
||||
mock_addon.ingress_stream = False
|
||||
|
||||
# Generate an ingress token and register the add-on
|
||||
ingress_token = coresys.ingress.create_session()
|
||||
with patch.object(coresys.ingress, "get", return_value=mock_addon):
|
||||
# Test 204 No Content - should NOT have Content-Type
|
||||
resp = await api_client.get(
|
||||
f"/ingress/{ingress_token}/204",
|
||||
cookies={"ingress_session": session},
|
||||
)
|
||||
assert resp.status == 204
|
||||
assert hdrs.CONTENT_TYPE not in resp.headers
|
||||
|
||||
# Test 304 Not Modified - should NOT have Content-Type
|
||||
resp = await api_client.get(
|
||||
f"/ingress/{ingress_token}/304",
|
||||
cookies={"ingress_session": session},
|
||||
)
|
||||
assert resp.status == 304
|
||||
assert hdrs.CONTENT_TYPE not in resp.headers
|
||||
|
||||
# Test HEAD request - SHOULD have Content-Type (same as GET)
|
||||
# per RFC 9110: HEAD should return same headers as GET
|
||||
resp = await api_client.head(
|
||||
f"/ingress/{ingress_token}/head",
|
||||
cookies={"ingress_session": session},
|
||||
)
|
||||
assert resp.status == 200
|
||||
assert hdrs.CONTENT_TYPE in resp.headers
|
||||
assert "text/html" in resp.headers[hdrs.CONTENT_TYPE]
|
||||
# Body should be empty for HEAD
|
||||
body = await resp.read()
|
||||
assert body == b""
|
||||
|
||||
# Test 200 OK with body - SHOULD have Content-Type
|
||||
resp = await api_client.get(
|
||||
f"/ingress/{ingress_token}/200",
|
||||
cookies={"ingress_session": session},
|
||||
)
|
||||
assert resp.status == 200
|
||||
assert hdrs.CONTENT_TYPE in resp.headers
|
||||
assert resp.headers[hdrs.CONTENT_TYPE] == "text/plain"
|
||||
body = await resp.read()
|
||||
assert body == b"test content"
|
||||
|
||||
# Test 200 OK without explicit Content-Type - SHOULD get default
|
||||
resp = await api_client.get(
|
||||
f"/ingress/{ingress_token}/200-no-content-type",
|
||||
cookies={"ingress_session": session},
|
||||
)
|
||||
assert resp.status == 200
|
||||
assert hdrs.CONTENT_TYPE in resp.headers
|
||||
# Should get application/octet-stream as default from aiohttp ClientResponse
|
||||
assert "application/octet-stream" in resp.headers[hdrs.CONTENT_TYPE]
|
||||
|
||||
# Test 200 OK with JSON - SHOULD preserve Content-Type
|
||||
resp = await api_client.get(
|
||||
f"/ingress/{ingress_token}/200-json",
|
||||
cookies={"ingress_session": session},
|
||||
)
|
||||
assert resp.status == 200
|
||||
assert hdrs.CONTENT_TYPE in resp.headers
|
||||
assert "application/json" in resp.headers[hdrs.CONTENT_TYPE]
|
||||
body = await resp.read()
|
||||
assert body == b'{"key": "value"}'
|
||||
|
||||
finally:
|
||||
await addon_server.close()
|
||||
|
||||
@@ -376,14 +376,3 @@ async def test_try_get_nvme_life_time_missing_percent_used(
|
||||
coresys.config.path_supervisor
|
||||
)
|
||||
assert lifetime is None
|
||||
|
||||
|
||||
async def test_try_get_nvme_life_time_dbus_not_connected(coresys: CoreSys):
|
||||
"""Test getting lifetime info from an NVMe when DBUS is not connected."""
|
||||
# Set the dbus for udisks2 bus to be None, to make it forcibly disconnected.
|
||||
coresys.dbus.udisks2.dbus = None
|
||||
|
||||
lifetime = await coresys.hardware.disk.get_disk_life_time(
|
||||
coresys.config.path_supervisor
|
||||
)
|
||||
assert lifetime is None
|
||||
|
||||
@@ -7,8 +7,8 @@ import pytest
|
||||
|
||||
from supervisor.coresys import CoreSys
|
||||
from supervisor.dbus.const import DeviceType
|
||||
from supervisor.host.configuration import Interface, VlanConfig, WifiConfig
|
||||
from supervisor.host.const import AuthMethod, InterfaceType, WifiMode
|
||||
from supervisor.host.configuration import Interface, VlanConfig
|
||||
from supervisor.host.const import InterfaceType
|
||||
|
||||
from tests.dbus_service_mocks.base import DBusServiceMock
|
||||
from tests.dbus_service_mocks.network_connection_settings import (
|
||||
@@ -291,237 +291,3 @@ async def test_equals_dbus_interface_eth0_10_real(
|
||||
|
||||
# Test should pass with matching VLAN config
|
||||
assert test_vlan_interface.equals_dbus_interface(network_interface) is True
|
||||
|
||||
|
||||
def test_map_nm_wifi_non_wireless_interface():
|
||||
"""Test _map_nm_wifi returns None for non-wireless interface."""
|
||||
# Mock non-wireless interface
|
||||
mock_interface = Mock()
|
||||
mock_interface.type = DeviceType.ETHERNET
|
||||
mock_interface.settings = Mock()
|
||||
|
||||
result = Interface._map_nm_wifi(mock_interface)
|
||||
assert result is None
|
||||
|
||||
|
||||
def test_map_nm_wifi_no_settings():
|
||||
"""Test _map_nm_wifi returns None when interface has no settings."""
|
||||
# Mock wireless interface without settings
|
||||
mock_interface = Mock()
|
||||
mock_interface.type = DeviceType.WIRELESS
|
||||
mock_interface.settings = None
|
||||
|
||||
result = Interface._map_nm_wifi(mock_interface)
|
||||
assert result is None
|
||||
|
||||
|
||||
def test_map_nm_wifi_open_authentication():
|
||||
"""Test _map_nm_wifi with open authentication (no security)."""
|
||||
# Mock wireless interface with open authentication
|
||||
mock_interface = Mock()
|
||||
mock_interface.type = DeviceType.WIRELESS
|
||||
mock_interface.settings = Mock()
|
||||
mock_interface.settings.wireless_security = None
|
||||
mock_interface.settings.wireless = Mock()
|
||||
mock_interface.settings.wireless.ssid = "TestSSID"
|
||||
mock_interface.settings.wireless.mode = "infrastructure"
|
||||
mock_interface.wireless = None
|
||||
mock_interface.interface_name = "wlan0"
|
||||
|
||||
result = Interface._map_nm_wifi(mock_interface)
|
||||
|
||||
assert result is not None
|
||||
assert isinstance(result, WifiConfig)
|
||||
assert result.mode == WifiMode.INFRASTRUCTURE
|
||||
assert result.ssid == "TestSSID"
|
||||
assert result.auth == AuthMethod.OPEN
|
||||
assert result.psk is None
|
||||
assert result.signal is None
|
||||
|
||||
|
||||
def test_map_nm_wifi_wep_authentication():
|
||||
"""Test _map_nm_wifi with WEP authentication."""
|
||||
# Mock wireless interface with WEP authentication
|
||||
mock_interface = Mock()
|
||||
mock_interface.type = DeviceType.WIRELESS
|
||||
mock_interface.settings = Mock()
|
||||
mock_interface.settings.wireless_security = Mock()
|
||||
mock_interface.settings.wireless_security.key_mgmt = "none"
|
||||
mock_interface.settings.wireless_security.psk = None
|
||||
mock_interface.settings.wireless = Mock()
|
||||
mock_interface.settings.wireless.ssid = "WEPNetwork"
|
||||
mock_interface.settings.wireless.mode = "infrastructure"
|
||||
mock_interface.wireless = None
|
||||
mock_interface.interface_name = "wlan0"
|
||||
|
||||
result = Interface._map_nm_wifi(mock_interface)
|
||||
|
||||
assert result is not None
|
||||
assert isinstance(result, WifiConfig)
|
||||
assert result.auth == AuthMethod.WEP
|
||||
assert result.ssid == "WEPNetwork"
|
||||
assert result.psk is None
|
||||
|
||||
|
||||
def test_map_nm_wifi_wpa_psk_authentication():
|
||||
"""Test _map_nm_wifi with WPA-PSK authentication."""
|
||||
# Mock wireless interface with WPA-PSK authentication
|
||||
mock_interface = Mock()
|
||||
mock_interface.type = DeviceType.WIRELESS
|
||||
mock_interface.settings = Mock()
|
||||
mock_interface.settings.wireless_security = Mock()
|
||||
mock_interface.settings.wireless_security.key_mgmt = "wpa-psk"
|
||||
mock_interface.settings.wireless_security.psk = "SecretPassword123"
|
||||
mock_interface.settings.wireless = Mock()
|
||||
mock_interface.settings.wireless.ssid = "SecureNetwork"
|
||||
mock_interface.settings.wireless.mode = "infrastructure"
|
||||
mock_interface.wireless = None
|
||||
mock_interface.interface_name = "wlan0"
|
||||
|
||||
result = Interface._map_nm_wifi(mock_interface)
|
||||
|
||||
assert result is not None
|
||||
assert isinstance(result, WifiConfig)
|
||||
assert result.auth == AuthMethod.WPA_PSK
|
||||
assert result.ssid == "SecureNetwork"
|
||||
assert result.psk == "SecretPassword123"
|
||||
|
||||
|
||||
def test_map_nm_wifi_unsupported_authentication():
|
||||
"""Test _map_nm_wifi returns None for unsupported authentication method."""
|
||||
# Mock wireless interface with unsupported authentication
|
||||
mock_interface = Mock()
|
||||
mock_interface.type = DeviceType.WIRELESS
|
||||
mock_interface.settings = Mock()
|
||||
mock_interface.settings.wireless_security = Mock()
|
||||
mock_interface.settings.wireless_security.key_mgmt = "wpa-eap" # Unsupported
|
||||
mock_interface.settings.wireless = Mock()
|
||||
mock_interface.settings.wireless.ssid = "EnterpriseNetwork"
|
||||
mock_interface.interface_name = "wlan0"
|
||||
|
||||
result = Interface._map_nm_wifi(mock_interface)
|
||||
|
||||
assert result is None
|
||||
|
||||
|
||||
def test_map_nm_wifi_different_modes():
|
||||
"""Test _map_nm_wifi with different wifi modes."""
|
||||
modes_to_test = [
|
||||
("infrastructure", WifiMode.INFRASTRUCTURE),
|
||||
("mesh", WifiMode.MESH),
|
||||
("adhoc", WifiMode.ADHOC),
|
||||
("ap", WifiMode.AP),
|
||||
]
|
||||
|
||||
for mode_value, expected_mode in modes_to_test:
|
||||
mock_interface = Mock()
|
||||
mock_interface.type = DeviceType.WIRELESS
|
||||
mock_interface.settings = Mock()
|
||||
mock_interface.settings.wireless_security = None
|
||||
mock_interface.settings.wireless = Mock()
|
||||
mock_interface.settings.wireless.ssid = "TestSSID"
|
||||
mock_interface.settings.wireless.mode = mode_value
|
||||
mock_interface.wireless = None
|
||||
mock_interface.interface_name = "wlan0"
|
||||
|
||||
result = Interface._map_nm_wifi(mock_interface)
|
||||
|
||||
assert result is not None
|
||||
assert result.mode == expected_mode
|
||||
|
||||
|
||||
def test_map_nm_wifi_with_signal():
|
||||
"""Test _map_nm_wifi with wireless signal strength."""
|
||||
# Mock wireless interface with active connection and signal
|
||||
mock_interface = Mock()
|
||||
mock_interface.type = DeviceType.WIRELESS
|
||||
mock_interface.settings = Mock()
|
||||
mock_interface.settings.wireless_security = None
|
||||
mock_interface.settings.wireless = Mock()
|
||||
mock_interface.settings.wireless.ssid = "TestSSID"
|
||||
mock_interface.settings.wireless.mode = "infrastructure"
|
||||
mock_interface.wireless = Mock()
|
||||
mock_interface.wireless.active = Mock()
|
||||
mock_interface.wireless.active.strength = 75
|
||||
mock_interface.interface_name = "wlan0"
|
||||
|
||||
result = Interface._map_nm_wifi(mock_interface)
|
||||
|
||||
assert result is not None
|
||||
assert result.signal == 75
|
||||
|
||||
|
||||
def test_map_nm_wifi_without_signal():
|
||||
"""Test _map_nm_wifi without wireless signal (no active connection)."""
|
||||
# Mock wireless interface without active connection
|
||||
mock_interface = Mock()
|
||||
mock_interface.type = DeviceType.WIRELESS
|
||||
mock_interface.settings = Mock()
|
||||
mock_interface.settings.wireless_security = None
|
||||
mock_interface.settings.wireless = Mock()
|
||||
mock_interface.settings.wireless.ssid = "TestSSID"
|
||||
mock_interface.settings.wireless.mode = "infrastructure"
|
||||
mock_interface.wireless = None
|
||||
mock_interface.interface_name = "wlan0"
|
||||
|
||||
result = Interface._map_nm_wifi(mock_interface)
|
||||
|
||||
assert result is not None
|
||||
assert result.signal is None
|
||||
|
||||
|
||||
def test_map_nm_wifi_wireless_no_active_ap():
|
||||
"""Test _map_nm_wifi with wireless object but no active access point."""
|
||||
# Mock wireless interface with wireless object but no active AP
|
||||
mock_interface = Mock()
|
||||
mock_interface.type = DeviceType.WIRELESS
|
||||
mock_interface.settings = Mock()
|
||||
mock_interface.settings.wireless_security = None
|
||||
mock_interface.settings.wireless = Mock()
|
||||
mock_interface.settings.wireless.ssid = "TestSSID"
|
||||
mock_interface.settings.wireless.mode = "infrastructure"
|
||||
mock_interface.wireless = Mock()
|
||||
mock_interface.wireless.active = None
|
||||
mock_interface.interface_name = "wlan0"
|
||||
|
||||
result = Interface._map_nm_wifi(mock_interface)
|
||||
|
||||
assert result is not None
|
||||
assert result.signal is None
|
||||
|
||||
|
||||
def test_map_nm_wifi_no_wireless_settings():
|
||||
"""Test _map_nm_wifi when wireless settings are missing."""
|
||||
# Mock wireless interface without wireless settings
|
||||
mock_interface = Mock()
|
||||
mock_interface.type = DeviceType.WIRELESS
|
||||
mock_interface.settings = Mock()
|
||||
mock_interface.settings.wireless_security = None
|
||||
mock_interface.settings.wireless = None
|
||||
mock_interface.wireless = None
|
||||
mock_interface.interface_name = "wlan0"
|
||||
|
||||
result = Interface._map_nm_wifi(mock_interface)
|
||||
|
||||
assert result is not None
|
||||
assert result.ssid == ""
|
||||
assert result.mode == WifiMode.INFRASTRUCTURE # Default mode
|
||||
|
||||
|
||||
def test_map_nm_wifi_no_wireless_mode():
|
||||
"""Test _map_nm_wifi when wireless mode is not specified."""
|
||||
# Mock wireless interface without mode specified
|
||||
mock_interface = Mock()
|
||||
mock_interface.type = DeviceType.WIRELESS
|
||||
mock_interface.settings = Mock()
|
||||
mock_interface.settings.wireless_security = None
|
||||
mock_interface.settings.wireless = Mock()
|
||||
mock_interface.settings.wireless.ssid = "TestSSID"
|
||||
mock_interface.settings.wireless.mode = None
|
||||
mock_interface.wireless = None
|
||||
mock_interface.interface_name = "wlan0"
|
||||
|
||||
result = Interface._map_nm_wifi(mock_interface)
|
||||
|
||||
assert result is not None
|
||||
assert result.mode == WifiMode.INFRASTRUCTURE # Default mode
|
||||
|
||||
Reference in New Issue
Block a user