From 005d4354d5fd6c18f847c56f089df5d6c40b657c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 11 Jul 2025 10:14:59 -1000 Subject: [PATCH] test this --- esphome/components/api/api_connection.cpp | 3 +- .../fixtures/host_mode_api_password.yaml | 27 ++++++++++ .../test_host_mode_api_password.py | 51 +++++++++++++++++++ 3 files changed, 79 insertions(+), 2 deletions(-) create mode 100644 tests/integration/fixtures/host_mode_api_password.yaml create mode 100644 tests/integration/test_host_mode_api_password.py diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 3ea88224ed..b83aadb2b8 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -1425,7 +1425,7 @@ void APIConnection::complete_authentication_() { } this->flags_.connection_state = static_cast(ConnectionState::AUTHENTICATED); - ESP_LOGD(TAG, "%s connected (no password)", this->get_client_combined_info().c_str()); + ESP_LOGD(TAG, "%s connected", this->get_client_combined_info().c_str()); #ifdef USE_API_CLIENT_CONNECTED_TRIGGER this->parent_->get_client_connected_trigger()->trigger(this->client_info_, this->client_peername_); #endif @@ -1471,7 +1471,6 @@ ConnectResponse APIConnection::connect(const ConnectRequest &msg) { // bool invalid_password = 1; resp.invalid_password = !correct; if (correct) { - ESP_LOGD(TAG, "%s connected", this->get_client_combined_info().c_str()); this->complete_authentication_(); } return resp; diff --git a/tests/integration/fixtures/host_mode_api_password.yaml b/tests/integration/fixtures/host_mode_api_password.yaml new file mode 100644 index 0000000000..cae7dd3a85 --- /dev/null +++ b/tests/integration/fixtures/host_mode_api_password.yaml @@ -0,0 +1,27 @@ +esphome: + name: ${name} + build_path: ${build_path} + friendly_name: ESPHome Host Mode API Password Test + name_add_mac_suffix: no + area: Entryway + platformio_options: + build_flags: + - -std=gnu++17 + - -Wall + build_unflags: + - -std=gnu++11 + +api: + password: "test_password_123" + +logger: + level: DEBUG + +# Test sensor to verify connection works +sensor: + - platform: template + name: Test Sensor + id: test_sensor + lambda: |- + return 42.0; + update_interval: 1s diff --git a/tests/integration/test_host_mode_api_password.py b/tests/integration/test_host_mode_api_password.py new file mode 100644 index 0000000000..d602dd56f9 --- /dev/null +++ b/tests/integration/test_host_mode_api_password.py @@ -0,0 +1,51 @@ +"""Integration test for API password authentication.""" + +from __future__ import annotations + +import asyncio + +from aioesphomeapi import APIConnectionError +import pytest + +from .types import APIClientConnectedFactory, RunCompiledFunction + + +@pytest.mark.asyncio +async def test_host_mode_api_password( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test API authentication with password.""" + async with run_compiled(yaml_config): + # First, try to connect without password - should fail + with pytest.raises(APIConnectionError, match="Authentication"): + async with api_client_connected(password=""): + pass # Should not reach here + + # Now connect with correct password + async with api_client_connected(password="test_password_123") as client: + # Verify we can get device info + device_info = await client.device_info() + assert device_info is not None + assert device_info.uses_password is True + assert device_info.name == "host-mode-api-password" + + # Subscribe to states to ensure authenticated connection works + states = {} + + def on_state(state): + states[state.key] = state + + await client.subscribe_states(on_state) + + # Wait a bit to receive the test sensor state + await asyncio.sleep(0.5) + + # Should have received at least one state (the test sensor) + assert len(states) > 0 + + # Test with wrong password - should fail + with pytest.raises(APIConnectionError, match="Authentication"): + async with api_client_connected(password="wrong_password"): + pass # Should not reach here