Add reauth and reconfigure to paperless (#145469)

* Add reauth and reconfigure

* Reauth and reconfigure in different functions

* Add duplicate check

* Add test for reconfigure duplicate

* Removed seconds config entry fixture
This commit is contained in:
Florian von Garrel 2025-05-25 14:03:13 +02:00 committed by GitHub
parent d0bc71752b
commit 8c971904ca
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 319 additions and 63 deletions

View File

@ -2,6 +2,7 @@
from __future__ import annotations from __future__ import annotations
from collections.abc import Mapping
from typing import Any from typing import Any
from pypaperless import Paperless from pypaperless import Paperless
@ -36,6 +37,7 @@ class PaperlessConfigFlow(ConfigFlow, domain=DOMAIN):
) -> ConfigFlowResult: ) -> ConfigFlowResult:
"""Handle the initial step.""" """Handle the initial step."""
errors: dict[str, str] = {}
if user_input is not None: if user_input is not None:
self._async_abort_entries_match( self._async_abort_entries_match(
{ {
@ -44,31 +46,9 @@ class PaperlessConfigFlow(ConfigFlow, domain=DOMAIN):
} }
) )
errors: dict[str, str] = {} errors = await self._validate_input(user_input)
if user_input is not None:
client = Paperless(
user_input[CONF_URL],
user_input[CONF_API_KEY],
session=async_get_clientsession(self.hass),
)
try: if not errors:
await client.initialize()
await client.statistics()
except PaperlessConnectionError:
errors[CONF_URL] = "cannot_connect"
except PaperlessInvalidTokenError:
errors[CONF_API_KEY] = "invalid_api_key"
except PaperlessInactiveOrDeletedError:
errors[CONF_API_KEY] = "user_inactive_or_deleted"
except PaperlessForbiddenError:
errors[CONF_API_KEY] = "forbidden"
except InitializationError:
errors[CONF_URL] = "cannot_connect"
except Exception as err: # noqa: BLE001
LOGGER.exception("Unexpected exception: %s", err)
errors["base"] = "unknown"
else:
return self.async_create_entry( return self.async_create_entry(
title=user_input[CONF_URL], data=user_input title=user_input[CONF_URL], data=user_input
) )
@ -76,3 +56,96 @@ class PaperlessConfigFlow(ConfigFlow, domain=DOMAIN):
return self.async_show_form( return self.async_show_form(
step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors
) )
async def async_step_reconfigure(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Reconfigure flow for Paperless-ngx integration."""
entry = self._get_reconfigure_entry()
errors: dict[str, str] = {}
if user_input is not None:
self._async_abort_entries_match(
{
CONF_URL: user_input[CONF_URL],
CONF_API_KEY: user_input[CONF_API_KEY],
}
)
errors = await self._validate_input(user_input)
if not errors:
return self.async_update_reload_and_abort(entry, data=user_input)
return self.async_show_form(
step_id="reconfigure",
data_schema=self.add_suggested_values_to_schema(
data_schema=STEP_USER_DATA_SCHEMA,
suggested_values={
CONF_URL: user_input[CONF_URL]
if user_input is not None
else entry.data[CONF_URL],
},
),
errors=errors,
)
async def async_step_reauth(
self, user_input: Mapping[str, Any]
) -> ConfigFlowResult:
"""Handle re-auth."""
return await self.async_step_reauth_confirm()
async def async_step_reauth_confirm(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Reauth flow for Paperless-ngx integration."""
entry = self._get_reauth_entry()
errors: dict[str, str] = {}
if user_input is not None:
updated_data = {**entry.data, CONF_API_KEY: user_input[CONF_API_KEY]}
errors = await self._validate_input(updated_data)
if not errors:
return self.async_update_reload_and_abort(
entry,
data=updated_data,
)
return self.async_show_form(
step_id="reauth_confirm",
data_schema=vol.Schema({vol.Required(CONF_API_KEY): str}),
errors=errors,
)
async def _validate_input(self, user_input: dict[str, str]) -> dict[str, str]:
errors: dict[str, str] = {}
client = Paperless(
user_input[CONF_URL],
user_input[CONF_API_KEY],
session=async_get_clientsession(self.hass),
)
try:
await client.initialize()
await client.statistics() # test permissions on api
except PaperlessConnectionError:
errors[CONF_URL] = "cannot_connect"
except PaperlessInvalidTokenError:
errors[CONF_API_KEY] = "invalid_api_key"
except PaperlessInactiveOrDeletedError:
errors[CONF_API_KEY] = "user_inactive_or_deleted"
except PaperlessForbiddenError:
errors[CONF_API_KEY] = "forbidden"
except InitializationError:
errors[CONF_URL] = "cannot_connect"
except Exception as err: # noqa: BLE001
LOGGER.exception("Unexpected exception: %s", err)
errors["base"] = "unknown"
return errors

View File

@ -17,7 +17,11 @@ from pypaperless.models import Statistic
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_API_KEY, CONF_URL from homeassistant.const import CONF_API_KEY, CONF_URL
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryError, ConfigEntryNotReady from homeassistant.exceptions import (
ConfigEntryAuthFailed,
ConfigEntryError,
ConfigEntryNotReady,
)
from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
@ -63,12 +67,12 @@ class PaperlessCoordinator(DataUpdateCoordinator[Statistic]):
translation_key="cannot_connect", translation_key="cannot_connect",
) from err ) from err
except PaperlessInvalidTokenError as err: except PaperlessInvalidTokenError as err:
raise ConfigEntryError( raise ConfigEntryAuthFailed(
translation_domain=DOMAIN, translation_domain=DOMAIN,
translation_key="invalid_api_key", translation_key="invalid_api_key",
) from err ) from err
except PaperlessInactiveOrDeletedError as err: except PaperlessInactiveOrDeletedError as err:
raise ConfigEntryError( raise ConfigEntryAuthFailed(
translation_domain=DOMAIN, translation_domain=DOMAIN,
translation_key="user_inactive_or_deleted", translation_key="user_inactive_or_deleted",
) from err ) from err
@ -98,12 +102,12 @@ class PaperlessCoordinator(DataUpdateCoordinator[Statistic]):
translation_key="forbidden", translation_key="forbidden",
) from err ) from err
except PaperlessInvalidTokenError as err: except PaperlessInvalidTokenError as err:
raise ConfigEntryError( raise ConfigEntryAuthFailed(
translation_domain=DOMAIN, translation_domain=DOMAIN,
translation_key="invalid_api_key", translation_key="invalid_api_key",
) from err ) from err
except PaperlessInactiveOrDeletedError as err: except PaperlessInactiveOrDeletedError as err:
raise ConfigEntryError( raise ConfigEntryAuthFailed(
translation_domain=DOMAIN, translation_domain=DOMAIN,
translation_key="user_inactive_or_deleted", translation_key="user_inactive_or_deleted",
) from err ) from err

