"""Test REST data module logging improvements.""" from datetime import timedelta import logging from unittest.mock import patch from freezegun.api import FrozenDateTimeFactory import pytest from homeassistant.components.rest import DOMAIN from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component from tests.common import async_fire_time_changed 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_with_incorrect_charset_in_header( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, caplog: pytest.LogCaptureFixture, freezer: FrozenDateTimeFactory, ) -> None: """Test that we can handle sites which provides an incorrect charset.""" aioclient_mock.get( "http://example.com/api", status=200, text="

Some html

", headers={"Content-Type": "text/html; charset=utf-8"}, ) assert await async_setup_component( hass, DOMAIN, { DOMAIN: { "resource": "http://example.com/api", "method": "GET", "encoding": "windows-1250", "sensor": [ { "name": "test_sensor", "value_template": "{{ value }}", } ], } }, ) await hass.async_block_till_done() with patch( "tests.test_util.aiohttp.AiohttpClientMockResponse.text", side_effect=UnicodeDecodeError("utf-8", b"", 1, 0, ""), ): freezer.tick(timedelta(minutes=1)) async_fire_time_changed(hass) await hass.async_block_till_done() log_text = "Response charset came back as utf-8 but could not be decoded, continue with configured encoding windows-1250." assert log_text in caplog.text caplog.clear() freezer.tick(timedelta(minutes=1)) async_fire_time_changed(hass) await hass.async_block_till_done() # Only log once as we only try once with automatic decoding assert log_text 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"