"""The tests for generic camera component.""" import asyncio from contextlib import suppress import copy from aiohttp.client_exceptions import ClientResponseError from homeassistant.components.buienradar.const import CONF_COUNTRY, CONF_DELTA, DOMAIN from homeassistant.const import ( CONF_LATITUDE, CONF_LONGITUDE, HTTP_INTERNAL_SERVER_ERROR, ) from homeassistant.helpers.entity_registry import async_get from homeassistant.util import dt as dt_util from tests.common import MockConfigEntry # An infinitesimally small time-delta. EPSILON_DELTA = 0.0000000001 TEST_LATITUDE = 51.5288504 TEST_LONGITUDE = 5.4002156 TEST_CFG_DATA = {CONF_LATITUDE: TEST_LATITUDE, CONF_LONGITUDE: TEST_LONGITUDE} def radar_map_url(country_code: str = "NL") -> str: """Build map URL.""" return f"https://api.buienradar.nl/image/1.0/RadarMap{country_code}?w=700&h=700" async def _setup_config_entry(hass, entry): entity_registry = async_get(hass) entity_registry.async_get_or_create( domain="camera", platform="buienradar", unique_id=f"{TEST_LATITUDE:2.6f}{TEST_LONGITUDE:2.6f}", config_entry=entry, original_name="Buienradar", ) await hass.async_block_till_done() await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() async def test_fetching_url_and_caching(aioclient_mock, hass, hass_client): """Test that it fetches the given url.""" aioclient_mock.get(radar_map_url(), text="hello world") mock_entry = MockConfigEntry(domain=DOMAIN, unique_id="TEST_ID", data=TEST_CFG_DATA) mock_entry.add_to_hass(hass) await _setup_config_entry(hass, mock_entry) client = await hass_client() resp = await client.get("/api/camera_proxy/camera.buienradar_51_5288505_400216") assert resp.status == 200 assert aioclient_mock.call_count == 1 body = await resp.text() assert body == "hello world" # default delta is 600s -> should be the same when calling immediately # afterwards. resp = await client.get("/api/camera_proxy/camera.buienradar_51_5288505_400216") assert aioclient_mock.call_count == 1 async def test_expire_delta(aioclient_mock, hass, hass_client): """Test that the cache expires after delta.""" aioclient_mock.get(radar_map_url(), text="hello world") options = {CONF_DELTA: EPSILON_DELTA} mock_entry = MockConfigEntry( domain=DOMAIN, unique_id="TEST_ID", data=TEST_CFG_DATA, options=options ) mock_entry.add_to_hass(hass) await _setup_config_entry(hass, mock_entry) client = await hass_client() resp = await client.get("/api/camera_proxy/camera.buienradar_51_5288505_400216") assert resp.status == 200 assert aioclient_mock.call_count == 1 body = await resp.text() assert body == "hello world" await asyncio.sleep(EPSILON_DELTA) # tiny delta has passed -> should immediately call again resp = await client.get("/api/camera_proxy/camera.buienradar_51_5288505_400216") assert aioclient_mock.call_count == 2 async def test_only_one_fetch_at_a_time(aioclient_mock, hass, hass_client): """Test that it fetches with only one request at the same time.""" aioclient_mock.get(radar_map_url(), text="hello world") mock_entry = MockConfigEntry(domain=DOMAIN, unique_id="TEST_ID", data=TEST_CFG_DATA) mock_entry.add_to_hass(hass) await _setup_config_entry(hass, mock_entry) client = await hass_client() resp_1 = client.get("/api/camera_proxy/camera.buienradar_51_5288505_400216") resp_2 = client.get("/api/camera_proxy/camera.buienradar_51_5288505_400216") resp = await resp_1 resp_2 = await resp_2 assert (await resp.text()) == (await resp_2.text()) assert aioclient_mock.call_count == 1 async def test_belgium_country(aioclient_mock, hass, hass_client): """Test that it actually adheres to another country like Belgium.""" aioclient_mock.get(radar_map_url(country_code="BE"), text="hello world") data = copy.deepcopy(TEST_CFG_DATA) data[CONF_COUNTRY] = "BE" mock_entry = MockConfigEntry(domain=DOMAIN, unique_id="TEST_ID", data=data) mock_entry.add_to_hass(hass) await _setup_config_entry(hass, mock_entry) client = await hass_client() await client.get("/api/camera_proxy/camera.buienradar_51_5288505_400216") assert aioclient_mock.call_count == 1 async def test_failure_response_not_cached(aioclient_mock, hass, hass_client): """Test that it does not cache a failure response.""" aioclient_mock.get(radar_map_url(), text="hello world", status=401) mock_entry = MockConfigEntry(domain=DOMAIN, unique_id="TEST_ID", data=TEST_CFG_DATA) mock_entry.add_to_hass(hass) await _setup_config_entry(hass, mock_entry) client = await hass_client() await client.get("/api/camera_proxy/camera.buienradar_51_5288505_400216") await client.get("/api/camera_proxy/camera.buienradar_51_5288505_400216") assert aioclient_mock.call_count == 2 async def test_last_modified_updates(aioclient_mock, hass, hass_client): """Test that it does respect HTTP not modified.""" # Build Last-Modified header value now = dt_util.utcnow() last_modified = now.strftime("%a, %d %m %Y %H:%M:%S GMT") aioclient_mock.get( radar_map_url(), text="hello world", status=200, headers={"Last-Modified": last_modified}, ) options = {CONF_DELTA: EPSILON_DELTA} mock_entry = MockConfigEntry( domain=DOMAIN, unique_id="TEST_ID", data=TEST_CFG_DATA, options=options ) mock_entry.add_to_hass(hass) await _setup_config_entry(hass, mock_entry) client = await hass_client() resp_1 = await client.get("/api/camera_proxy/camera.buienradar_51_5288505_400216") # It is not possible to check if header was sent. assert aioclient_mock.call_count == 1 await asyncio.sleep(EPSILON_DELTA) # Content has expired, change response to a 304 NOT MODIFIED, which has no # text, i.e. old value should be kept aioclient_mock.clear_requests() # mock call count is now reset as well: assert aioclient_mock.call_count == 0 aioclient_mock.get(radar_map_url(), text=None, status=304) resp_2 = await client.get("/api/camera_proxy/camera.buienradar_51_5288505_400216") assert aioclient_mock.call_count == 1 assert (await resp_1.read()) == (await resp_2.read()) async def test_retries_after_error(aioclient_mock, hass, hass_client): """Test that it does retry after an error instead of caching.""" mock_entry = MockConfigEntry(domain=DOMAIN, unique_id="TEST_ID", data=TEST_CFG_DATA) mock_entry.add_to_hass(hass) await _setup_config_entry(hass, mock_entry) client = await hass_client() aioclient_mock.get(radar_map_url(), text=None, status=HTTP_INTERNAL_SERVER_ERROR) # A 404 should not return data and throw: with suppress(ClientResponseError): await client.get("/api/camera_proxy/camera.buienradar_51_5288505_400216") assert aioclient_mock.call_count == 1 # Change the response to a 200 aioclient_mock.clear_requests() aioclient_mock.get(radar_map_url(), text="DEADBEEF") assert aioclient_mock.call_count == 0 # http error should not be cached, immediate retry. resp_2 = await client.get("/api/camera_proxy/camera.buienradar_51_5288505_400216") assert aioclient_mock.call_count == 1 # Binary text can not be added as body to `aioclient_mock.get(text=...)`, # while `resp.read()` returns bytes, encode the value. assert (await resp_2.read()) == b"DEADBEEF"