1
0
mirror of https://github.com/esphome/esphome.git synced 2025-10-31 07:03:55 +00:00

Merge branch 'auto_auth' into integration

This commit is contained in:
J. Nick Koston
2025-07-11 11:54:23 -10:00
39 changed files with 1668 additions and 200 deletions

View File

@@ -0,0 +1,14 @@
esphome:
name: host-mode-api-password
host:
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: 0.1s

View File

@@ -0,0 +1,43 @@
esphome:
name: scheduler-null-name
host:
logger:
level: DEBUG
api:
services:
- service: test_null_name
then:
- lambda: |-
// First, create a scenario that would trigger the crash
// The crash happens when defer() is called with a name that would be cancelled
// Test 1: Create a defer with a valid name
App.scheduler.set_timeout(nullptr, "test_defer", 0, []() {
ESP_LOGI("TEST", "First defer should be cancelled");
});
// Test 2: Create another defer with the same name - this triggers cancel_item_locked_
// In the unfixed code, this would crash if the name was NULL
App.scheduler.set_timeout(nullptr, "test_defer", 0, []() {
ESP_LOGI("TEST", "Second defer executed");
});
// Test 3: Now test with nullptr - this is the actual crash scenario
// Create a defer item without a name (like voice assistant does)
const char* null_name = nullptr;
App.scheduler.set_timeout(nullptr, null_name, 0, []() {
ESP_LOGI("TEST", "Defer with null name executed");
});
// Test 4: Create another defer with null name - this would trigger the crash
App.scheduler.set_timeout(nullptr, null_name, 0, []() {
ESP_LOGI("TEST", "Second null defer executed");
});
// Test 5: Verify scheduler still works
App.scheduler.set_timeout(nullptr, "valid_timeout", 50, []() {
ESP_LOGI("TEST", "Test completed successfully");
});

View File

@@ -0,0 +1,53 @@
"""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):
# 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
loop = asyncio.get_running_loop()
state_future: asyncio.Future[bool] = loop.create_future()
states = {}
def on_state(state):
states[state.key] = state
if not state_future.done():
state_future.set_result(True)
client.subscribe_states(on_state)
# Wait for at least one state with timeout
try:
await asyncio.wait_for(state_future, timeout=5.0)
except asyncio.TimeoutError:
pytest.fail("No states received within timeout")
# 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="Invalid password"):
async with api_client_connected(password="wrong_password"):
pass # Should not reach here

View File

@@ -0,0 +1,59 @@
"""Test that scheduler handles NULL names safely without crashing."""
import asyncio
import re
import pytest
from .types import APIClientConnectedFactory, RunCompiledFunction
@pytest.mark.asyncio
async def test_scheduler_null_name(
yaml_config: str,
run_compiled: RunCompiledFunction,
api_client_connected: APIClientConnectedFactory,
) -> None:
"""Test that scheduler handles NULL names safely without crashing."""
loop = asyncio.get_running_loop()
test_complete_future: asyncio.Future[bool] = loop.create_future()
# Pattern to match test completion
test_complete_pattern = re.compile(r"Test completed successfully")
def check_output(line: str) -> None:
"""Check log output for test completion."""
if not test_complete_future.done() and test_complete_pattern.search(line):
test_complete_future.set_result(True)
async with run_compiled(yaml_config, line_callback=check_output):
async with api_client_connected() as client:
# Verify we can connect
device_info = await client.device_info()
assert device_info is not None
assert device_info.name == "scheduler-null-name"
# List services
_, services = await asyncio.wait_for(
client.list_entities_services(), timeout=5.0
)
# Find our test service
test_null_name_service = next(
(s for s in services if s.name == "test_null_name"), None
)
assert test_null_name_service is not None, (
"test_null_name service not found"
)
# Execute the test
client.execute_service(test_null_name_service, {})
# Wait for test completion
try:
await asyncio.wait_for(test_complete_future, timeout=10.0)
except asyncio.TimeoutError:
pytest.fail(
"Test did not complete within timeout - likely crashed due to NULL name"
)