core/tests/components/rest/test_data.py

494 lines
14 KiB
Python

"""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="<html><body>Access Denied</body></html>",
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 "<html><body>Access Denied</body></html>" 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="<p>This is HTML, not JSON!</p>",
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='<?xml version="1.0"?><root><value>42</value></root>',
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 = "<p>Your access is blocked due to automated access</p>"
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="<html><body><h1>404 Not Found</h1></body></html>",
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 "<html><body><h1>404 Not Found</h1></body></html>" 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"