View File

@ -7,6 +7,6 @@
"integration_type": "service", "integration_type": "service",
"iot_class": "local_polling", "iot_class": "local_polling",
"loggers": ["pypaperless"], "loggers": ["pypaperless"],
"quality_scale": "bronze", "quality_scale": "silver",
"requirements": ["pypaperless==4.1.0"] "requirements": ["pypaperless==4.1.0"]
} }

View File

@ -38,7 +38,7 @@ rules:
integration-owner: done integration-owner: done
log-when-unavailable: done log-when-unavailable: done
parallel-updates: done parallel-updates: done
reauthentication-flow: todo reauthentication-flow: done
test-coverage: done test-coverage: done
# Gold # Gold
@ -66,7 +66,7 @@ rules:
entity-translations: done entity-translations: done
exception-translations: done exception-translations: done
icon-translations: done icon-translations: done
reconfiguration-flow: todo reconfiguration-flow: done
repair-issues: todo repair-issues: todo
stale-devices: stale-devices:
status: exempt status: exempt

View File

@ -11,6 +11,26 @@
"api_key": "API key to connect to the Paperless-ngx API" "api_key": "API key to connect to the Paperless-ngx API"
}, },
"title": "Add Paperless-ngx instance" "title": "Add Paperless-ngx instance"
},
"reauth_confirm": {
"data": {
"api_key": "[%key:common::config_flow::data::api_key%]"
},
"data_description": {
"api_key": "[%key:component::paperless_ngx::config::step::user::data_description::api_key%]"
},
"title": "Re-auth Paperless-ngx instance"
},
"reconfigure": {
"data": {
"url": "[%key:common::config_flow::data::url%]",
"api_key": "[%key:common::config_flow::data::api_key%]"
},
"data_description": {
"url": "[%key:component::paperless_ngx::config::step::user::data_description::url%]",
"api_key": "[%key:component::paperless_ngx::config::step::user::data_description::api_key%]"
},
"title": "Reconfigure Paperless-ngx instance"
} }
}, },
"error": { "error": {
@ -21,7 +41,9 @@
"unknown": "[%key:common::config_flow::error::unknown%]" "unknown": "[%key:common::config_flow::error::unknown%]"
}, },
"abort": { "abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_service%]" "already_configured": "[%key:common::config_flow::abort::already_configured_service%]",
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]",
"reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]"
} }
}, },
"entity": { "entity": {

View File

@ -11,7 +11,7 @@ from homeassistant.components.paperless_ngx.const import DOMAIN
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from . import setup_integration from . import setup_integration
from .const import USER_INPUT from .const import USER_INPUT_ONE
from tests.common import MockConfigEntry, load_fixture from tests.common import MockConfigEntry, load_fixture
@ -59,10 +59,10 @@ def mock_paperless(mock_statistic_data: MagicMock) -> Generator[AsyncMock]:
def mock_config_entry() -> MockConfigEntry: def mock_config_entry() -> MockConfigEntry:
"""Return the default mocked config entry.""" """Return the default mocked config entry."""
return MockConfigEntry( return MockConfigEntry(
entry_id="paperless_ngx_test", entry_id="0KLG00V55WEVTJ0CJHM0GADNGH",
title="Paperless-ngx", title="Paperless-ngx",
domain=DOMAIN, domain=DOMAIN,
data=USER_INPUT, data=USER_INPUT_ONE,
) )

View File

@ -2,7 +2,14 @@
from homeassistant.const import CONF_API_KEY, CONF_URL from homeassistant.const import CONF_API_KEY, CONF_URL
USER_INPUT = { USER_INPUT_ONE = {
CONF_URL: "https://192.168.69.16:8000", CONF_URL: "https://192.168.69.16:8000",
CONF_API_KEY: "test_token", CONF_API_KEY: "12345678",
} }
USER_INPUT_TWO = {
CONF_URL: "https://paperless.example.de",
CONF_API_KEY: "87654321",
}
USER_INPUT_REAUTH = {CONF_API_KEY: "192837465"}

View File

@ -31,7 +31,7 @@
'previous_unique_id': None, 'previous_unique_id': None,
'supported_features': 0, 'supported_features': 0,
'translation_key': 'correspondent_count', 'translation_key': 'correspondent_count',
'unique_id': 'paperless_ngx_test_correspondent_count', 'unique_id': '0KLG00V55WEVTJ0CJHM0GADNGH_correspondent_count',
'unit_of_measurement': 'correspondents', 'unit_of_measurement': 'correspondents',
}) })
# --- # ---
@ -82,7 +82,7 @@
'previous_unique_id': None, 'previous_unique_id': None,
'supported_features': 0, 'supported_features': 0,
'translation_key': 'document_type_count', 'translation_key': 'document_type_count',
'unique_id': 'paperless_ngx_test_document_type_count', 'unique_id': '0KLG00V55WEVTJ0CJHM0GADNGH_document_type_count',
'unit_of_measurement': 'document types', 'unit_of_measurement': 'document types',
}) })
# --- # ---
@ -133,7 +133,7 @@
'previous_unique_id': None, 'previous_unique_id': None,
'supported_features': 0, 'supported_features': 0,
'translation_key': 'documents_inbox', 'translation_key': 'documents_inbox',
'unique_id': 'paperless_ngx_test_documents_inbox', 'unique_id': '0KLG00V55WEVTJ0CJHM0GADNGH_documents_inbox',
'unit_of_measurement': 'documents', 'unit_of_measurement': 'documents',
}) })
# --- # ---
@ -184,7 +184,7 @@
'previous_unique_id': None, 'previous_unique_id': None,
'supported_features': 0, 'supported_features': 0,
'translation_key': 'tag_count', 'translation_key': 'tag_count',
'unique_id': 'paperless_ngx_test_tag_count', 'unique_id': '0KLG00V55WEVTJ0CJHM0GADNGH_tag_count',
'unit_of_measurement': 'tags', 'unit_of_measurement': 'tags',
}) })
# --- # ---
@ -235,7 +235,7 @@
'previous_unique_id': None, 'previous_unique_id': None,
'supported_features': 0, 'supported_features': 0,
'translation_key': 'characters_count', 'translation_key': 'characters_count',
'unique_id': 'paperless_ngx_test_characters_count', 'unique_id': '0KLG00V55WEVTJ0CJHM0GADNGH_characters_count',
'unit_of_measurement': 'characters', 'unit_of_measurement': 'characters',
}) })
# --- # ---
@ -286,7 +286,7 @@
'previous_unique_id': None, 'previous_unique_id': None,
'supported_features': 0, 'supported_features': 0,
'translation_key': 'documents_total', 'translation_key': 'documents_total',
'unique_id': 'paperless_ngx_test_documents_total', 'unique_id': '0KLG00V55WEVTJ0CJHM0GADNGH_documents_total',
'unit_of_measurement': 'documents', 'unit_of_measurement': 'documents',
}) })
# --- # ---

