"""Test REST data module logging improvements.""" import logging import pytest from homeassistant.components.rest import DOMAIN from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component from tests.test_util.aiohttp import AiohttpClientMocker async def test_rest_data_log_warning_on_error_status( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, caplog: pytest.LogCaptureFixture, ) -> None: """Test that warning is logged for error status codes.""" # Mock a 403 response with HTML content aioclient_mock.get( "http://example.com/api", status=403, text="Access Denied", headers={"Content-Type": "text/html"}, ) assert await async_setup_component( hass, DOMAIN, { DOMAIN: { "resource": "http://example.com/api", "method": "GET", "sensor": [ { "name": "test_sensor", "value_template": "{{ value_json.test }}", } ], } }, ) await hass.async_block_till_done() # Check that warning was logged assert ( "REST request to http://example.com/api returned status 403 " "with text/html response" in caplog.text ) assert "Access Denied" in caplog.text async def test_rest_data_no_warning_on_200_with_wrong_content_type( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, caplog: pytest.LogCaptureFixture, ) -> None: """Test that no warning is logged for 200 status with wrong content.""" # Mock a 200 response with HTML - users might still want to parse this aioclient_mock.get( "http://example.com/api", status=200, text="

This is HTML, not JSON!

", headers={"Content-Type": "text/html; charset=utf-8"}, ) assert await async_setup_component( hass, DOMAIN, { DOMAIN: { "resource": "http://example.com/api", "method": "GET", "sensor": [ { "name": "test_sensor", "value_template": "{{ value }}", } ], } }, ) await hass.async_block_till_done() # Should NOT warn for 200 status, even with HTML content type assert ( "REST request to http://example.com/api returned status 200" not in caplog.text ) async def test_rest_data_no_warning_on_success_json( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, caplog: pytest.LogCaptureFixture, ) -> None: """Test that no warning is logged for successful JSON responses.""" # Mock a successful JSON response aioclient_mock.get( "http://example.com/api", status=200, json={"status": "ok", "value": 42}, headers={"Content-Type": "application/json"}, ) assert await async_setup_component( hass, DOMAIN, { DOMAIN: { "resource": "http://example.com/api", "method": "GET", "sensor": [ { "name": "test_sensor", "value_template": "{{ value_json.value }}", } ], } }, ) await hass.async_block_till_done() # Check that no warning was logged assert "REST request to http://example.com/api returned status" not in caplog.text async def test_rest_data_no_warning_on_success_xml( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, caplog: pytest.LogCaptureFixture, ) -> None: """Test that no warning is logged for successful XML responses.""" # Mock a successful XML response aioclient_mock.get( "http://example.com/api", status=200, text='42', headers={"Content-Type": "application/xml"}, ) assert await async_setup_component( hass, DOMAIN, { DOMAIN: { "resource": "http://example.com/api", "method": "GET", "sensor": [ { "name": "test_sensor", "value_template": "{{ value_json.root.value }}", } ], } }, ) await hass.async_block_till_done() # Check that no warning was logged assert "REST request to http://example.com/api returned status" not in caplog.text async def test_rest_data_warning_truncates_long_responses( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, caplog: pytest.LogCaptureFixture, ) -> None: """Test that warning truncates very long response bodies.""" # Create a very long error message long_message = "Error: " + "x" * 1000 aioclient_mock.get( "http://example.com/api", status=500, text=long_message, headers={"Content-Type": "text/plain"}, ) assert await async_setup_component( hass, DOMAIN, { DOMAIN: { "resource": "http://example.com/api", "method": "GET", "sensor": [ { "name": "test_sensor", "value_template": "{{ value_json.test }}", } ], } }, ) await hass.async_block_till_done() # Check that warning was logged with truncation # Set the logger filter to only check our specific logger caplog.set_level(logging.WARNING, logger="homeassistant.components.rest.data") # Verify the truncated warning appears assert ( "REST request to http://example.com/api returned status 500 " "with text/plain response: Error: " + "x" * 493 + "..." in caplog.text ) async def test_rest_data_debug_logging_shows_response_details( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, caplog: pytest.LogCaptureFixture, ) -> None: """Test that debug logging shows response details.""" caplog.set_level(logging.DEBUG) aioclient_mock.get( "http://example.com/api", status=200, json={"test": "data"}, headers={"Content-Type": "application/json"}, ) assert await async_setup_component( hass, DOMAIN, { DOMAIN: { "resource": "http://example.com/api", "method": "GET", "sensor": [ { "name": "test_sensor", "value_template": "{{ value_json.test }}", } ], } }, ) await hass.async_block_till_done() # Check debug log assert ( "REST response from http://example.com/api: status=200, " "content-type=application/json, length=" in caplog.text ) async def test_rest_data_no_content_type_header( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, caplog: pytest.LogCaptureFixture, ) -> None: """Test handling of responses without Content-Type header.""" caplog.set_level(logging.DEBUG) # Mock response without Content-Type header aioclient_mock.get( "http://example.com/api", status=200, text="plain text response", headers={}, # No Content-Type ) assert await async_setup_component( hass, DOMAIN, { DOMAIN: { "resource": "http://example.com/api", "method": "GET", "sensor": [ { "name": "test_sensor", } ], } }, ) await hass.async_block_till_done() # Check debug log shows "not set" assert "content-type=not set" in caplog.text # No warning for 200 with missing content-type assert "REST request to http://example.com/api returned status" not in caplog.text async def test_rest_data_real_world_bom_blocking_scenario( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, caplog: pytest.LogCaptureFixture, ) -> None: """Test real-world scenario where BOM blocks with HTML response.""" # Mock BOM blocking response bom_block_html = "

