"""Tests for the web_server AP mode helpers.""" import logging import pytest from esphome.components.web_server import ( _final_validate_ap_mode, serve_captive, serve_local, ) from esphome.const import ( CONF_AP, CONF_LOCAL, CONF_NETWORKS, CONF_PORT, CONF_SSID, CONF_VERSION, CONF_WIFI, ) import esphome.final_validate as fv AP_ONLY = {CONF_AP: {}} AP_FALLBACK = {CONF_AP: {}, CONF_NETWORKS: [{CONF_SSID: "x"}]} STA_ONLY = {CONF_NETWORKS: [{CONF_SSID: "x"}]} @pytest.mark.parametrize( ("web_server_config", "wifi_config", "expected"), [ # AP only: embed the interface, the AP has no internet. ({CONF_VERSION: 2}, AP_ONLY, True), ({CONF_VERSION: 3}, AP_ONLY, True), # Explicit setting always wins. ({CONF_VERSION: 2, CONF_LOCAL: False}, AP_ONLY, False), ({CONF_VERSION: 2, CONF_LOCAL: True}, STA_ONLY, True), # AP fallback, no AP, no wifi, or version 1 (no local mode): hosted page. ({CONF_VERSION: 2}, AP_FALLBACK, False), ({CONF_VERSION: 2}, STA_ONLY, False), ({CONF_VERSION: 2}, None, False), ({CONF_VERSION: 1}, AP_ONLY, False), ], ) def test_serve_local( web_server_config: dict, wifi_config: dict | None, expected: bool ) -> None: """The interface is embedded for AP only WiFi unless local is set explicitly.""" assert serve_local(web_server_config, wifi_config) is expected @pytest.mark.parametrize( ("web_server_config", "full_config", "expected"), [ # AP only: local is implied, web_server is the captive portal. ({CONF_VERSION: 2}, {CONF_WIFI: AP_ONLY}, True), # Captive portal probes only work on port 80. ({CONF_VERSION: 2, CONF_PORT: 8080}, {CONF_WIFI: AP_ONLY}, False), # AP fallback needs an explicit local: true to be captive. ({CONF_VERSION: 2}, {CONF_WIFI: AP_FALLBACK}, False), ({CONF_VERSION: 2, CONF_LOCAL: True}, {CONF_WIFI: AP_FALLBACK}, True), # captive_portal owns the role when configured. ({CONF_VERSION: 2}, {CONF_WIFI: AP_ONLY, "captive_portal": {}}, False), # No AP, no wifi, hosted page, or version 1: never captive. ({CONF_VERSION: 2, CONF_LOCAL: True}, {CONF_WIFI: STA_ONLY}, False), ({CONF_VERSION: 2, CONF_LOCAL: True}, {}, False), ({CONF_VERSION: 2, CONF_LOCAL: False}, {CONF_WIFI: AP_ONLY}, False), ({CONF_VERSION: 1}, {CONF_WIFI: AP_ONLY}, False), ], ) def test_serve_captive( web_server_config: dict, full_config: dict, expected: bool ) -> None: web_server_config.setdefault(CONF_PORT, 80) assert serve_captive(web_server_config, full_config) is expected @pytest.mark.parametrize( ("web_server_config", "expect_warning"), [ # Explicit local: false on an AP only device: the hosted page will stay blank. ({CONF_VERSION: 2, CONF_PORT: 80, CONF_LOCAL: False}, True), # Default: embedded and captive, nothing to warn about. ({CONF_VERSION: 2, CONF_PORT: 80}, False), ], ) def test_final_validate_ap_mode_warns_for_hosted_page( web_server_config: dict, expect_warning: bool, caplog: pytest.LogCaptureFixture ) -> None: token = fv.full_config.set({"web_server": web_server_config, CONF_WIFI: AP_ONLY}) try: with caplog.at_level(logging.WARNING): _final_validate_ap_mode(web_server_config) finally: fv.full_config.reset(token) assert ("stays blank" in caplog.text) is expect_warning def test_final_validate_ap_mode_warns_for_non_default_port( caplog: pytest.LogCaptureFixture, ) -> None: """Captive portal detection needs port 80; other ports get a hint, not captive mode.""" config = {CONF_VERSION: 2, CONF_PORT: 8080} token = fv.full_config.set({"web_server": config, CONF_WIFI: AP_ONLY}) try: with caplog.at_level(logging.WARNING): _final_validate_ap_mode(config) finally: fv.full_config.reset(token) assert "cannot open automatically" in caplog.text assert "http://192.168.4.1:8080/" in caplog.text