View File

@ -19,7 +19,7 @@ from homeassistant.const import CONF_API_KEY, CONF_URL
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import FlowResultType from homeassistant.data_entry_flow import FlowResultType
from .const import USER_INPUT from .const import USER_INPUT_ONE, USER_INPUT_REAUTH, USER_INPUT_TWO
from tests.common import MockConfigEntry, patch from tests.common import MockConfigEntry, patch
@ -46,13 +46,58 @@ async def test_full_config_flow(hass: HomeAssistant) -> None:
result = await hass.config_entries.flow.async_configure( result = await hass.config_entries.flow.async_configure(
result["flow_id"], result["flow_id"],
USER_INPUT, USER_INPUT_ONE,
) )
config_entry = result["result"] config_entry = result["result"]
assert config_entry.title == USER_INPUT[CONF_URL] assert config_entry.title == USER_INPUT_ONE[CONF_URL]
assert result["type"] is FlowResultType.CREATE_ENTRY assert result["type"] is FlowResultType.CREATE_ENTRY
assert config_entry.data == USER_INPUT assert config_entry.data == USER_INPUT_ONE
async def test_full_reauth_flow(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_setup_entry: AsyncMock,
) -> None:
"""Test reauth an integration and finishing flow works."""
mock_config_entry.add_to_hass(hass)
reauth_flow = await mock_config_entry.start_reauth_flow(hass)
assert reauth_flow["type"] is FlowResultType.FORM
assert reauth_flow["step_id"] == "reauth_confirm"
result_configure = await hass.config_entries.flow.async_configure(
reauth_flow["flow_id"], USER_INPUT_REAUTH
)
assert result_configure["type"] is FlowResultType.ABORT
assert result_configure["reason"] == "reauth_successful"
assert mock_config_entry.data[CONF_API_KEY] == USER_INPUT_REAUTH[CONF_API_KEY]
async def test_full_reconfigure_flow(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_setup_entry: AsyncMock,
) -> None:
"""Test reconfigure an integration and finishing flow works."""
mock_config_entry.add_to_hass(hass)
reconfigure_flow = await mock_config_entry.start_reconfigure_flow(hass)
assert reconfigure_flow["type"] is FlowResultType.FORM
assert reconfigure_flow["step_id"] == "reconfigure"
result_configure = await hass.config_entries.flow.async_configure(
reconfigure_flow["flow_id"],
USER_INPUT_TWO,
)
assert result_configure["type"] is FlowResultType.ABORT
assert result_configure["reason"] == "reconfigure_successful"
assert mock_config_entry.data == USER_INPUT_TWO
@pytest.mark.parametrize( @pytest.mark.parametrize(
@ -78,7 +123,7 @@ async def test_config_flow_error_handling(
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
DOMAIN, DOMAIN,
context={"source": SOURCE_USER}, context={"source": SOURCE_USER},
data=USER_INPUT, data=USER_INPUT_ONE,
) )
assert result["type"] is FlowResultType.FORM assert result["type"] is FlowResultType.FORM
@ -89,12 +134,87 @@ async def test_config_flow_error_handling(
result = await hass.config_entries.flow.async_configure( result = await hass.config_entries.flow.async_configure(
result["flow_id"], result["flow_id"],
user_input=USER_INPUT, user_input=USER_INPUT_ONE,
) )
assert result["type"] is FlowResultType.CREATE_ENTRY assert result["type"] is FlowResultType.CREATE_ENTRY
assert result["title"] == USER_INPUT[CONF_URL] assert result["title"] == USER_INPUT_ONE[CONF_URL]
assert result["data"] == USER_INPUT assert result["data"] == USER_INPUT_ONE
@pytest.mark.parametrize(
("side_effect", "expected_error"),
[
(PaperlessConnectionError(), {CONF_URL: "cannot_connect"}),
(PaperlessInvalidTokenError(), {CONF_API_KEY: "invalid_api_key"}),
(PaperlessInactiveOrDeletedError(), {CONF_API_KEY: "user_inactive_or_deleted"}),
(PaperlessForbiddenError(), {CONF_API_KEY: "forbidden"}),
(InitializationError(), {CONF_URL: "cannot_connect"}),
(Exception("BOOM!"), {"base": "unknown"}),
],
)
async def test_reauth_flow_error_handling(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_paperless: AsyncMock,
side_effect: Exception,
expected_error: str,
) -> None:
"""Test reauth flow with various initialization errors."""
mock_config_entry.add_to_hass(hass)
mock_paperless.initialize.side_effect = side_effect
reauth_flow = await mock_config_entry.start_reauth_flow(hass)
assert reauth_flow["type"] is FlowResultType.FORM
assert reauth_flow["step_id"] == "reauth_confirm"
result_configure = await hass.config_entries.flow.async_configure(
reauth_flow["flow_id"], USER_INPUT_REAUTH
)
await hass.async_block_till_done()
assert result_configure["type"] is FlowResultType.FORM
assert result_configure["errors"] == expected_error
@pytest.mark.parametrize(
("side_effect", "expected_error"),
[
(PaperlessConnectionError(), {CONF_URL: "cannot_connect"}),
(PaperlessInvalidTokenError(), {CONF_API_KEY: "invalid_api_key"}),
(PaperlessInactiveOrDeletedError(), {CONF_API_KEY: "user_inactive_or_deleted"}),
(PaperlessForbiddenError(), {CONF_API_KEY: "forbidden"}),
(InitializationError(), {CONF_URL: "cannot_connect"}),
(Exception("BOOM!"), {"base": "unknown"}),
],
)
async def test_reconfigure_flow_error_handling(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_paperless: AsyncMock,
side_effect: Exception,
expected_error: str,
) -> None:
"""Test reconfigure flow with various initialization errors."""
mock_config_entry.add_to_hass(hass)
mock_paperless.initialize.side_effect = side_effect
reauth_flow = await mock_config_entry.start_reconfigure_flow(hass)
assert reauth_flow["type"] is FlowResultType.FORM
assert reauth_flow["step_id"] == "reconfigure"
result_configure = await hass.config_entries.flow.async_configure(
reauth_flow["flow_id"],
USER_INPUT_TWO,
)
await hass.async_block_till_done()
assert result_configure["type"] is FlowResultType.FORM
assert result_configure["errors"] == expected_error
async def test_config_already_exists( async def test_config_already_exists(
@ -105,8 +225,36 @@ async def test_config_already_exists(
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
DOMAIN, DOMAIN,
data=USER_INPUT, data=USER_INPUT_ONE,
context={"source": config_entries.SOURCE_USER}, context={"source": config_entries.SOURCE_USER},
) )
assert result["type"] is FlowResultType.ABORT assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "already_configured" assert result["reason"] == "already_configured"
async def test_config_already_exists_reconfigure(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_setup_entry: AsyncMock,
) -> None:
"""Test we only allow a single config if reconfiguring an entry."""
mock_config_entry.add_to_hass(hass)
mock_config_entry_two = MockConfigEntry(
entry_id="J87G00V55WEVTJ0CJHM0GADBH5",
title="Paperless-ngx - Two",
domain=DOMAIN,
data=USER_INPUT_TWO,
)
mock_config_entry_two.add_to_hass(hass)
reconfigure_flow = await mock_config_entry_two.start_reconfigure_flow(hass)
assert reconfigure_flow["type"] is FlowResultType.FORM
assert reconfigure_flow["step_id"] == "reconfigure"
result_configure = await hass.config_entries.flow.async_configure(
reconfigure_flow["flow_id"],
USER_INPUT_ONE,
)
assert result_configure["type"] is FlowResultType.ABORT
assert result_configure["reason"] == "already_configured"

View File

@ -12,6 +12,7 @@ from pypaperless.exceptions import (
from pypaperless.models import Statistic from pypaperless.models import Statistic
import pytest import pytest
from homeassistant.components.paperless_ngx.coordinator import UPDATE_INTERVAL
from homeassistant.const import STATE_UNAVAILABLE, Platform from homeassistant.const import STATE_UNAVAILABLE, Platform
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er from homeassistant.helpers import entity_registry as er
@ -60,7 +61,7 @@ async def test_statistic_sensor_state(
) )
) )
freezer.tick(timedelta(seconds=120)) freezer.tick(timedelta(seconds=UPDATE_INTERVAL))
async_fire_time_changed(hass) async_fire_time_changed(hass)
await hass.async_block_till_done() await hass.async_block_till_done()
@ -70,12 +71,12 @@ async def test_statistic_sensor_state(
@pytest.mark.usefixtures("init_integration") @pytest.mark.usefixtures("init_integration")
@pytest.mark.parametrize( @pytest.mark.parametrize(
"error_cls", ("error_cls", "assert_state"),
[ [
PaperlessForbiddenError, (PaperlessForbiddenError, "420"),
PaperlessConnectionError, (PaperlessConnectionError, "420"),
PaperlessInactiveOrDeletedError, (PaperlessInactiveOrDeletedError, STATE_UNAVAILABLE),
PaperlessInvalidTokenError, (PaperlessInvalidTokenError, STATE_UNAVAILABLE),
], ],
) )
async def test__statistic_sensor_state_on_error( async def test__statistic_sensor_state_on_error(
@ -84,28 +85,29 @@ async def test__statistic_sensor_state_on_error(
freezer: FrozenDateTimeFactory, freezer: FrozenDateTimeFactory,
mock_statistic_data_update, mock_statistic_data_update,
error_cls, error_cls,
assert_state,
) -> None: ) -> None:
"""Ensure sensor entities are added automatically.""" """Ensure sensor entities are added automatically."""
# simulate error # simulate error
mock_paperless.statistics.side_effect = error_cls mock_paperless.statistics.side_effect = error_cls
freezer.tick(timedelta(seconds=120)) freezer.tick(timedelta(seconds=UPDATE_INTERVAL))
async_fire_time_changed(hass) async_fire_time_changed(hass)
await hass.async_block_till_done() await hass.async_block_till_done()
state = hass.states.get("sensor.paperless_ngx_total_documents") state = hass.states.get("sensor.paperless_ngx_total_documents")
assert state.state == STATE_UNAVAILABLE assert state.state == STATE_UNAVAILABLE
# recover from error # recover from not auth errors
mock_paperless.statistics = AsyncMock( mock_paperless.statistics = AsyncMock(
return_value=Statistic.create_with_data( return_value=Statistic.create_with_data(
mock_paperless, data=mock_statistic_data_update, fetched=True mock_paperless, data=mock_statistic_data_update, fetched=True
) )
) )
freezer.tick(timedelta(seconds=120)) freezer.tick(timedelta(seconds=UPDATE_INTERVAL))
async_fire_time_changed(hass) async_fire_time_changed(hass)
await hass.async_block_till_done() await hass.async_block_till_done()
state = hass.states.get("sensor.paperless_ngx_total_documents") state = hass.states.get("sensor.paperless_ngx_total_documents")
assert state.state == "420" assert state.state == assert_state