Your access is blocked due to automated access

" aioclient_mock.get( "http://www.bom.gov.au/fwo/IDN60901/IDN60901.94767.json", status=403, text=bom_block_html, headers={"Content-Type": "text/html"}, ) assert await async_setup_component( hass, DOMAIN, { DOMAIN: { "resource": ("http://www.bom.gov.au/fwo/IDN60901/IDN60901.94767.json"), "method": "GET", "sensor": [ { "name": "bom_temperature", "value_template": ( "{{ value_json.observations.data[0].air_temp }}" ), } ], } }, ) await hass.async_block_till_done() # Check that warning was logged with clear indication of the issue assert ( "REST request to http://www.bom.gov.au/fwo/IDN60901/" "IDN60901.94767.json returned status 403 with text/html response" ) in caplog.text assert "Your access is blocked" in caplog.text async def test_rest_data_warning_on_html_error( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, caplog: pytest.LogCaptureFixture, ) -> None: """Test that warning is logged for error status with HTML content.""" # Mock a 404 response with HTML error page aioclient_mock.get( "http://example.com/api", status=404, text="

404 Not Found

", headers={"Content-Type": "text/html"}, ) assert await async_setup_component( hass, DOMAIN, { DOMAIN: { "resource": "http://example.com/api", "method": "GET", "sensor": [ { "name": "test_sensor", "value_template": "{{ value_json.test }}", } ], } }, ) await hass.async_block_till_done() # Should warn for error status with HTML assert ( "REST request to http://example.com/api returned status 404 " "with text/html response" in caplog.text ) assert "

404 Not Found

" in caplog.text async def test_rest_data_no_warning_on_json_error( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, caplog: pytest.LogCaptureFixture, ) -> None: """Test POST request that returns JSON error - no warning expected.""" aioclient_mock.post( "http://example.com/api", status=400, text='{"error": "Invalid request payload"}', headers={"Content-Type": "application/json"}, ) assert await async_setup_component( hass, DOMAIN, { DOMAIN: { "resource": "http://example.com/api", "method": "POST", "payload": '{"data": "test"}', "sensor": [ { "name": "test_sensor", "value_template": "{{ value_json.error }}", } ], } }, ) await hass.async_block_till_done() # Should NOT warn for JSON error responses - users can parse these assert ( "REST request to http://example.com/api returned status 400" not in caplog.text ) async def test_rest_data_timeout_error( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, caplog: pytest.LogCaptureFixture, ) -> None: """Test timeout error logging.""" aioclient_mock.get( "http://example.com/api", exc=TimeoutError(), ) assert await async_setup_component( hass, DOMAIN, { DOMAIN: { "resource": "http://example.com/api", "method": "GET", "timeout": 10, "sensor": [ { "name": "test_sensor", "value_template": "{{ value_json.test }}", } ], } }, ) await hass.async_block_till_done() # Check timeout error is logged or platform reports not ready assert ( "Timeout while fetching data: http://example.com/api" in caplog.text or "Platform rest not ready yet" in caplog.text ) async def test_rest_data_boolean_params_converted_to_strings( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, caplog: pytest.LogCaptureFixture, ) -> None: """Test that boolean parameters are converted to lowercase strings.""" # Mock the request and capture the actual URL aioclient_mock.get( "http://example.com/api", status=200, json={"status": "ok"}, headers={"Content-Type": "application/json"}, ) assert await async_setup_component( hass, DOMAIN, { DOMAIN: { "resource": "http://example.com/api", "method": "GET", "params": { "boolTrue": True, "boolFalse": False, "stringParam": "test", "intParam": 123, }, "sensor": [ { "name": "test_sensor", "value_template": "{{ value_json.status }}", } ], } }, ) await hass.async_block_till_done() # Check that the request was made with boolean values converted to strings assert len(aioclient_mock.mock_calls) == 1 method, url, data, headers = aioclient_mock.mock_calls[0] # Check that the URL query parameters have boolean values converted to strings assert url.query["boolTrue"] == "true" assert url.query["boolFalse"] == "false" assert url.query["stringParam"] == "test" assert url.query["intParam"] == "123"