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:
		
							
								
								
									
										14
									
								
								tests/integration/fixtures/host_mode_api_password.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								tests/integration/fixtures/host_mode_api_password.yaml
									
									
									
									
									
										Normal 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 | ||||
							
								
								
									
										43
									
								
								tests/integration/fixtures/scheduler_null_name.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								tests/integration/fixtures/scheduler_null_name.yaml
									
									
									
									
									
										Normal 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"); | ||||
|             }); | ||||
							
								
								
									
										53
									
								
								tests/integration/test_host_mode_api_password.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										53
									
								
								tests/integration/test_host_mode_api_password.py
									
									
									
									
									
										Normal 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 | ||||
							
								
								
									
										59
									
								
								tests/integration/test_scheduler_null_name.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										59
									
								
								tests/integration/test_scheduler_null_name.py
									
									
									
									
									
										Normal 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" | ||||
|                 ) | ||||
		Reference in New Issue
	
	Block a user